diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-08-23 07:35:31 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-08-23 07:35:31 +0000 |
| commit | 63740a1b00d78441713a2b909aa40429098184d2 (patch) | |
| tree | f5d05afee1d424f3f74a2099285000fd80c16798 /src/rpc.cpp | |
| parent | 33f2e8a164d0b8a508dc97372a3592d126b96a1a (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.cpp | 72 |
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". | ||
| 549 | static 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. | ||
| 564 | static bool PayoutRoundIdentifiable(int height, const uint256& hash) | ||
| 565 | { | ||
| 566 | return hash != 0 || height != 0; | ||
| 567 | } | ||
| 568 | |||
| 533 | static bool PayoutFullyPaid(const PendingPayout& pp) | 569 | static 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. |
| 1286 | static bool SamePayment(const PayingRecord& a, const PayingRecord& b) | 1336 | static 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 | } |