diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-08-23 11:41:28 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-08-23 11:41:28 +0000 |
| commit | 9d6fcb2058d90e3ecfb011e2168e4eee04e3a90a (patch) | |
| tree | 334e7c0297c858c07f1f42037ac96752a4aede0e /src/main.cpp | |
| parent | d273a04ff89233f0f01431bd6e3c18d5261818b0 (diff) | |
payout: ask the wallet why a send failed instead of guessing from the balance
A payment that the pool could not fund was being recorded as a fault
against the payment, and 1440 faults erased the debt with the coin still
in the operator's wallet.
The classifier read GetBalance() after the failure and asked whether it
covered `amount + nTransactionFee + CENT`. Every term of that was wrong.
nTransactionFee is 0 on this pool -- it is only ever assigned from the
GUI preferences dialog, which a headless node does not have -- so the
entire reserve was one CENT. The fee it is meant to reserve for is
GetMinFee(fDiscount=true), which is a step function of the serialised
size: 0 below 10,000 bytes and (1 + bytes/1000) * CENT at or above it.
There is no required-fee value between 1 and 11 CENT, so a one-CENT
reserve is never the right number -- below the step nothing needs
reserving, at the step it is short by a factor of eleven.
Worse, the trigger correlates with the failure. A pool wallet close to
broke selects many small inputs BECAUSE it is close to broke, and about
56 signed P2PKH inputs is 10 KB, which a pool that has been paying out
accumulates as change. So: balance of amount + 0.05, the pre-send gate
passes, CreateTransaction builds a transaction over 10 KB, the fee
becomes 0.11, SelectCoins(amount + 0.11) fails, SendMoney returns false
-- and (amount + 0.05) < (amount + 0 + 0.01) is false, so "funded", so a
fault. The condition is stable, so it recurred every 60 s until the
budget ran out and ForfeitPayoutRecipient erased the miner's money.
Three changes.
The wallet now says what it did. CreateTransaction takes an optional
WalletSendResult out-parameter and sets WALLET_SEND_INSUFFICIENT_FUNDS
on the one return that means a funding shortfall -- SelectCoins failing
to cover nValue plus whatever fee that pass is working with, which on
the second and later passes is GetMinFee() of the transaction the first
pass actually built. SendMoney gains a four-argument overload returning
that plus the fee that was required. The three-argument SendMoney stays,
as a separate declaration rather than a default argument, because
gui.cpp declares it itself instead of including main.h; walletcmd.cpp
and the GUI are unchanged. These are wallet functions, not consensus.
ClassifyPayoutSendFailure() then classifies the answer rather than
guessing: locked or short of coin defers, anything else is a fault.
There is no reserve left to get wrong, and the size-dependent fee is
accounted for exactly, because the wallet computed it.
And the give-up branch is gone. 1440 failures is not a decidable state,
it is a count of how long we have been asking, and this pool reached it
by misclassifying a permanent funding shortfall. A payment that keeps
failing now stays in flight, keeps its ledger entry, keeps being
reported as owed in pool_status.json, and keeps retrying once a minute.
A debt that is never written off cannot be written off wrongly. The
remaining call to ForfeitPayoutRecipient is the one decidable case --
the ledger names a string that is not a valid address, so no transaction
can pay it -- which previously only dropped the in-flight record and
left the ledger and the round proof claiming the debt forever.
Tested against a real wallet and the real CreateTransaction, not against
injected values, because the honest question is whether a wallet holding
many small coins reports the state this code assumes. 60 coins of 1 BTF,
balance 60.00, amount 59.95: CreateTransaction returns false with
WALLET_SEND_INSUFFICIENT_FUNDS and a required fee of 0.11 -- and since
GetMinFee returns 0 below 10,000 bytes, a required fee of 11 CENT is
itself the measurement that the transaction crossed the step. The same
test asserts that the old reserve judged that state "funded", and that
the same wallet pays a 3-input transaction with a required fee of 0.00,
which is why no fixed reserve is right for both.
Property: a send that failed because the wallet could not fund it is
deferred, whatever the transaction's size, and no number of failures
erases a debt.
Diffstat (limited to 'src/main.cpp')
| -rw-r--r-- | src/main.cpp | 47 |
1 files changed, 43 insertions, 4 deletions
diff --git a/src/main.cpp b/src/main.cpp index 252911b..7b3e555 100644 --- a/src/main.cpp +++ b/src/main.cpp | |||
| @@ -4907,9 +4907,17 @@ bool SelectCoins(int64 nTargetValue, set<CWalletTx*>& setCoinsRet) | |||
| 4907 | 4907 | ||
| 4908 | 4908 | ||
| 4909 | 4909 | ||
| 4910 | bool CreateTransaction(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew, int64& nFeeRequiredRet) | 4910 | // presultRet, when given, says WHY this returned false: the caller cannot work |
| 4911 | // it out afterwards. "Coin selection came up short" and "the wallet could not | ||
| 4912 | // build the transaction" are indistinguishable from the outside -- both are | ||
| 4913 | // just `false` -- and the difference decides whether a pool defers a payment | ||
| 4914 | // or counts it against the payment's retry budget. See WalletSendResult. | ||
| 4915 | bool CreateTransaction(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew, int64& nFeeRequiredRet, | ||
| 4916 | WalletSendResult* presultRet) | ||
| 4911 | { | 4917 | { |
| 4912 | nFeeRequiredRet = 0; | 4918 | nFeeRequiredRet = 0; |
| 4919 | if (presultRet) | ||
| 4920 | *presultRet = WALLET_SEND_FAILED; | ||
| 4913 | CRITICAL_BLOCK(cs_main) | 4921 | CRITICAL_BLOCK(cs_main) |
| 4914 | { | 4922 | { |
| 4915 | // txdb must be opened before the mapWallet lock | 4923 | // txdb must be opened before the mapWallet lock |
| @@ -4940,10 +4948,21 @@ bool CreateTransaction(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew, in | |||
| 4940 | // fixed this in 107d9e288. | 4948 | // fixed this in 107d9e288. |
| 4941 | int64 nTotalValue = nValue + nFee; | 4949 | int64 nTotalValue = nValue + nFee; |
| 4942 | 4950 | ||
| 4943 | // Choose coins to use | 4951 | // Choose coins to use. |
| 4952 | // | ||
| 4953 | // Failing here is a funding shortfall and nothing else: there | ||
| 4954 | // is not enough spendable coin for nValue plus the fee this | ||
| 4955 | // pass is working with. On the second and later passes that | ||
| 4956 | // fee is GetMinFee() of the transaction the first pass built, | ||
| 4957 | // so this is also how a wallet that can afford the amount but | ||
| 4958 | // not the 11 * CENT charged at 10,000 bytes reports itself. | ||
| 4944 | set<CWalletTx*> setCoins; | 4959 | set<CWalletTx*> setCoins; |
| 4945 | if (!SelectCoins(nTotalValue, setCoins)) | 4960 | if (!SelectCoins(nTotalValue, setCoins)) |
| 4961 | { | ||
| 4962 | if (presultRet) | ||
| 4963 | *presultRet = WALLET_SEND_INSUFFICIENT_FUNDS; | ||
| 4946 | return false; | 4964 | return false; |
| 4965 | } | ||
| 4947 | int64 nValueIn = 0; | 4966 | int64 nValueIn = 0; |
| 4948 | foreach(CWalletTx* pcoin, setCoins) | 4967 | foreach(CWalletTx* pcoin, setCoins) |
| 4949 | nValueIn += pcoin->GetCredit(); | 4968 | nValueIn += pcoin->GetCredit(); |
| @@ -5013,6 +5032,8 @@ bool CreateTransaction(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew, in | |||
| 5013 | } | 5032 | } |
| 5014 | } | 5033 | } |
| 5015 | } | 5034 | } |
| 5035 | if (presultRet) | ||
| 5036 | *presultRet = WALLET_SEND_OK; | ||
| 5016 | return true; | 5037 | return true; |
| 5017 | } | 5038 | } |
| 5018 | 5039 | ||
| @@ -5075,13 +5096,24 @@ bool CommitTransactionSpent(const CWalletTx& wtxNew) | |||
| 5075 | 5096 | ||
| 5076 | 5097 | ||
| 5077 | 5098 | ||
| 5078 | bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew) | 5099 | // outcomeRet reports what happened rather than only whether it worked. A |
| 5100 | // caller that has to decide what a failure MEANS -- the pool payout thread -- | ||
| 5101 | // cannot reconstruct it from a balance reading afterwards, because the fee | ||
| 5102 | // that was actually required depends on the size of a transaction that was | ||
| 5103 | // never built. See WalletSendResult. | ||
| 5104 | bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew, | ||
| 5105 | SendMoneyOutcome& outcomeRet) | ||
| 5079 | { | 5106 | { |
| 5107 | outcomeRet.result = WALLET_SEND_FAILED; | ||
| 5108 | outcomeRet.nFeeRequired = 0; | ||
| 5080 | CRITICAL_BLOCK(cs_main) | 5109 | CRITICAL_BLOCK(cs_main) |
| 5081 | { | 5110 | { |
| 5082 | int64 nFeeRequired; | 5111 | int64 nFeeRequired; |
| 5083 | if (!CreateTransaction(scriptPubKey, nValue, wtxNew, nFeeRequired)) | 5112 | WalletSendResult createResult = WALLET_SEND_FAILED; |
| 5113 | if (!CreateTransaction(scriptPubKey, nValue, wtxNew, nFeeRequired, &createResult)) | ||
| 5084 | { | 5114 | { |
| 5115 | outcomeRet.result = createResult; | ||
| 5116 | outcomeRet.nFeeRequired = nFeeRequired; | ||
| 5085 | string strError; | 5117 | string strError; |
| 5086 | if (nValue + nFeeRequired > GetBalance()) | 5118 | if (nValue + nFeeRequired > GetBalance()) |
| 5087 | // Not an oversized transaction -- this branch is simply "the | 5119 | // Not an oversized transaction -- this branch is simply "the |
| @@ -5119,6 +5151,13 @@ bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew) | |||
| 5119 | } | 5151 | } |
| 5120 | wtxNew.RelayWalletTransaction(); | 5152 | wtxNew.RelayWalletTransaction(); |
| 5121 | } | 5153 | } |
| 5154 | outcomeRet.result = WALLET_SEND_OK; | ||
| 5122 | MainFrameRepaint(); | 5155 | MainFrameRepaint(); |
| 5123 | return true; | 5156 | return true; |
| 5124 | } | 5157 | } |
| 5158 | |||
| 5159 | bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew) | ||
| 5160 | { | ||
| 5161 | SendMoneyOutcome ignored; | ||
| 5162 | return SendMoney(scriptPubKey, nValue, wtxNew, ignored); | ||
| 5163 | } | ||