diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-08-23 07:38:39 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-08-23 07:38:39 +0000 |
| commit | 489be141ee3be1c355650e1429c3f830eb8e442d (patch) | |
| tree | 45ee1d997d78298addd2308082850e569c63087d /src/wallet_sqlite.cpp | |
| parent | 63740a1b00d78441713a2b909aa40429098184d2 (diff) | |
payout: one in-flight record per payment, and give it a real identity
A restart duplicated every in-flight payment, deterministically, and the
duplicates were load-bearing.
gQueuedPayouts is memory-only. ReconcilePayouts restored gPaying from disk but
left that set empty, and FlushMaturePayouts deduped only against it and
against the ledger's paidTxids -- never against gPaying itself. So a round
that had been exploded but not yet paid got a SECOND PayingRecord for every
unpaid recipient on the first flush after every restart. Another restart added
a third. The crash test saw exactly that: two byte-identical records for one
(block, address).
ForgetPayment and ReschedulePayment then found their record with SamePayment(),
a field-by-field comparison, which stops being an identity the moment two
records are equal. Every update landed in the first slot and the second was
never touched:
- the duplicate's retryAfter stayed 0, so it ran in every 500 ms batch
instead of once a minute;
- each pass wrote retries = 0+1 into the FIRST slot, resetting its count, so
the 1440-retry budget never completed and forfeit was unreachable;
- the duplicate's fProbeWallet was never cleared, so FindRecordedPoolPayout
-- a full mapWallet scan under cs_mapWallet -- ran twice a second per stuck
recipient, forever, along with GetBalance() and, whenever funds allowed,
SendMoney/CreateTransaction under cs_main.
That is a permanent hot loop holding cs_mapWallet and cs_main on a pool node,
starving RebuildJob() so job refresh and block submission stall. It is reached
by the ordinary state of a payment waiting for coin or for an unlocked wallet.
It did not double-pay only because a slot can reach SendMoney unprobed only
when fProbeWallet is already false, and any successful payment calls
ForgetPayment, which erases exactly the one slot that could be in that state.
That invariant is emergent, undocumented, and dies the moment anyone erases by
index or gives the record a stable identity -- which is precisely what this
commit does, so it is replaced by a structural one.
Both halves are fixed.
Not creating duplicates:
- ReconcilePayouts rebuilds gQueuedPayouts from the restored gPaying, so the
first flush after a restart no longer re-explodes rounds already in flight;
- FlushMaturePayouts additionally refuses to append a record for a
(round, address) that gPaying already holds. That check is the invariant
proper -- the round-level set is an optimisation that can be stale, gPaying
is the list being protected;
- LoadPaying collapses duplicates already on disk as it reads them, merging
the retry and deferral counts upwards, so a file written by the broken
build does not inherit the pathology into the fixed one.
Real identity: PayingRecord gains an id, assigned under gPayoutExecMutex, and
ForgetPayment/ReschedulePayment match on it instead of on values. SamePayment
is deleted. The id is process-local and deliberately not persisted -- gPaying
is rebuilt from disk at every start, so it only has to name a slot within one
run, and reading ids back would mean trusting a file to contain no duplicates.
The exactly-once property is now the conjunction of four checks that each hold
on their own, rather than one accident:
1. at most one gPaying record per (round, address), enforced at insert;
2. the ledger's paidTxids, re-checked at the top of every attempt;
3. the wallet probe, once per record, before its first send;
4. state updates addressed by record id, so no slot can be skipped.
No on-disk format change: the id is not written, and duplicate records in an
existing file are read and collapsed.
Diffstat (limited to 'src/wallet_sqlite.cpp')
0 files changed, 0 insertions, 0 deletions