diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-02 08:59:21 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-02 08:59:40 +0000 |
| commit | d8eb3aeec198e9e6753b89a53d25f4f91f8ab3d6 (patch) | |
| tree | 602a3399828d2dc542a3cab7e7f85f92a6ca3518 | |
| parent | a2e1f27b32442bcea92eeda3ac09432f6c5ec2c5 (diff) | |
payout: refuse comment-only evidence for a round that has a proof
A pre-upgrade payout comment names only the height, so when a height is
won, orphaned, and won again, the old transaction matches the new round's
(height, address, amount) exactly and the miner is never paid. Round
proofs decide the ambiguity where it is decidable: a proof is written when
a round is created, and structured payout metadata (upstream 0b01d6c)
predates round proofs (upstream b1eb8b6), so a round with a proof was
created by a build whose every payment names its round -- a comment-only
match can only be another round's money. Skipping the fallback cannot
double-pay: any real payment for such a round is already found by the
structured probe. Proofless rounds keep the documented bias.
| -rw-r--r-- | src/rpc.cpp | 161 |
1 files changed, 139 insertions, 22 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp index dbbf06d..310c0d4 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp | |||
| @@ -1369,6 +1369,24 @@ static void MarkPoolRoundForfeited(const std::string& id, | |||
| 1369 | PublishPoolRounds(snap); | 1369 | PublishPoolRounds(snap); |
| 1370 | } | 1370 | } |
| 1371 | 1371 | ||
| 1372 | // Does this round have a published round proof? | ||
| 1373 | // | ||
| 1374 | // Existence answers a temporal question the legacy fallback below needs: | ||
| 1375 | // which builds could have paid this round. RecordPoolRound writes the proof | ||
| 1376 | // when the round is created, and structured payout metadata (upstream | ||
| 1377 | // cbdd06d) is an ancestor of round proofs (upstream 83ba822) -- no build | ||
| 1378 | // able to write a proof has ever sent a payment that only a comment can | ||
| 1379 | // identify. A round with a proof therefore cannot have a comment-only | ||
| 1380 | // payment; one that matches it is another round's, colliding on the height. | ||
| 1381 | static bool PoolRoundHasProof(const std::string& id) | ||
| 1382 | { | ||
| 1383 | std::lock_guard<std::mutex> lk(gPoolRoundsMutex); | ||
| 1384 | for (const PoolRoundProof& r : gPoolRounds) | ||
| 1385 | if (r.id == id) | ||
| 1386 | return true; | ||
| 1387 | return false; | ||
| 1388 | } | ||
| 1389 | |||
| 1372 | // Which wallet transaction, if any, already paid this? | 1390 | // Which wallet transaction, if any, already paid this? |
| 1373 | // | 1391 | // |
| 1374 | // Two kinds of evidence, in order of quality. | 1392 | // Two kinds of evidence, in order of quality. |
| @@ -1404,11 +1422,22 @@ static void MarkPoolRoundForfeited(const std::string& id, | |||
| 1404 | // (round, address) pair -- from the ledger's paidTxids, which is durable, | 1422 | // (round, address) pair -- from the ledger's paidTxids, which is durable, |
| 1405 | // and from what this process has already claimed. One legacy transaction | 1423 | // and from what this process has already claimed. One legacy transaction |
| 1406 | // can therefore settle at most one debt, so N transactions can never | 1424 | // can therefore settle at most one debt, so N transactions can never |
| 1407 | // satisfy more than N obligations. | 1425 | // satisfy more than N obligations; |
| 1426 | // | ||
| 1427 | // 4. it is not consulted at all for a round that has a round proof | ||
| 1428 | // (fAllowLegacy, decided by the caller from PoolRoundHasProof). Proofs | ||
| 1429 | // are written when a round is created, and only by builds whose every | ||
| 1430 | // payment names its round exactly -- so a comment-only transaction | ||
| 1431 | // cannot be a payment for such a round, only a same-height collision | ||
| 1432 | // from an older one, and skipping the fallback cannot hide a real | ||
| 1433 | // payment: the structured probe above already found any that exist. | ||
| 1408 | // | 1434 | // |
| 1409 | // What is left unresolved by construction is two pre-upgrade rounds at the | 1435 | // What is left unresolved by construction is a comment-only transaction and |
| 1410 | // SAME height paying the same address the same amount, since the comment | 1436 | // a PROOFLESS round at the same height paying the same address the same |
| 1411 | // carries only the height. That needs a reorg. | 1437 | // amount: two grandfathered zero-hash twins, or a twin pair from the builds |
| 1438 | // that keyed the ledger by hash before any metadata reached the wallet. The | ||
| 1439 | // comment carries only the height, so nothing ties the transaction to one | ||
| 1440 | // twin rather than the other. That needs a reorg. | ||
| 1412 | // | 1441 | // |
| 1413 | // This used to say the losing twin's ledger entry is voided by the orphan | 1442 | // This used to say the losing twin's ledger entry is voided by the orphan |
| 1414 | // check before it can be paid. That is wrong: PayoutBlockStillOnChain() | 1443 | // check before it can be paid. That is wrong: PayoutBlockStillOnChain() |
| @@ -1445,6 +1474,7 @@ static PayoutProbeResult FindRecordedPoolPayout(const std::string& id, | |||
| 1445 | const uint256& blockHash, | 1474 | const uint256& blockHash, |
| 1446 | const std::string& address, | 1475 | const std::string& address, |
| 1447 | int64 amount, | 1476 | int64 amount, |
| 1477 | bool fAllowLegacy, | ||
| 1448 | const std::set<std::string>& excluded, | 1478 | const std::set<std::string>& excluded, |
| 1449 | std::string& txidRet) | 1479 | std::string& txidRet) |
| 1450 | { | 1480 | { |
| @@ -1505,6 +1535,8 @@ static PayoutProbeResult FindRecordedPoolPayout(const std::string& id, | |||
| 1505 | return PAYOUT_PROBE_STRUCTURED; | 1535 | return PAYOUT_PROBE_STRUCTURED; |
| 1506 | } | 1536 | } |
| 1507 | 1537 | ||
| 1538 | if (!fAllowLegacy) | ||
| 1539 | continue; // comment-only evidence is inadmissible: rule 4 | ||
| 1508 | if (!fHaveScript) | 1540 | if (!fHaveScript) |
| 1509 | continue; | 1541 | continue; |
| 1510 | // The comment is written only by the payout path, so its presence | 1542 | // The comment is written only by the payout path, so its presence |
| @@ -2142,10 +2174,15 @@ static void PayoutThreadFn(void*) | |||
| 2142 | std::set<std::string> excluded; | 2174 | std::set<std::string> excluded; |
| 2143 | CollectForeignPayoutTxids(r.blockHeight, r.blockHash, r.address, | 2175 | CollectForeignPayoutTxids(r.blockHeight, r.blockHash, r.address, |
| 2144 | claimKey, excluded); | 2176 | claimKey, excluded); |
| 2177 | // A round with a published proof was created by a build whose | ||
| 2178 | // every payment names its round exactly, so only structured | ||
| 2179 | // evidence can settle it -- a comment match would be another | ||
| 2180 | // round's payment colliding on the height. | ||
| 2181 | const bool fAllowLegacy = !PoolRoundHasProof(id); | ||
| 2145 | r.fProbeWallet = false; | 2182 | r.fProbeWallet = false; |
| 2146 | PayoutProbeResult probe = FindRecordedPoolPayout( | 2183 | PayoutProbeResult probe = FindRecordedPoolPayout( |
| 2147 | id, r.blockHeight, r.blockHash, r.address, r.amount, | 2184 | id, r.blockHeight, r.blockHash, r.address, r.amount, |
| 2148 | excluded, recovered); | 2185 | fAllowLegacy, excluded, recovered); |
| 2149 | if (probe != PAYOUT_PROBE_NONE) { | 2186 | if (probe != PAYOUT_PROBE_NONE) { |
| 2150 | if (probe == PAYOUT_PROBE_LEGACY) { | 2187 | if (probe == PAYOUT_PROBE_LEGACY) { |
| 2151 | // Loud on purpose: this is the upgrade path, where the | 2188 | // Loud on purpose: this is the upgrade path, where the |
| @@ -3880,31 +3917,34 @@ int RunPayoutLedgerSelfTest() | |||
| 3880 | const std::string comment = LegacyPayoutComment(height); | 3917 | const std::string comment = LegacyPayoutComment(height); |
| 3881 | std::set<std::string> none; | 3918 | std::set<std::string> none; |
| 3882 | std::string found; | 3919 | std::string found; |
| 3920 | // No round proof exists for these rounds, so legacy evidence is | ||
| 3921 | // admissible -- computed exactly as the payout thread computes it. | ||
| 3922 | const bool fAllowLegacy = !PoolRoundHasProof(id); | ||
| 3883 | 3923 | ||
| 3884 | check(FindRecordedPoolPayout(id, height, 0, addrA, amount, none, found) | 3924 | check(FindRecordedPoolPayout(id, height, 0, addrA, amount, fAllowLegacy, none, found) |
| 3885 | == PAYOUT_PROBE_NONE, | 3925 | == PAYOUT_PROBE_NONE, |
| 3886 | "an empty wallet is not evidence that anything was paid"); | 3926 | "an empty wallet is not evidence that anything was paid"); |
| 3887 | 3927 | ||
| 3888 | const std::string legacyTxid = | 3928 | const std::string legacyTxid = |
| 3889 | PayoutTestAddWalletTx(comment, addrA, amount, 1, ""); | 3929 | PayoutTestAddWalletTx(comment, addrA, amount, 1, ""); |
| 3890 | check(FindRecordedPoolPayout(id, height, 0, addrA, amount, none, found) | 3930 | check(FindRecordedPoolPayout(id, height, 0, addrA, amount, fAllowLegacy, none, found) |
| 3891 | == PAYOUT_PROBE_LEGACY && found == legacyTxid, | 3931 | == PAYOUT_PROBE_LEGACY && found == legacyTxid, |
| 3892 | "a payment carrying only a pre-upgrade comment is recognised as" | 3932 | "a payment carrying only a pre-upgrade comment is recognised as" |
| 3893 | " already paid"); | 3933 | " already paid"); |
| 3894 | 3934 | ||
| 3895 | check(FindRecordedPoolPayout(id, height, 0, addrB, amount, none, found) | 3935 | check(FindRecordedPoolPayout(id, height, 0, addrB, amount, fAllowLegacy, none, found) |
| 3896 | == PAYOUT_PROBE_NONE, | 3936 | == PAYOUT_PROBE_NONE, |
| 3897 | "the legacy match requires the output to pay THIS recipient"); | 3937 | "the legacy match requires the output to pay THIS recipient"); |
| 3898 | check(FindRecordedPoolPayout(id, height, 0, addrA, amount + COIN, none, found) | 3938 | check(FindRecordedPoolPayout(id, height, 0, addrA, amount + COIN, fAllowLegacy, none, found) |
| 3899 | == PAYOUT_PROBE_NONE, | 3939 | == PAYOUT_PROBE_NONE, |
| 3900 | "the legacy match requires the output to pay exactly this amount"); | 3940 | "the legacy match requires the output to pay exactly this amount"); |
| 3901 | check(FindRecordedPoolPayout(id, height + 1, 0, addrA, amount, none, found) | 3941 | check(FindRecordedPoolPayout(id, height + 1, 0, addrA, amount, fAllowLegacy, none, found) |
| 3902 | == PAYOUT_PROBE_NONE, | 3942 | == PAYOUT_PROBE_NONE, |
| 3903 | "the legacy match requires the comment for this block height"); | 3943 | "the legacy match requires the comment for this block height"); |
| 3904 | 3944 | ||
| 3905 | std::set<std::string> spent; | 3945 | std::set<std::string> spent; |
| 3906 | spent.insert(legacyTxid); | 3946 | spent.insert(legacyTxid); |
| 3907 | check(FindRecordedPoolPayout(id, height, 0, addrA, amount, spent, found) | 3947 | check(FindRecordedPoolPayout(id, height, 0, addrA, amount, fAllowLegacy, spent, found) |
| 3908 | == PAYOUT_PROBE_NONE, | 3948 | == PAYOUT_PROBE_NONE, |
| 3909 | "one legacy transaction cannot settle a second debt"); | 3949 | "one legacy transaction cannot settle a second debt"); |
| 3910 | 3950 | ||
| @@ -3913,7 +3953,7 @@ int RunPayoutLedgerSelfTest() | |||
| 3913 | // exactly what the structured metadata exists to prevent. | 3953 | // exactly what the structured metadata exists to prevent. |
| 3914 | const std::string otherId = PayoutIdForBlock(height, 0) + "-other"; | 3954 | const std::string otherId = PayoutIdForBlock(height, 0) + "-other"; |
| 3915 | PayoutTestAddWalletTx(comment, addrB, amount, 2, otherId); | 3955 | PayoutTestAddWalletTx(comment, addrB, amount, 2, otherId); |
| 3916 | check(FindRecordedPoolPayout(id, height, 0, addrB, amount, none, found) | 3956 | check(FindRecordedPoolPayout(id, height, 0, addrB, amount, fAllowLegacy, none, found) |
| 3917 | == PAYOUT_PROBE_NONE, | 3957 | == PAYOUT_PROBE_NONE, |
| 3918 | "another round's structured payment is not legacy evidence for" | 3958 | "another round's structured payment is not legacy evidence for" |
| 3919 | " this one"); | 3959 | " this one"); |
| @@ -3921,7 +3961,7 @@ int RunPayoutLedgerSelfTest() | |||
| 3921 | // The structured path still wins where it applies. | 3961 | // The structured path still wins where it applies. |
| 3922 | const std::string structuredTxid = | 3962 | const std::string structuredTxid = |
| 3923 | PayoutTestAddWalletTx(comment, addrB, amount, 3, id); | 3963 | PayoutTestAddWalletTx(comment, addrB, amount, 3, id); |
| 3924 | check(FindRecordedPoolPayout(id, height, 0, addrB, amount, none, found) | 3964 | check(FindRecordedPoolPayout(id, height, 0, addrB, amount, fAllowLegacy, none, found) |
| 3925 | == PAYOUT_PROBE_STRUCTURED && found == structuredTxid, | 3965 | == PAYOUT_PROBE_STRUCTURED && found == structuredTxid, |
| 3926 | "structured payout metadata is matched in preference to a comment"); | 3966 | "structured payout metadata is matched in preference to a comment"); |
| 3927 | 3967 | ||
| @@ -3935,7 +3975,7 @@ int RunPayoutLedgerSelfTest() | |||
| 3935 | PayoutTestClearBlockIndex(); | 3975 | PayoutTestClearBlockIndex(); |
| 3936 | const std::string addrC = PayoutTestAddress(0x33); | 3976 | const std::string addrC = PayoutTestAddress(0x33); |
| 3937 | PayoutTestAddWalletTx(comment, addrC, amount, 4, "", false); | 3977 | PayoutTestAddWalletTx(comment, addrC, amount, 4, "", false); |
| 3938 | check(FindRecordedPoolPayout(id, height, 0, addrC, amount, none, found) | 3978 | check(FindRecordedPoolPayout(id, height, 0, addrC, amount, fAllowLegacy, none, found) |
| 3939 | == PAYOUT_PROBE_NONE, | 3979 | == PAYOUT_PROBE_NONE, |
| 3940 | "a pre-upgrade payout that never confirmed is not read as" | 3980 | "a pre-upgrade payout that never confirmed is not read as" |
| 3941 | " payment"); | 3981 | " payment"); |
| @@ -3945,7 +3985,7 @@ int RunPayoutLedgerSelfTest() | |||
| 3945 | mapWallet.clear(); | 3985 | mapWallet.clear(); |
| 3946 | const std::string confirmedTxid = | 3986 | const std::string confirmedTxid = |
| 3947 | PayoutTestAddWalletTx(comment, addrC, amount, 4, "", true); | 3987 | PayoutTestAddWalletTx(comment, addrC, amount, 4, "", true); |
| 3948 | check(FindRecordedPoolPayout(id, height, 0, addrC, amount, none, found) | 3988 | check(FindRecordedPoolPayout(id, height, 0, addrC, amount, fAllowLegacy, none, found) |
| 3949 | == PAYOUT_PROBE_LEGACY && found == confirmedTxid, | 3989 | == PAYOUT_PROBE_LEGACY && found == confirmedTxid, |
| 3950 | "the same payout, confirmed, is"); | 3990 | "the same payout, confirmed, is"); |
| 3951 | 3991 | ||
| @@ -3956,7 +3996,7 @@ int RunPayoutLedgerSelfTest() | |||
| 3956 | PayoutTestAddWalletTx(comment, addrC, amount, 5, "", false); | 3996 | PayoutTestAddWalletTx(comment, addrC, amount, 5, "", false); |
| 3957 | std::set<std::string> spentFirst; | 3997 | std::set<std::string> spentFirst; |
| 3958 | spentFirst.insert(confirmedTxid); | 3998 | spentFirst.insert(confirmedTxid); |
| 3959 | check(FindRecordedPoolPayout(id, height, 0, addrC, amount, spentFirst, found) | 3999 | check(FindRecordedPoolPayout(id, height, 0, addrC, amount, fAllowLegacy, spentFirst, found) |
| 3960 | == PAYOUT_PROBE_LEGACY && found == alsoConfirmed, | 4000 | == PAYOUT_PROBE_LEGACY && found == alsoConfirmed, |
| 3961 | "an unconfirmed transaction does not hide a confirmed one behind" | 4001 | "an unconfirmed transaction does not hide a confirmed one behind" |
| 3962 | " it"); | 4002 | " it"); |
| @@ -4511,11 +4551,14 @@ int RunPayoutLedgerSelfTest() | |||
| 4511 | const std::string idB = PayoutIdForBlock(height, hashB); | 4551 | const std::string idB = PayoutIdForBlock(height, hashB); |
| 4512 | std::set<std::string> none; | 4552 | std::set<std::string> none; |
| 4513 | std::string found; | 4553 | std::string found; |
| 4554 | // Neither twin has a round proof here (section 15 covers the gate), | ||
| 4555 | // and their ids collide, so one answer serves both. | ||
| 4556 | const bool fAllowLegacy = !PoolRoundHasProof(idA); | ||
| 4514 | PayoutTestAddWalletTx(PayoutComment(height, hashA), addrA, amount, 1, ""); | 4557 | PayoutTestAddWalletTx(PayoutComment(height, hashA), addrA, amount, 1, ""); |
| 4515 | check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount, none, found) | 4558 | check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount, fAllowLegacy, none, found) |
| 4516 | == PAYOUT_PROBE_NONE, | 4559 | == PAYOUT_PROBE_NONE, |
| 4517 | "a new-format comment is never legacy evidence for a twin round"); | 4560 | "a new-format comment is never legacy evidence for a twin round"); |
| 4518 | check(FindRecordedPoolPayout(idA, height, hashA, addrA, amount, none, found) | 4561 | check(FindRecordedPoolPayout(idA, height, hashA, addrA, amount, fAllowLegacy, none, found) |
| 4519 | == PAYOUT_PROBE_NONE, | 4562 | == PAYOUT_PROBE_NONE, |
| 4520 | "nor a match channel for its own round -- structured metadata is"); | 4563 | "nor a match channel for its own round -- structured metadata is"); |
| 4521 | 4564 | ||
| @@ -4527,7 +4570,7 @@ int RunPayoutLedgerSelfTest() | |||
| 4527 | const std::string paidA = | 4570 | const std::string paidA = |
| 4528 | PayoutTestAddWalletTx(PayoutComment(height, hashA), addrA, amount, | 4571 | PayoutTestAddWalletTx(PayoutComment(height, hashA), addrA, amount, |
| 4529 | 2, idA); | 4572 | 2, idA); |
| 4530 | check(FindRecordedPoolPayout(idA, height, hashA, addrA, amount, none, found) | 4573 | check(FindRecordedPoolPayout(idA, height, hashA, addrA, amount, fAllowLegacy, none, found) |
| 4531 | == PAYOUT_PROBE_STRUCTURED && found == paidA, | 4574 | == PAYOUT_PROBE_STRUCTURED && found == paidA, |
| 4532 | "the winning twin's own payment is found by its structured id"); | 4575 | "the winning twin's own payment is found by its structured id"); |
| 4533 | 4576 | ||
| @@ -4539,7 +4582,7 @@ int RunPayoutLedgerSelfTest() | |||
| 4539 | check(idA == idB, | 4582 | check(idA == idB, |
| 4540 | "twin hashes differing only past the sixteenth hex digit collide" | 4583 | "twin hashes differing only past the sixteenth hex digit collide" |
| 4541 | " in id -- the reason the full hash must decide"); | 4584 | " in id -- the reason the full hash must decide"); |
| 4542 | check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount, none, found) | 4585 | check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount, fAllowLegacy, none, found) |
| 4543 | == PAYOUT_PROBE_STRUCTURED, | 4586 | == PAYOUT_PROBE_STRUCTURED, |
| 4544 | "a payment without the block key answers for a colliding twin --" | 4587 | "a payment without the block key answers for a colliding twin --" |
| 4545 | " the accepted residual for pre-key transactions"); | 4588 | " the accepted residual for pre-key transactions"); |
| @@ -4552,10 +4595,10 @@ int RunPayoutLedgerSelfTest() | |||
| 4552 | const std::string keyedA = | 4595 | const std::string keyedA = |
| 4553 | PayoutTestAddWalletTx(PayoutComment(height, hashA), addrA, amount, | 4596 | PayoutTestAddWalletTx(PayoutComment(height, hashA), addrA, amount, |
| 4554 | 3, idA, true, hashA.ToString()); | 4597 | 3, idA, true, hashA.ToString()); |
| 4555 | check(FindRecordedPoolPayout(idA, height, hashA, addrA, amount, none, found) | 4598 | check(FindRecordedPoolPayout(idA, height, hashA, addrA, amount, fAllowLegacy, none, found) |
| 4556 | == PAYOUT_PROBE_STRUCTURED && found == keyedA, | 4599 | == PAYOUT_PROBE_STRUCTURED && found == keyedA, |
| 4557 | "a payment carrying the full block hash settles its own round"); | 4600 | "a payment carrying the full block hash settles its own round"); |
| 4558 | check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount, none, found) | 4601 | check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount, fAllowLegacy, none, found) |
| 4559 | == PAYOUT_PROBE_NONE, | 4602 | == PAYOUT_PROBE_NONE, |
| 4560 | "and is no evidence for the twin, so the re-won height is paid"); | 4603 | "and is no evidence for the twin, so the re-won height is paid"); |
| 4561 | 4604 | ||
| @@ -4564,6 +4607,80 @@ int RunPayoutLedgerSelfTest() | |||
| 4564 | PayoutTestClearBlockIndex(); | 4607 | PayoutTestClearBlockIndex(); |
| 4565 | } | 4608 | } |
| 4566 | 4609 | ||
| 4610 | // -- 15. A proof-bearing round rejects comment-only evidence ------------- | ||
| 4611 | // | ||
| 4612 | // The surviving half of the same-height collision: a pre-upgrade | ||
| 4613 | // transaction carries only "Pool payout height H", so if H is won again | ||
| 4614 | // after a reorg, it matches the new round's (height, address, amount) | ||
| 4615 | // exactly and would settle a debt it never paid. The new round has a | ||
| 4616 | // round proof -- written at creation, and only by builds whose payments | ||
| 4617 | // all name their round -- so for it, comment-only evidence is | ||
| 4618 | // inadmissible and the miner is paid. A round without a proof keeps | ||
| 4619 | // today's bias: the evidence is admissible and doubt resolves to | ||
| 4620 | // "already paid". | ||
| 4621 | { | ||
| 4622 | PayoutTestResetState(); | ||
| 4623 | CRITICAL_BLOCK(cs_mapWallet) | ||
| 4624 | mapWallet.clear(); | ||
| 4625 | PayoutTestClearBlockIndex(); | ||
| 4626 | const std::string roundsPath = dir + "/pool_rounds_gate_selftest.json"; | ||
| 4627 | const std::string savedRoundsFile = strPoolRoundsFile; | ||
| 4628 | strPoolRoundsFile = roundsPath; | ||
| 4629 | remove(roundsPath.c_str()); | ||
| 4630 | |||
| 4631 | const int height = 7171; | ||
| 4632 | const int64 amount = 4 * COIN; | ||
| 4633 | uint256 hashB = 0; | ||
| 4634 | hashB.SetHex("00000000000000000000000000000000000000000000000000000000000000cc"); | ||
| 4635 | const std::string idB = PayoutIdForBlock(height, hashB); | ||
| 4636 | std::set<std::string> none; | ||
| 4637 | std::string found; | ||
| 4638 | |||
| 4639 | // The colliding pre-upgrade payment: confirmed, comment-only, same | ||
| 4640 | // height, same address, same amount -- the strongest wrong evidence | ||
| 4641 | // there is. | ||
| 4642 | PayoutTestAddWalletTx(LegacyPayoutComment(height), addrA, amount, 1, ""); | ||
| 4643 | |||
| 4644 | check(!PoolRoundHasProof(idB), | ||
| 4645 | "a round with no recorded proof does not claim one"); | ||
| 4646 | check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount, | ||
| 4647 | !PoolRoundHasProof(idB), none, found) | ||
| 4648 | == PAYOUT_PROBE_LEGACY, | ||
| 4649 | "without a proof the doubt is genuine and still resolves to" | ||
| 4650 | " already-paid"); | ||
| 4651 | |||
| 4652 | PoolRoundProof round; | ||
| 4653 | round.id = idB; | ||
| 4654 | round.blockHeight = height; | ||
| 4655 | round.matureAtHeight = height + COINBASE_SPENDABLE_DEPTH; | ||
| 4656 | round.payouts[addrA] = amount; | ||
| 4657 | RecordPoolRound(round); | ||
| 4658 | check(PoolRoundHasProof(idB), | ||
| 4659 | "recording the round proof marks the round as this lineage's"); | ||
| 4660 | check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount, | ||
| 4661 | !PoolRoundHasProof(idB), none, found) | ||
| 4662 | == PAYOUT_PROBE_NONE, | ||
| 4663 | "with a proof, a same-height comment-only transaction is not" | ||
| 4664 | " evidence, and the re-won height is paid"); | ||
| 4665 | |||
| 4666 | // Structured evidence still is: the round's own payment, recovered. | ||
| 4667 | const std::string paidB = | ||
| 4668 | PayoutTestAddWalletTx(PayoutComment(height, hashB), addrA, amount, | ||
| 4669 | 2, idB, true, hashB.ToString()); | ||
| 4670 | check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount, | ||
| 4671 | !PoolRoundHasProof(idB), none, found) | ||
| 4672 | == PAYOUT_PROBE_STRUCTURED && found == paidB, | ||
| 4673 | "the gate does not blind the probe to the round's own structured" | ||
| 4674 | " payment"); | ||
| 4675 | |||
| 4676 | CRITICAL_BLOCK(cs_mapWallet) | ||
| 4677 | mapWallet.clear(); | ||
| 4678 | PayoutTestClearBlockIndex(); | ||
| 4679 | remove(roundsPath.c_str()); | ||
| 4680 | remove((roundsPath + ".tmp").c_str()); | ||
| 4681 | strPoolRoundsFile = savedRoundsFile; | ||
| 4682 | } | ||
| 4683 | |||
| 4567 | // -- clean up ------------------------------------------------------------ | 4684 | // -- clean up ------------------------------------------------------------ |
| 4568 | PayoutTestResetState(); | 4685 | PayoutTestResetState(); |
| 4569 | CRITICAL_BLOCK(cs_mapWallet) | 4686 | CRITICAL_BLOCK(cs_mapWallet) |