upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/rpc.cpp
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-08-23 11:50:12 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-08-23 11:50:12 +0000
commit46864cdf3c90d15d5d21a330bb54f43696aa7d3b (patch)
tree213ce88d42cbc207448c14335c3263a140e40902 /src/rpc.cpp
parente960802fa7149c9f997074ec7c1a7af66fb47e0f (diff)
payout: say why a grandfathered pair is safe, since it is not the orphan check
The comment on FindRecordedPoolPayout justified the legacy fallback by saying that where two pre-upgrade rounds share a height, the losing one's ledger entry is voided by the orphan check before it can be paid. That check is PayoutBlockStillOnChain(), which returns true for a zero hash -- and every pre-upgrade entry has a zero hash, because they were written before payouts recorded one. So it is a no-op for exactly the population the fallback exists to serve. The stated reason for a safety property was not the reason. The property does hold, by a different mechanism. SamePayoutRound() treats two zero-hash entries at the same height as the same round, so the pair is indistinguishable to every consumer, not just to this one: FlushMaturePayouts explodes one and skips the other as already queued, PaymentInFlightLocked refuses a second record for that (round, address), and RecordPayoutTxid settles against the first match. The pair presents as one debt and can be paid at most once. The cost of that -- only one of a genuine same-height pair would ever be paid -- is now written down too, rather than left as an implication. Both halves are asserted rather than reasoned about: the self-test checks that the orphan check passes a zero hash, and that two identical zero-hash entries at one height produce exactly one in-flight record. Property: the comment describes the mechanism that is actually load-bearing, and a test fails if either half of it stops being true.
Diffstat (limited to 'src/rpc.cpp')
-rw-r--r--src/rpc.cpp58
1 files changed, 53 insertions, 5 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp
index 7b45fea..7ea0597 100644
--- a/src/rpc.cpp
+++ b/src/rpc.cpp
@@ -1379,11 +1379,32 @@ static void MarkPoolRoundForfeited(const std::string& id,
1379// 1379//
1380// What is left unresolved by construction is two pre-upgrade rounds at the 1380// What is left unresolved by construction is two pre-upgrade rounds at the
1381// SAME height paying the same address the same amount, since the comment 1381// SAME height paying the same address the same amount, since the comment
1382// carries only the height. That needs a reorg, and the losing round's ledger 1382// carries only the height. That needs a reorg.
1383// entry is voided by the orphan check before it can be paid, so the pair 1383//
1384// cannot both be live. Where the evidence genuinely runs out the fallback 1384// This used to say the losing twin's ledger entry is voided by the orphan
1385// simply does not match, and the payment is made -- paying a debt we cannot 1385// check before it can be paid. That is wrong: PayoutBlockStillOnChain()
1386// prove was settled is the correct default for a pool. 1386// returns true for a zero hash, and EVERY pre-upgrade entry has a zero hash,
1387// so the orphan check is a no-op for exactly the population this fallback
1388// serves.
1389//
1390// The outcome is still safe, by a different mechanism. SamePayoutRound()
1391// treats two zero-hash entries at the same height as the SAME round, so the
1392// pair is indistinguishable everywhere, not only here: FlushMaturePayouts
1393// explodes one of them and skips the other as already queued,
1394// PaymentInFlightLocked refuses a second record for that (round, address), and
1395// RecordPayoutTxid settles against the first match. The pair therefore
1396// presents as ONE debt and can be paid at most once -- which is the direction
1397// that matters for a fallback whose failure mode is treating one payment as
1398// proof of two.
1399//
1400// The cost of that mechanism, stated plainly: only one of a genuine
1401// same-height pair would ever be paid. It takes a reorg AND both rounds
1402// predating hash keying, and no file this tree has written since can produce
1403// one, so it is a property of history rather than a live risk.
1404//
1405// Where the evidence genuinely runs out the fallback simply does not match,
1406// and the payment is made -- paying a debt we cannot prove was settled is the
1407// correct default for a pool.
1387enum PayoutProbeResult { 1408enum PayoutProbeResult {
1388 PAYOUT_PROBE_NONE = 0, // the wallet holds no record of this payment 1409 PAYOUT_PROBE_NONE = 0, // the wallet holds no record of this payment
1389 PAYOUT_PROBE_STRUCTURED, // matched this build's pool_payout_* metadata 1410 PAYOUT_PROBE_STRUCTURED, // matched this build's pool_payout_* metadata
@@ -3594,6 +3615,33 @@ int RunPayoutLedgerSelfTest()
3594 PayoutRoundIdentifiable(7, 0), 3615 PayoutRoundIdentifiable(7, 0),
3595 "an entry with neither hash nor height is not adjudicable"); 3616 "an entry with neither hash nor height is not adjudicable");
3596 3617
3618 // What actually makes a grandfathered same-height pair safe, since the
3619 // comment on FindRecordedPoolPayout used to claim it was the orphan
3620 // check. It is not: the orphan check passes every zero-hash entry, and
3621 // every pre-upgrade entry has a zero hash.
3622 check(PayoutBlockStillOnChain(0),
3623 "the orphan check passes every grandfathered entry, so it cannot"
3624 " be what voids a losing twin");
3625 PayoutTestResetState();
3626 remove(ledgerPath.c_str());
3627 remove(payingPath.c_str());
3628 {
3629 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
3630 PendingPayout twin;
3631 twin.blockHeight = 300; twin.blockHash = 0; twin.matureAtHeight = 300;
3632 twin.amounts[addrA] = 1 * COIN;
3633 gPendingPayouts.push_back(twin);
3634 gPendingPayouts.push_back(twin); // the losing side of a reorg
3635 }
3636 nBestHeight = 5000;
3637 FlushMaturePayouts();
3638 {
3639 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
3640 check(gPaying.size() == 1,
3641 "two grandfathered rounds at one height are one round, so the"
3642 " pair presents as one debt and is paid at most once");
3643 }
3644
3597 // End to end: recording a payment against one grandfathered round must 3645 // End to end: recording a payment against one grandfathered round must
3598 // not settle the other's debt. 3646 // not settle the other's debt.
3599 PayoutTestResetState(); 3647 PayoutTestResetState();