upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/main.cpp
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-08-23 11:41:28 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-08-23 11:41:28 +0000
commit9d6fcb2058d90e3ecfb011e2168e4eee04e3a90a (patch)
tree334e7c0297c858c07f1f42037ac96752a4aede0e /src/main.cpp
parentd273a04ff89233f0f01431bd6e3c18d5261818b0 (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.cpp47
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
4910bool 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.
4915bool 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
5078bool 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.
5104bool 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
5159bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew)
5160{
5161 SendMoneyOutcome ignored;
5162 return SendMoney(scriptPubKey, nValue, wtxNew, ignored);
5163}