upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/main.h
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.h
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.h')
-rw-r--r--src/main.h45
1 files changed, 44 insertions, 1 deletions
diff --git a/src/main.h b/src/main.h
index 8bb536b..382dd9c 100644
--- a/src/main.h
+++ b/src/main.h
@@ -342,9 +342,52 @@ struct WalletRecoveryAudit
342 } 342 }
343}; 343};
344WalletRecoveryAudit GetWalletRecoveryAudit(); 344WalletRecoveryAudit GetWalletRecoveryAudit();
345bool CreateTransaction(CScript scriptPubKey, int64 nValue, CWalletTx& txNew, int64& nFeeRequiredRet); 345
346// Why a send did not go through.
347//
348// The pool's payout thread has to decide, on every failed SendMoney, whether
349// the failure was the wallet's condition (wait for it to change) or a fault
350// (something is wrong with this payment). It used to guess by re-reading
351// GetBalance() afterwards and allowing a fixed CENT of headroom for the fee.
352// That cannot work: the fee here is a step function of the SERIALISED SIZE of
353// a transaction that no longer exists, and the step is from 0 to 11 * CENT at
354// 10,000 bytes -- so a one-CENT allowance is either unnecessary or an order of
355// magnitude short, never right. A pool wallet close to broke selects many
356// small inputs BECAUSE it is close to broke, which is what pushes the
357// transaction over 10 KB, so the guess is wrong exactly when the money is
358// tight.
359//
360// The wallet already knows the answer; it just did not say. SelectCoins()
361// failing is "there is not enough spendable coin to cover the amount plus
362// whatever fee this size of transaction requires", and it is the only reason
363// CreateTransaction() can fail that is about funding rather than about the
364// wallet being broken.
365enum WalletSendResult
366{
367 WALLET_SEND_OK = 0,
368 WALLET_SEND_INSUFFICIENT_FUNDS, // coin selection could not cover amount+fee
369 WALLET_SEND_FAILED // anything else: could not build, sign or accept
370};
371
372// What the wallet did with a send request. nFeeRequired is the fee the
373// transaction turned out to need, which for a failed send is what coin
374// selection was asked for on top of the amount and could not find -- 0 if it
375// never got that far.
376struct SendMoneyOutcome
377{
378 WalletSendResult result = WALLET_SEND_OK;
379 int64 nFeeRequired = 0;
380};
381
382bool CreateTransaction(CScript scriptPubKey, int64 nValue, CWalletTx& txNew, int64& nFeeRequiredRet,
383 WalletSendResult* presultRet=NULL);
346bool CommitTransactionSpent(const CWalletTx& wtxNew); 384bool CommitTransactionSpent(const CWalletTx& wtxNew);
385// The three-argument form is kept, and kept as a distinct declaration rather
386// than a defaulted argument, because gui.cpp declares SendMoney itself instead
387// of including this header.
347bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew); 388bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew);
389bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew,
390 SendMoneyOutcome& outcomeRet);
348 391
349 392
350 393