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 07:35:31 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-08-23 07:35:31 +0000
commit63740a1b00d78441713a2b909aa40429098184d2 (patch)
treef5d05afee1d424f3f74a2099285000fd80c16798 /src/rpc.cpp
parent33f2e8a164d0b8a508dc97372a3592d126b96a1a (diff)
payout: stop grandfathered zero-hash rounds answering for each other
Every ledger lookup identified an entry by its block hash alone -- ReconcilePayouts, the payout thread's already-paid check, RecordPayoutTxid, ForfeitPayoutRecipient, ForgetPayment's still-in-flight test, and SamePayment. Entries restored from a file written before payouts were keyed by hash all carry hash 0. Two of them therefore compare equal, and the first one found answers for the second: - RecordPayoutTxid writes a txid against the wrong round. That round can then satisfy PayoutFullyPaid and be erased from the ledger while the debt it recorded is still owed, and nothing on disk remembers it. - ReconcilePayouts drops a live in-flight record on startup because a different zero-hash round had already paid that same address. SamePayoutRound() decides it properly: a non-zero hash is unique and settles the question on its own, a zero hash falls back to the block height (which LoadPendingPayouts supplies for any entry able to name its own maturity), and a zero hash never matches a real one. Refusing to match is the safe direction at all six sites, because each treats a match as "this debt is already handled". The orphan-void scan is deliberately left alone: PayoutBlockStillOnChain returns true for a zero hash, so a grandfathered entry can never enter the orphaned list and there is nothing there to cross-match. For the residual case where an entry has neither hash nor height -- which no file this tree has written can produce -- FlushMaturePayouts now refuses to explode it and logs why, rather than paying against an identity that could belong to another round. Not paying is recoverable; recording a payment against the wrong debt is not. PayoutIdForBlock's grandfathered "block-<height>" fallback is unchanged and still safe: that id is only ever matched against pool_payout_id, which no pre-upgrade transaction carries. No on-disk format change.
Diffstat (limited to 'src/rpc.cpp')
-rw-r--r--src/rpc.cpp72
1 files changed, 65 insertions, 7 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp
index 76867ab..b6b26f1 100644
--- a/src/rpc.cpp
+++ b/src/rpc.cpp
@@ -530,6 +530,42 @@ static std::string PayoutIdForBlock(int blockHeight, const uint256& blockHash)
530 blockHash.ToString().substr(0, 16).c_str()); 530 blockHash.ToString().substr(0, 16).c_str());
531} 531}
532 532
533// Do these two references name the same payout round?
534//
535// The block hash alone does not decide it, and every ledger lookup used to
536// assume it did. Entries grandfathered in from a file written before payouts
537// were keyed by hash all carry hash 0, so under a hash-only comparison the
538// first zero-hash entry answers for every other one: RecordPayoutTxid writes
539// a txid against the wrong round (which is then erased as "fully paid" while
540// the real debt still stands), and ReconcilePayouts drops a live in-flight
541// record because a *different* zero-hash round happened to pay that address.
542//
543// A non-zero hash is unique and decides on its own -- that is the whole point
544// of keying on it. A zero hash falls back to the height, which
545// LoadPendingPayouts supplies for any entry that can name its own maturity.
546// Mixed hash-ness never matches: refusing to match is the safe direction for
547// every caller here, because each of them treats a match as "this debt is
548// already dealt with".
549static bool SamePayoutRound(int aHeight, const uint256& aHash,
550 int bHeight, const uint256& bHash)
551{
552 if (aHash != bHash)
553 return false;
554 if (aHash != 0)
555 return true;
556 return aHeight == bHeight;
557}
558
559// An entry with neither a hash nor a height cannot be told apart from any
560// other entry in the same state, so nothing may be recorded against it: a
561// txid written to the wrong one reports a debt as settled that is still owed.
562// Such an entry is never paid and is logged instead -- refusing to act is the
563// safe failure, and no file this tree has ever written can produce one.
564static bool PayoutRoundIdentifiable(int height, const uint256& hash)
565{
566 return hash != 0 || height != 0;
567}
568
533static bool PayoutFullyPaid(const PendingPayout& pp) 569static bool PayoutFullyPaid(const PendingPayout& pp)
534{ 570{
535 for (const auto& kv : pp.amounts) 571 for (const auto& kv : pp.amounts)
@@ -938,7 +974,9 @@ static void ReconcilePayouts()
938 for (auto it = gPaying.begin(); it != gPaying.end(); ) { 974 for (auto it = gPaying.begin(); it != gPaying.end(); ) {
939 bool paid = false; 975 bool paid = false;
940 for (const PendingPayout& pp : gPendingPayouts) 976 for (const PendingPayout& pp : gPendingPayouts)
941 if (pp.blockHash == it->blockHash && pp.paidTxids.count(it->address)) { 977 if (SamePayoutRound(pp.blockHeight, pp.blockHash,
978 it->blockHeight, it->blockHash)
979 && pp.paidTxids.count(it->address)) {
942 paid = true; 980 paid = true;
943 break; 981 break;
944 } 982 }
@@ -1237,6 +1275,18 @@ static void FlushMaturePayouts()
1237 continue; 1275 continue;
1238 if (PayoutFullyPaid(pp)) 1276 if (PayoutFullyPaid(pp))
1239 continue; 1277 continue;
1278 // An entry with neither a hash nor a height cannot be told apart
1279 // from another in the same state, so a txid recorded against it
1280 // could settle the wrong debt. Paying it is the unsafe move;
1281 // leaving it standing and saying so is not.
1282 if (!PayoutRoundIdentifiable(pp.blockHeight, pp.blockHash)) {
1283 LogPrint("payout", "[payout] WARNING: ledger entry maturing at"
1284 " height %d names neither a block hash nor a height --"
1285 " refusing to pay %zu recipient(s) that cannot be told"
1286 " apart from another round's\n",
1287 pp.matureAtHeight, pp.amounts.size());
1288 continue;
1289 }
1240 ready.push_back(pp); 1290 ready.push_back(pp);
1241 } 1291 }
1242 } 1292 }
@@ -1285,8 +1335,8 @@ static void FlushMaturePayouts()
1285// amounts is a map keyed by address, so (block, address) is already unique. 1335// amounts is a map keyed by address, so (block, address) is already unique.
1286static bool SamePayment(const PayingRecord& a, const PayingRecord& b) 1336static bool SamePayment(const PayingRecord& a, const PayingRecord& b)
1287{ 1337{
1288 return a.matureAtHeight == b.matureAtHeight 1338 return SamePayoutRound(a.blockHeight, a.blockHash, b.blockHeight, b.blockHash)
1289 && a.blockHash == b.blockHash 1339 && a.matureAtHeight == b.matureAtHeight
1290 && a.address == b.address 1340 && a.address == b.address
1291 && a.amount == b.amount; 1341 && a.amount == b.amount;
1292} 1342}
@@ -1309,7 +1359,11 @@ static void ForgetPayment(const PayingRecord& r)
1309 // standing). 1359 // standing).
1310 bool stillInFlight = false; 1360 bool stillInFlight = false;
1311 for (const auto& e : gPaying) 1361 for (const auto& e : gPaying)
1312 if (e.blockHash == r.blockHash) { stillInFlight = true; break; } 1362 if (SamePayoutRound(e.blockHeight, e.blockHash,
1363 r.blockHeight, r.blockHash)) {
1364 stillInFlight = true;
1365 break;
1366 }
1313 if (!stillInFlight) 1367 if (!stillInFlight)
1314 gQueuedPayouts.erase(PayoutIdForBlock(r.blockHeight, r.blockHash)); 1368 gQueuedPayouts.erase(PayoutIdForBlock(r.blockHeight, r.blockHash));
1315 1369
@@ -1355,7 +1409,8 @@ static void RecordPayoutTxid(const PayingRecord& r, const std::string& txid)
1355 { 1409 {
1356 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex); 1410 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
1357 for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ++it) { 1411 for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ++it) {
1358 if (it->blockHash != r.blockHash) 1412 if (!SamePayoutRound(it->blockHeight, it->blockHash,
1413 r.blockHeight, r.blockHash))
1359 continue; 1414 continue;
1360 it->paidTxids[r.address] = txid; 1415 it->paidTxids[r.address] = txid;
1361 it->retryAfter = 0; 1416 it->retryAfter = 0;
@@ -1389,7 +1444,8 @@ static void ForfeitPayoutRecipient(const PayingRecord& r)
1389 { 1444 {
1390 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex); 1445 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
1391 for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ++it) { 1446 for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ++it) {
1392 if (it->blockHash != r.blockHash) 1447 if (!SamePayoutRound(it->blockHeight, it->blockHash,
1448 r.blockHeight, r.blockHash))
1393 continue; 1449 continue;
1394 it->amounts.erase(r.address); 1450 it->amounts.erase(r.address);
1395 if (it->amounts.empty() || PayoutFullyPaid(*it)) 1451 if (it->amounts.empty() || PayoutFullyPaid(*it))
@@ -1443,7 +1499,9 @@ static void PayoutThreadFn(void*)
1443 { 1499 {
1444 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex); 1500 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
1445 for (const PendingPayout& pp : gPendingPayouts) 1501 for (const PendingPayout& pp : gPendingPayouts)
1446 if (pp.blockHash == r.blockHash && pp.paidTxids.count(r.address)) { 1502 if (SamePayoutRound(pp.blockHeight, pp.blockHash,
1503 r.blockHeight, r.blockHash)
1504 && pp.paidTxids.count(r.address)) {
1447 alreadyPaid = true; 1505 alreadyPaid = true;
1448 break; 1506 break;
1449 } 1507 }