diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/rpc.cpp | 413 |
1 files changed, 386 insertions, 27 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp index 579c01c..1d53fb2 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp | |||
| @@ -812,6 +812,29 @@ static void PublishPendingPayouts(const VersionedBlob& v) | |||
| 812 | gPayoutLedgerPublishedSeq, "pending payout ledger"); | 812 | gPayoutLedgerPublishedSeq, "pending payout ledger"); |
| 813 | } | 813 | } |
| 814 | 814 | ||
| 815 | // Parse a persisted block-hash field, refusing to guess. SetHex() reads | ||
| 816 | // whatever hex prefix it finds and stops at the first character it does not | ||
| 817 | // recognise, so a corrupt value does not fail to parse -- it parses as a | ||
| 818 | // DIFFERENT hash. Both shapes of that are wrong in a dangerous direction: a | ||
| 819 | // value with a hex-looking prefix ("deadbeef...") becomes a hash no block | ||
| 820 | // ever had, which PayoutBlockStillOnChain reads as an orphaned block and | ||
| 821 | // FlushMaturePayouts then VOIDS the debt; a value with no hex prefix at all | ||
| 822 | // becomes 0, which is grandfathered past the orphan check on the strength | ||
| 823 | // of garbage. A field that is not the 64-digit hex string ToString() wrote | ||
| 824 | // is corruption, and corruption is a load failure like any other parse | ||
| 825 | // error -- the file is quarantined, not read around. | ||
| 826 | static uint256 ParsePayoutBlockHash(const std::string& str) | ||
| 827 | { | ||
| 828 | if (str.size() != 64 || | ||
| 829 | str.find_first_not_of("0123456789abcdefABCDEF") != std::string::npos) | ||
| 830 | throw std::runtime_error( | ||
| 831 | strprintf("blockHash \"%s\" is not a 64-digit hex string", | ||
| 832 | str.c_str())); | ||
| 833 | uint256 h; | ||
| 834 | h.SetHex(str); | ||
| 835 | return h; | ||
| 836 | } | ||
| 837 | |||
| 815 | static void LoadPendingPayouts() | 838 | static void LoadPendingPayouts() |
| 816 | { | 839 | { |
| 817 | std::string path = PayoutFilePath(); | 840 | std::string path = PayoutFilePath(); |
| @@ -833,7 +856,8 @@ static void LoadPendingPayouts() | |||
| 833 | // field. Leave blockHash zero and they are grandfathered past the | 856 | // field. Leave blockHash zero and they are grandfathered past the |
| 834 | // orphan check rather than voided on a guess. | 857 | // orphan check rather than voided on a guess. |
| 835 | if (obj.contains("blockHash") && obj["blockHash"].is_string()) | 858 | if (obj.contains("blockHash") && obj["blockHash"].is_string()) |
| 836 | pp.blockHash.SetHex(obj["blockHash"].get<std::string>()); | 859 | pp.blockHash = |
| 860 | ParsePayoutBlockHash(obj["blockHash"].get<std::string>()); | ||
| 837 | if (obj.contains("blockHeight") && obj["blockHeight"].is_number()) | 861 | if (obj.contains("blockHeight") && obj["blockHeight"].is_number()) |
| 838 | pp.blockHeight = obj["blockHeight"].get<int>(); | 862 | pp.blockHeight = obj["blockHeight"].get<int>(); |
| 839 | // Upstream's tolerance for an entry with no usable height, kept | 863 | // Upstream's tolerance for an entry with no usable height, kept |
| @@ -1012,7 +1036,8 @@ static void LoadPaying() | |||
| 1012 | r.amount = obj["amount"].get<int64>(); | 1036 | r.amount = obj["amount"].get<int64>(); |
| 1013 | r.matureAtHeight = obj["matureAtHeight"].get<int>(); | 1037 | r.matureAtHeight = obj["matureAtHeight"].get<int>(); |
| 1014 | if (obj.contains("blockHash") && obj["blockHash"].is_string()) | 1038 | if (obj.contains("blockHash") && obj["blockHash"].is_string()) |
| 1015 | r.blockHash.SetHex(obj["blockHash"].get<std::string>()); | 1039 | r.blockHash = |
| 1040 | ParsePayoutBlockHash(obj["blockHash"].get<std::string>()); | ||
| 1016 | if (obj.contains("blockHeight") && obj["blockHeight"].is_number()) | 1041 | if (obj.contains("blockHeight") && obj["blockHeight"].is_number()) |
| 1017 | r.blockHeight = obj["blockHeight"].get<int>(); | 1042 | r.blockHeight = obj["blockHeight"].get<int>(); |
| 1018 | if (obj.contains("retries") && obj["retries"].is_number()) | 1043 | if (obj.contains("retries") && obj["retries"].is_number()) |
| @@ -1222,11 +1247,35 @@ static bool PayoutBlockStillOnChain(const uint256& blockHash) | |||
| 1222 | return false; | 1247 | return false; |
| 1223 | } | 1248 | } |
| 1224 | 1249 | ||
| 1225 | static std::string PayoutComment(int blockHeight) | 1250 | // The comment every pre-upgrade payout transaction carries, and therefore the |
| 1251 | // ONLY needle the legacy fallback in FindRecordedPoolPayout may search with. | ||
| 1252 | // This format is frozen: change it and every payment those builds made stops | ||
| 1253 | // matching, every one of those debts looks unpaid, and reconciliation pays | ||
| 1254 | // them all a second time. | ||
| 1255 | static std::string LegacyPayoutComment(int blockHeight) | ||
| 1226 | { | 1256 | { |
| 1227 | return strprintf("Pool payout height %d", blockHeight); | 1257 | return strprintf("Pool payout height %d", blockHeight); |
| 1228 | } | 1258 | } |
| 1229 | 1259 | ||
| 1260 | // The comment written on new sends. It names the funding block, because the | ||
| 1261 | // height alone cannot: a height that is won, lost to a reorg, and won again | ||
| 1262 | // yields two rounds whose height-only comments are identical -- and identical | ||
| 1263 | // amounts too, whenever the same miner held both rounds. The structured | ||
| 1264 | // pool_payout_* metadata already keys the payment by hash for the code; this | ||
| 1265 | // puts the same fact where an operator reading the wallet can see it, and | ||
| 1266 | // makes a new transaction unmatchable by the legacy needle even if the | ||
| 1267 | // structured keys are ever lost. | ||
| 1268 | // | ||
| 1269 | // A zero hash keeps the legacy form: those records predate hash keying, and | ||
| 1270 | // there is no hash to name. | ||
| 1271 | static std::string PayoutComment(int blockHeight, const uint256& blockHash) | ||
| 1272 | { | ||
| 1273 | if (blockHash == 0) | ||
| 1274 | return LegacyPayoutComment(blockHeight); | ||
| 1275 | return strprintf("Pool payout height %d block %s", blockHeight, | ||
| 1276 | blockHash.ToString().c_str()); | ||
| 1277 | } | ||
| 1278 | |||
| 1230 | // Startup reconciliation. | 1279 | // Startup reconciliation. |
| 1231 | // | 1280 | // |
| 1232 | // This used to be the crash-recovery mechanism: it scanned the wallet with | 1281 | // This used to be the crash-recovery mechanism: it scanned the wallet with |
| @@ -1381,14 +1430,37 @@ static void MarkPoolRoundForfeited(const std::string& id, | |||
| 1381 | PublishPoolRounds(snap); | 1430 | PublishPoolRounds(snap); |
| 1382 | } | 1431 | } |
| 1383 | 1432 | ||
| 1433 | // Does this round have a published round proof? | ||
| 1434 | // | ||
| 1435 | // Existence answers a temporal question the legacy fallback below needs: | ||
| 1436 | // which builds could have paid this round. RecordPoolRound writes the proof | ||
| 1437 | // when the round is created, and structured payout metadata (upstream | ||
| 1438 | // cbdd06d) is an ancestor of round proofs (upstream 83ba822) -- no build | ||
| 1439 | // able to write a proof has ever sent a payment that only a comment can | ||
| 1440 | // identify. A round with a proof therefore cannot have a comment-only | ||
| 1441 | // payment; one that matches it is another round's, colliding on the height. | ||
| 1442 | static bool PoolRoundHasProof(const std::string& id) | ||
| 1443 | { | ||
| 1444 | std::lock_guard<std::mutex> lk(gPoolRoundsMutex); | ||
| 1445 | for (const PoolRoundProof& r : gPoolRounds) | ||
| 1446 | if (r.id == id) | ||
| 1447 | return true; | ||
| 1448 | return false; | ||
| 1449 | } | ||
| 1450 | |||
| 1384 | // Which wallet transaction, if any, already paid this? | 1451 | // Which wallet transaction, if any, already paid this? |
| 1385 | // | 1452 | // |
| 1386 | // Two kinds of evidence, in order of quality. | 1453 | // Two kinds of evidence, in order of quality. |
| 1387 | // | 1454 | // |
| 1388 | // Structured (pool_payout_id / pool_payout_address / pool_payout_amount) is | 1455 | // Structured (pool_payout_id / pool_payout_address / pool_payout_amount, |
| 1389 | // upstream's, and it is exact: the id is derived from the block hash, so it | 1456 | // plus pool_payout_block since this series) is upstream's, extended. The id |
| 1390 | // names one round and one round only. Every transaction this build sends | 1457 | // is derived from the block hash, but only from the FIRST sixteen hex digits |
| 1391 | // carries it. | 1458 | // of it -- and proof-of-work concentrates zeros exactly there, so the id can |
| 1459 | // name two same-height rounds identically. New sends therefore record the | ||
| 1460 | // full hash as pool_payout_block, and a structured match must agree with it | ||
| 1461 | // wherever both sides have one; a transaction without the key was sent | ||
| 1462 | // before it existed, and for those the id is the best evidence there is. | ||
| 1463 | // Every transaction this build sends carries all four keys. | ||
| 1392 | // | 1464 | // |
| 1393 | // But NO transaction this pool has ever sent carries it. Every payout in the | 1465 | // But NO transaction this pool has ever sent carries it. Every payout in the |
| 1394 | // existing wallet has only a "comment". So on the first boot after this | 1466 | // existing wallet has only a "comment". So on the first boot after this |
| @@ -1411,11 +1483,22 @@ static void MarkPoolRoundForfeited(const std::string& id, | |||
| 1411 | // (round, address) pair -- from the ledger's paidTxids, which is durable, | 1483 | // (round, address) pair -- from the ledger's paidTxids, which is durable, |
| 1412 | // and from what this process has already claimed. One legacy transaction | 1484 | // and from what this process has already claimed. One legacy transaction |
| 1413 | // can therefore settle at most one debt, so N transactions can never | 1485 | // can therefore settle at most one debt, so N transactions can never |
| 1414 | // satisfy more than N obligations. | 1486 | // satisfy more than N obligations; |
| 1415 | // | 1487 | // |
| 1416 | // What is left unresolved by construction is two pre-upgrade rounds at the | 1488 | // 4. it is not consulted at all for a round that has a round proof |
| 1417 | // SAME height paying the same address the same amount, since the comment | 1489 | // (fAllowLegacy, decided by the caller from PoolRoundHasProof). Proofs |
| 1418 | // carries only the height. That needs a reorg. | 1490 | // are written when a round is created, and only by builds whose every |
| 1491 | // payment names its round exactly -- so a comment-only transaction | ||
| 1492 | // cannot be a payment for such a round, only a same-height collision | ||
| 1493 | // from an older one, and skipping the fallback cannot hide a real | ||
| 1494 | // payment: the structured probe above already found any that exist. | ||
| 1495 | // | ||
| 1496 | // What is left unresolved by construction is a comment-only transaction and | ||
| 1497 | // a PROOFLESS round at the same height paying the same address the same | ||
| 1498 | // amount: two grandfathered zero-hash twins, or a twin pair from the builds | ||
| 1499 | // that keyed the ledger by hash before any metadata reached the wallet. The | ||
| 1500 | // comment carries only the height, so nothing ties the transaction to one | ||
| 1501 | // twin rather than the other. That needs a reorg. | ||
| 1419 | // | 1502 | // |
| 1420 | // This used to say the losing twin's ledger entry is voided by the orphan | 1503 | // This used to say the losing twin's ledger entry is voided by the orphan |
| 1421 | // check before it can be paid. That is wrong: PayoutBlockStillOnChain() | 1504 | // check before it can be paid. That is wrong: PayoutBlockStillOnChain() |
| @@ -1449,8 +1532,10 @@ enum PayoutProbeResult { | |||
| 1449 | 1532 | ||
| 1450 | static PayoutProbeResult FindRecordedPoolPayout(const std::string& id, | 1533 | static PayoutProbeResult FindRecordedPoolPayout(const std::string& id, |
| 1451 | int blockHeight, | 1534 | int blockHeight, |
| 1535 | const uint256& blockHash, | ||
| 1452 | const std::string& address, | 1536 | const std::string& address, |
| 1453 | int64 amount, | 1537 | int64 amount, |
| 1538 | bool fAllowLegacy, | ||
| 1454 | const std::set<std::string>& excluded, | 1539 | const std::set<std::string>& excluded, |
| 1455 | std::string& txidRet) | 1540 | std::string& txidRet) |
| 1456 | { | 1541 | { |
| @@ -1461,7 +1546,7 @@ static PayoutProbeResult FindRecordedPoolPayout(const std::string& id, | |||
| 1461 | CScript want; | 1546 | CScript want; |
| 1462 | if (fHaveScript) | 1547 | if (fHaveScript) |
| 1463 | want << OP_DUP << OP_HASH160 << h160 << OP_EQUALVERIFY << OP_CHECKSIG; | 1548 | want << OP_DUP << OP_HASH160 << h160 << OP_EQUALVERIFY << OP_CHECKSIG; |
| 1464 | const std::string comment = PayoutComment(blockHeight); | 1549 | const std::string comment = LegacyPayoutComment(blockHeight); |
| 1465 | 1550 | ||
| 1466 | // Candidate legacy matches, adjudicated after cs_mapWallet is released. | 1551 | // Candidate legacy matches, adjudicated after cs_mapWallet is released. |
| 1467 | // | 1552 | // |
| @@ -1494,10 +1579,25 @@ static PayoutProbeResult FindRecordedPoolPayout(const std::string& id, | |||
| 1494 | map<string, string>::const_iterator amtIt = wtx.mapValue.find("pool_payout_amount"); | 1579 | map<string, string>::const_iterator amtIt = wtx.mapValue.find("pool_payout_amount"); |
| 1495 | if (amtIt == wtx.mapValue.end() || amtIt->second != strprintf("%lld", amount)) | 1580 | if (amtIt == wtx.mapValue.end() || amtIt->second != strprintf("%lld", amount)) |
| 1496 | continue; | 1581 | continue; |
| 1582 | // The id alone cannot tell twin rounds at one height apart | ||
| 1583 | // (see above); the full hash can, where the transaction | ||
| 1584 | // recorded one. Where it did not, the id decides, as it | ||
| 1585 | // always has -- refusing those transactions would make every | ||
| 1586 | // payment sent before the key existed look unpaid and pay it | ||
| 1587 | // again. | ||
| 1588 | if (blockHash != 0) { | ||
| 1589 | map<string, string>::const_iterator blkIt = | ||
| 1590 | wtx.mapValue.find("pool_payout_block"); | ||
| 1591 | if (blkIt != wtx.mapValue.end() | ||
| 1592 | && blkIt->second != blockHash.ToString()) | ||
| 1593 | continue; // a twin round's payment, not this round's | ||
| 1594 | } | ||
| 1497 | txidRet = wtx.GetHash().GetHex(); | 1595 | txidRet = wtx.GetHash().GetHex(); |
| 1498 | return PAYOUT_PROBE_STRUCTURED; | 1596 | return PAYOUT_PROBE_STRUCTURED; |
| 1499 | } | 1597 | } |
| 1500 | 1598 | ||
| 1599 | if (!fAllowLegacy) | ||
| 1600 | continue; // comment-only evidence is inadmissible: rule 4 | ||
| 1501 | if (!fHaveScript) | 1601 | if (!fHaveScript) |
| 1502 | continue; | 1602 | continue; |
| 1503 | // The comment is written only by the payout path, so its presence | 1603 | // The comment is written only by the payout path, so its presence |
| @@ -2137,9 +2237,15 @@ static void PayoutThreadFn(void*) | |||
| 2137 | std::set<std::string> excluded; | 2237 | std::set<std::string> excluded; |
| 2138 | CollectForeignPayoutTxids(r.blockHeight, r.blockHash, r.address, | 2238 | CollectForeignPayoutTxids(r.blockHeight, r.blockHash, r.address, |
| 2139 | claimKey, excluded); | 2239 | claimKey, excluded); |
| 2240 | // A round with a published proof was created by a build whose | ||
| 2241 | // every payment names its round exactly, so only structured | ||
| 2242 | // evidence can settle it -- a comment match would be another | ||
| 2243 | // round's payment colliding on the height. | ||
| 2244 | const bool fAllowLegacy = !PoolRoundHasProof(id); | ||
| 2140 | r.fProbeWallet = false; | 2245 | r.fProbeWallet = false; |
| 2141 | PayoutProbeResult probe = FindRecordedPoolPayout( | 2246 | PayoutProbeResult probe = FindRecordedPoolPayout( |
| 2142 | id, r.blockHeight, r.address, r.amount, excluded, recovered); | 2247 | id, r.blockHeight, r.blockHash, r.address, r.amount, |
| 2248 | fAllowLegacy, excluded, recovered); | ||
| 2143 | if (probe != PAYOUT_PROBE_NONE) { | 2249 | if (probe != PAYOUT_PROBE_NONE) { |
| 2144 | if (probe == PAYOUT_PROBE_LEGACY) { | 2250 | if (probe == PAYOUT_PROBE_LEGACY) { |
| 2145 | // Loud on purpose: this is the upgrade path, where the | 2251 | // Loud on purpose: this is the upgrade path, where the |
| @@ -2224,7 +2330,7 @@ static void PayoutThreadFn(void*) | |||
| 2224 | } | 2330 | } |
| 2225 | 2331 | ||
| 2226 | CWalletTx wtx; | 2332 | CWalletTx wtx; |
| 2227 | wtx.mapValue["comment"] = PayoutComment(r.blockHeight); | 2333 | wtx.mapValue["comment"] = PayoutComment(r.blockHeight, r.blockHash); |
| 2228 | // Upstream's structured metadata. This is what makes the wallet | 2334 | // Upstream's structured metadata. This is what makes the wallet |
| 2229 | // probe above possible at all -- the old comment-only form could | 2335 | // probe above possible at all -- the old comment-only form could |
| 2230 | // not distinguish two rounds paying the same address the same | 2336 | // not distinguish two rounds paying the same address the same |
| @@ -2233,6 +2339,11 @@ static void PayoutThreadFn(void*) | |||
| 2233 | wtx.mapValue["pool_payout_id"] = id; | 2339 | wtx.mapValue["pool_payout_id"] = id; |
| 2234 | wtx.mapValue["pool_payout_address"] = r.address; | 2340 | wtx.mapValue["pool_payout_address"] = r.address; |
| 2235 | wtx.mapValue["pool_payout_amount"] = strprintf("%lld", r.amount); | 2341 | wtx.mapValue["pool_payout_amount"] = strprintf("%lld", r.amount); |
| 2342 | // The full hash, not only the id's sixteen-digit prefix: the | ||
| 2343 | // prefix of a proof-of-work hash is where the zeros live, and | ||
| 2344 | // the probe needs to tell twin rounds at one height apart. | ||
| 2345 | if (r.blockHash != 0) | ||
| 2346 | wtx.mapValue["pool_payout_block"] = r.blockHash.ToString(); | ||
| 2236 | 2347 | ||
| 2237 | SendMoneyOutcome outcome; | 2348 | SendMoneyOutcome outcome; |
| 2238 | if (SendMoney(sc, r.amount, wtx, outcome)) { | 2349 | if (SendMoney(sc, r.amount, wtx, outcome)) { |
| @@ -3443,7 +3554,8 @@ static std::string PayoutTestAddWalletTx(const std::string& comment, | |||
| 3443 | int64 amount, | 3554 | int64 amount, |
| 3444 | int nonce, | 3555 | int nonce, |
| 3445 | const std::string& structuredId, | 3556 | const std::string& structuredId, |
| 3446 | bool fConfirmed = true) | 3557 | bool fConfirmed = true, |
| 3558 | const std::string& structuredBlock = "") | ||
| 3447 | { | 3559 | { |
| 3448 | CWalletTx wtx; | 3560 | CWalletTx wtx; |
| 3449 | wtx.vin.resize(1); | 3561 | wtx.vin.resize(1); |
| @@ -3458,6 +3570,8 @@ static std::string PayoutTestAddWalletTx(const std::string& comment, | |||
| 3458 | wtx.mapValue["pool_payout_id"] = structuredId; | 3570 | wtx.mapValue["pool_payout_id"] = structuredId; |
| 3459 | wtx.mapValue["pool_payout_address"] = address; | 3571 | wtx.mapValue["pool_payout_address"] = address; |
| 3460 | wtx.mapValue["pool_payout_amount"] = strprintf("%lld", amount); | 3572 | wtx.mapValue["pool_payout_amount"] = strprintf("%lld", amount); |
| 3573 | if (!structuredBlock.empty()) | ||
| 3574 | wtx.mapValue["pool_payout_block"] = structuredBlock; | ||
| 3461 | } | 3575 | } |
| 3462 | uint256 hash = wtx.GetHash(); | 3576 | uint256 hash = wtx.GetHash(); |
| 3463 | if (fConfirmed) { | 3577 | if (fConfirmed) { |
| @@ -4007,34 +4121,37 @@ int RunPayoutLedgerSelfTest() | |||
| 4007 | const int height = 8080; | 4121 | const int height = 8080; |
| 4008 | const int64 amount = 7 * COIN; | 4122 | const int64 amount = 7 * COIN; |
| 4009 | const std::string id = PayoutIdForBlock(height, 0); | 4123 | const std::string id = PayoutIdForBlock(height, 0); |
| 4010 | const std::string comment = PayoutComment(height); | 4124 | const std::string comment = LegacyPayoutComment(height); |
| 4011 | std::set<std::string> none; | 4125 | std::set<std::string> none; |
| 4012 | std::string found; | 4126 | std::string found; |
| 4127 | // No round proof exists for these rounds, so legacy evidence is | ||
| 4128 | // admissible -- computed exactly as the payout thread computes it. | ||
| 4129 | const bool fAllowLegacy = !PoolRoundHasProof(id); | ||
| 4013 | 4130 | ||
| 4014 | check(FindRecordedPoolPayout(id, height, addrA, amount, none, found) | 4131 | check(FindRecordedPoolPayout(id, height, 0, addrA, amount, fAllowLegacy, none, found) |
| 4015 | == PAYOUT_PROBE_NONE, | 4132 | == PAYOUT_PROBE_NONE, |
| 4016 | "an empty wallet is not evidence that anything was paid"); | 4133 | "an empty wallet is not evidence that anything was paid"); |
| 4017 | 4134 | ||
| 4018 | const std::string legacyTxid = | 4135 | const std::string legacyTxid = |
| 4019 | PayoutTestAddWalletTx(comment, addrA, amount, 1, ""); | 4136 | PayoutTestAddWalletTx(comment, addrA, amount, 1, ""); |
| 4020 | check(FindRecordedPoolPayout(id, height, addrA, amount, none, found) | 4137 | check(FindRecordedPoolPayout(id, height, 0, addrA, amount, fAllowLegacy, none, found) |
| 4021 | == PAYOUT_PROBE_LEGACY && found == legacyTxid, | 4138 | == PAYOUT_PROBE_LEGACY && found == legacyTxid, |
| 4022 | "a payment carrying only a pre-upgrade comment is recognised as" | 4139 | "a payment carrying only a pre-upgrade comment is recognised as" |
| 4023 | " already paid"); | 4140 | " already paid"); |
| 4024 | 4141 | ||
| 4025 | check(FindRecordedPoolPayout(id, height, addrB, amount, none, found) | 4142 | check(FindRecordedPoolPayout(id, height, 0, addrB, amount, fAllowLegacy, none, found) |
| 4026 | == PAYOUT_PROBE_NONE, | 4143 | == PAYOUT_PROBE_NONE, |
| 4027 | "the legacy match requires the output to pay THIS recipient"); | 4144 | "the legacy match requires the output to pay THIS recipient"); |
| 4028 | check(FindRecordedPoolPayout(id, height, addrA, amount + COIN, none, found) | 4145 | check(FindRecordedPoolPayout(id, height, 0, addrA, amount + COIN, fAllowLegacy, none, found) |
| 4029 | == PAYOUT_PROBE_NONE, | 4146 | == PAYOUT_PROBE_NONE, |
| 4030 | "the legacy match requires the output to pay exactly this amount"); | 4147 | "the legacy match requires the output to pay exactly this amount"); |
| 4031 | check(FindRecordedPoolPayout(id, height + 1, addrA, amount, none, found) | 4148 | check(FindRecordedPoolPayout(id, height + 1, 0, addrA, amount, fAllowLegacy, none, found) |
| 4032 | == PAYOUT_PROBE_NONE, | 4149 | == PAYOUT_PROBE_NONE, |
| 4033 | "the legacy match requires the comment for this block height"); | 4150 | "the legacy match requires the comment for this block height"); |
| 4034 | 4151 | ||
| 4035 | std::set<std::string> spent; | 4152 | std::set<std::string> spent; |
| 4036 | spent.insert(legacyTxid); | 4153 | spent.insert(legacyTxid); |
| 4037 | check(FindRecordedPoolPayout(id, height, addrA, amount, spent, found) | 4154 | check(FindRecordedPoolPayout(id, height, 0, addrA, amount, fAllowLegacy, spent, found) |
| 4038 | == PAYOUT_PROBE_NONE, | 4155 | == PAYOUT_PROBE_NONE, |
| 4039 | "one legacy transaction cannot settle a second debt"); | 4156 | "one legacy transaction cannot settle a second debt"); |
| 4040 | 4157 | ||
| @@ -4043,7 +4160,7 @@ int RunPayoutLedgerSelfTest() | |||
| 4043 | // exactly what the structured metadata exists to prevent. | 4160 | // exactly what the structured metadata exists to prevent. |
| 4044 | const std::string otherId = PayoutIdForBlock(height, 0) + "-other"; | 4161 | const std::string otherId = PayoutIdForBlock(height, 0) + "-other"; |
| 4045 | PayoutTestAddWalletTx(comment, addrB, amount, 2, otherId); | 4162 | PayoutTestAddWalletTx(comment, addrB, amount, 2, otherId); |
| 4046 | check(FindRecordedPoolPayout(id, height, addrB, amount, none, found) | 4163 | check(FindRecordedPoolPayout(id, height, 0, addrB, amount, fAllowLegacy, none, found) |
| 4047 | == PAYOUT_PROBE_NONE, | 4164 | == PAYOUT_PROBE_NONE, |
| 4048 | "another round's structured payment is not legacy evidence for" | 4165 | "another round's structured payment is not legacy evidence for" |
| 4049 | " this one"); | 4166 | " this one"); |
| @@ -4051,7 +4168,7 @@ int RunPayoutLedgerSelfTest() | |||
| 4051 | // The structured path still wins where it applies. | 4168 | // The structured path still wins where it applies. |
| 4052 | const std::string structuredTxid = | 4169 | const std::string structuredTxid = |
| 4053 | PayoutTestAddWalletTx(comment, addrB, amount, 3, id); | 4170 | PayoutTestAddWalletTx(comment, addrB, amount, 3, id); |
| 4054 | check(FindRecordedPoolPayout(id, height, addrB, amount, none, found) | 4171 | check(FindRecordedPoolPayout(id, height, 0, addrB, amount, fAllowLegacy, none, found) |
| 4055 | == PAYOUT_PROBE_STRUCTURED && found == structuredTxid, | 4172 | == PAYOUT_PROBE_STRUCTURED && found == structuredTxid, |
| 4056 | "structured payout metadata is matched in preference to a comment"); | 4173 | "structured payout metadata is matched in preference to a comment"); |
| 4057 | 4174 | ||
| @@ -4065,7 +4182,7 @@ int RunPayoutLedgerSelfTest() | |||
| 4065 | PayoutTestClearBlockIndex(); | 4182 | PayoutTestClearBlockIndex(); |
| 4066 | const std::string addrC = PayoutTestAddress(0x33); | 4183 | const std::string addrC = PayoutTestAddress(0x33); |
| 4067 | PayoutTestAddWalletTx(comment, addrC, amount, 4, "", false); | 4184 | PayoutTestAddWalletTx(comment, addrC, amount, 4, "", false); |
| 4068 | check(FindRecordedPoolPayout(id, height, addrC, amount, none, found) | 4185 | check(FindRecordedPoolPayout(id, height, 0, addrC, amount, fAllowLegacy, none, found) |
| 4069 | == PAYOUT_PROBE_NONE, | 4186 | == PAYOUT_PROBE_NONE, |
| 4070 | "a pre-upgrade payout that never confirmed is not read as" | 4187 | "a pre-upgrade payout that never confirmed is not read as" |
| 4071 | " payment"); | 4188 | " payment"); |
| @@ -4075,7 +4192,7 @@ int RunPayoutLedgerSelfTest() | |||
| 4075 | mapWallet.clear(); | 4192 | mapWallet.clear(); |
| 4076 | const std::string confirmedTxid = | 4193 | const std::string confirmedTxid = |
| 4077 | PayoutTestAddWalletTx(comment, addrC, amount, 4, "", true); | 4194 | PayoutTestAddWalletTx(comment, addrC, amount, 4, "", true); |
| 4078 | check(FindRecordedPoolPayout(id, height, addrC, amount, none, found) | 4195 | check(FindRecordedPoolPayout(id, height, 0, addrC, amount, fAllowLegacy, none, found) |
| 4079 | == PAYOUT_PROBE_LEGACY && found == confirmedTxid, | 4196 | == PAYOUT_PROBE_LEGACY && found == confirmedTxid, |
| 4080 | "the same payout, confirmed, is"); | 4197 | "the same payout, confirmed, is"); |
| 4081 | 4198 | ||
| @@ -4086,7 +4203,7 @@ int RunPayoutLedgerSelfTest() | |||
| 4086 | PayoutTestAddWalletTx(comment, addrC, amount, 5, "", false); | 4203 | PayoutTestAddWalletTx(comment, addrC, amount, 5, "", false); |
| 4087 | std::set<std::string> spentFirst; | 4204 | std::set<std::string> spentFirst; |
| 4088 | spentFirst.insert(confirmedTxid); | 4205 | spentFirst.insert(confirmedTxid); |
| 4089 | check(FindRecordedPoolPayout(id, height, addrC, amount, spentFirst, found) | 4206 | check(FindRecordedPoolPayout(id, height, 0, addrC, amount, fAllowLegacy, spentFirst, found) |
| 4090 | == PAYOUT_PROBE_LEGACY && found == alsoConfirmed, | 4207 | == PAYOUT_PROBE_LEGACY && found == alsoConfirmed, |
| 4091 | "an unconfirmed transaction does not hide a confirmed one behind" | 4208 | "an unconfirmed transaction does not hide a confirmed one behind" |
| 4092 | " it"); | 4209 | " it"); |
| @@ -4601,6 +4718,248 @@ int RunPayoutLedgerSelfTest() | |||
| 4601 | } | 4718 | } |
| 4602 | #endif | 4719 | #endif |
| 4603 | 4720 | ||
| 4721 | // -- 14. The payout comment names the funding block ---------------------- | ||
| 4722 | // | ||
| 4723 | // Same height, two hashes: the shape a reorg leaves when the pool wins a | ||
| 4724 | // height, loses the block, and wins the height again. The legacy needle is | ||
| 4725 | // frozen at the pre-upgrade format, so pre-upgrade payments keep matching; | ||
| 4726 | // a new-format comment never matches it, so a new send can never be read | ||
| 4727 | // as a pre-upgrade payment for a twin round. | ||
| 4728 | { | ||
| 4729 | PayoutTestResetState(); | ||
| 4730 | CRITICAL_BLOCK(cs_mapWallet) | ||
| 4731 | mapWallet.clear(); | ||
| 4732 | PayoutTestClearBlockIndex(); | ||
| 4733 | |||
| 4734 | const int height = 9090; | ||
| 4735 | const int64 amount = 3 * COIN; | ||
| 4736 | uint256 hashA = 0, hashB = 0; | ||
| 4737 | hashA.SetHex("00000000000000000000000000000000000000000000000000000000000000aa"); | ||
| 4738 | hashB.SetHex("00000000000000000000000000000000000000000000000000000000000000bb"); | ||
| 4739 | |||
| 4740 | check(LegacyPayoutComment(height) == | ||
| 4741 | strprintf("Pool payout height %d", height), | ||
| 4742 | "the legacy needle is frozen at the format pre-upgrade builds" | ||
| 4743 | " wrote"); | ||
| 4744 | check(PayoutComment(height, 0) == LegacyPayoutComment(height), | ||
| 4745 | "a grandfathered zero-hash record still writes the legacy form"); | ||
| 4746 | check(PayoutComment(height, hashA) == | ||
| 4747 | strprintf("Pool payout height %d block %s", height, | ||
| 4748 | hashA.ToString().c_str()), | ||
| 4749 | "a hash-keyed record's comment names its funding block"); | ||
| 4750 | check(PayoutComment(height, hashA) != PayoutComment(height, hashB), | ||
| 4751 | "twin rounds at one height carry distinguishable comments"); | ||
| 4752 | |||
| 4753 | // A transaction wearing the NEW comment format and -- hypothetically | ||
| 4754 | // -- no structured metadata must not be matched by the legacy needle: | ||
| 4755 | // the fallback exists for pre-upgrade transactions, and the new format | ||
| 4756 | // proves this is not one. | ||
| 4757 | const std::string idA = PayoutIdForBlock(height, hashA); | ||
| 4758 | const std::string idB = PayoutIdForBlock(height, hashB); | ||
| 4759 | std::set<std::string> none; | ||
| 4760 | std::string found; | ||
| 4761 | // Neither twin has a round proof here (section 15 covers the gate), | ||
| 4762 | // and their ids collide, so one answer serves both. | ||
| 4763 | const bool fAllowLegacy = !PoolRoundHasProof(idA); | ||
| 4764 | PayoutTestAddWalletTx(PayoutComment(height, hashA), addrA, amount, 1, ""); | ||
| 4765 | check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount, fAllowLegacy, none, found) | ||
| 4766 | == PAYOUT_PROBE_NONE, | ||
| 4767 | "a new-format comment is never legacy evidence for a twin round"); | ||
| 4768 | check(FindRecordedPoolPayout(idA, height, hashA, addrA, amount, fAllowLegacy, none, found) | ||
| 4769 | == PAYOUT_PROBE_NONE, | ||
| 4770 | "nor a match channel for its own round -- structured metadata is"); | ||
| 4771 | |||
| 4772 | // Round A's own payment, sent by this build, is found by its | ||
| 4773 | // structured id. | ||
| 4774 | CRITICAL_BLOCK(cs_mapWallet) | ||
| 4775 | mapWallet.clear(); | ||
| 4776 | PayoutTestClearBlockIndex(); | ||
| 4777 | const std::string paidA = | ||
| 4778 | PayoutTestAddWalletTx(PayoutComment(height, hashA), addrA, amount, | ||
| 4779 | 2, idA); | ||
| 4780 | check(FindRecordedPoolPayout(idA, height, hashA, addrA, amount, fAllowLegacy, none, found) | ||
| 4781 | == PAYOUT_PROBE_STRUCTURED && found == paidA, | ||
| 4782 | "the winning twin's own payment is found by its structured id"); | ||
| 4783 | |||
| 4784 | // The id is derived from the FIRST sixteen hex digits of the hash -- | ||
| 4785 | // the end proof-of-work fills with zeros -- so twin hashes that agree | ||
| 4786 | // there collide in id, and a payment carrying no pool_payout_block | ||
| 4787 | // answers for either twin. That is the accepted trade for payments | ||
| 4788 | // sent before the key existed, pinned here so it stays a decision. | ||
| 4789 | check(idA == idB, | ||
| 4790 | "twin hashes differing only past the sixteenth hex digit collide" | ||
| 4791 | " in id -- the reason the full hash must decide"); | ||
| 4792 | check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount, fAllowLegacy, none, found) | ||
| 4793 | == PAYOUT_PROBE_STRUCTURED, | ||
| 4794 | "a payment without the block key answers for a colliding twin --" | ||
| 4795 | " the accepted residual for pre-key transactions"); | ||
| 4796 | |||
| 4797 | // With the full hash recorded -- as every send now records it -- the | ||
| 4798 | // twins are told apart and the re-won height is paid. | ||
| 4799 | CRITICAL_BLOCK(cs_mapWallet) | ||
| 4800 | mapWallet.clear(); | ||
| 4801 | PayoutTestClearBlockIndex(); | ||
| 4802 | const std::string keyedA = | ||
| 4803 | PayoutTestAddWalletTx(PayoutComment(height, hashA), addrA, amount, | ||
| 4804 | 3, idA, true, hashA.ToString()); | ||
| 4805 | check(FindRecordedPoolPayout(idA, height, hashA, addrA, amount, fAllowLegacy, none, found) | ||
| 4806 | == PAYOUT_PROBE_STRUCTURED && found == keyedA, | ||
| 4807 | "a payment carrying the full block hash settles its own round"); | ||
| 4808 | check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount, fAllowLegacy, none, found) | ||
| 4809 | == PAYOUT_PROBE_NONE, | ||
| 4810 | "and is no evidence for the twin, so the re-won height is paid"); | ||
| 4811 | |||
| 4812 | CRITICAL_BLOCK(cs_mapWallet) | ||
| 4813 | mapWallet.clear(); | ||
| 4814 | PayoutTestClearBlockIndex(); | ||
| 4815 | } | ||
| 4816 | |||
| 4817 | // -- 15. A proof-bearing round rejects comment-only evidence ------------- | ||
| 4818 | // | ||
| 4819 | // The surviving half of the same-height collision: a pre-upgrade | ||
| 4820 | // transaction carries only "Pool payout height H", so if H is won again | ||
| 4821 | // after a reorg, it matches the new round's (height, address, amount) | ||
| 4822 | // exactly and would settle a debt it never paid. The new round has a | ||
| 4823 | // round proof -- written at creation, and only by builds whose payments | ||
| 4824 | // all name their round -- so for it, comment-only evidence is | ||
| 4825 | // inadmissible and the miner is paid. A round without a proof keeps | ||
| 4826 | // today's bias: the evidence is admissible and doubt resolves to | ||
| 4827 | // "already paid". | ||
| 4828 | { | ||
| 4829 | PayoutTestResetState(); | ||
| 4830 | CRITICAL_BLOCK(cs_mapWallet) | ||
| 4831 | mapWallet.clear(); | ||
| 4832 | PayoutTestClearBlockIndex(); | ||
| 4833 | const std::string roundsPath = dir + "/pool_rounds_gate_selftest.json"; | ||
| 4834 | const std::string savedRoundsFile = strPoolRoundsFile; | ||
| 4835 | strPoolRoundsFile = roundsPath; | ||
| 4836 | remove(roundsPath.c_str()); | ||
| 4837 | |||
| 4838 | const int height = 7171; | ||
| 4839 | const int64 amount = 4 * COIN; | ||
| 4840 | uint256 hashB = 0; | ||
| 4841 | hashB.SetHex("00000000000000000000000000000000000000000000000000000000000000cc"); | ||
| 4842 | const std::string idB = PayoutIdForBlock(height, hashB); | ||
| 4843 | std::set<std::string> none; | ||
| 4844 | std::string found; | ||
| 4845 | |||
| 4846 | // The colliding pre-upgrade payment: confirmed, comment-only, same | ||
| 4847 | // height, same address, same amount -- the strongest wrong evidence | ||
| 4848 | // there is. | ||
| 4849 | PayoutTestAddWalletTx(LegacyPayoutComment(height), addrA, amount, 1, ""); | ||
| 4850 | |||
| 4851 | check(!PoolRoundHasProof(idB), | ||
| 4852 | "a round with no recorded proof does not claim one"); | ||
| 4853 | check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount, | ||
| 4854 | !PoolRoundHasProof(idB), none, found) | ||
| 4855 | == PAYOUT_PROBE_LEGACY, | ||
| 4856 | "without a proof the doubt is genuine and still resolves to" | ||
| 4857 | " already-paid"); | ||
| 4858 | |||
| 4859 | PoolRoundProof round; | ||
| 4860 | round.id = idB; | ||
| 4861 | round.blockHeight = height; | ||
| 4862 | round.matureAtHeight = height + COINBASE_SPENDABLE_DEPTH; | ||
| 4863 | round.payouts[addrA] = amount; | ||
| 4864 | RecordPoolRound(round); | ||
| 4865 | check(PoolRoundHasProof(idB), | ||
| 4866 | "recording the round proof marks the round as this lineage's"); | ||
| 4867 | check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount, | ||
| 4868 | !PoolRoundHasProof(idB), none, found) | ||
| 4869 | == PAYOUT_PROBE_NONE, | ||
| 4870 | "with a proof, a same-height comment-only transaction is not" | ||
| 4871 | " evidence, and the re-won height is paid"); | ||
| 4872 | |||
| 4873 | // Structured evidence still is: the round's own payment, recovered. | ||
| 4874 | const std::string paidB = | ||
| 4875 | PayoutTestAddWalletTx(PayoutComment(height, hashB), addrA, amount, | ||
| 4876 | 2, idB, true, hashB.ToString()); | ||
| 4877 | check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount, | ||
| 4878 | !PoolRoundHasProof(idB), none, found) | ||
| 4879 | == PAYOUT_PROBE_STRUCTURED && found == paidB, | ||
| 4880 | "the gate does not blind the probe to the round's own structured" | ||
| 4881 | " payment"); | ||
| 4882 | |||
| 4883 | CRITICAL_BLOCK(cs_mapWallet) | ||
| 4884 | mapWallet.clear(); | ||
| 4885 | PayoutTestClearBlockIndex(); | ||
| 4886 | remove(roundsPath.c_str()); | ||
| 4887 | remove((roundsPath + ".tmp").c_str()); | ||
| 4888 | strPoolRoundsFile = savedRoundsFile; | ||
| 4889 | } | ||
| 4890 | |||
| 4891 | // -- 16. A malformed block hash is corruption, not a hash ---------------- | ||
| 4892 | // | ||
| 4893 | // SetHex() parses whatever hex prefix it finds and stops, so a corrupt | ||
| 4894 | // blockHash did not fail to load -- it loaded as a DIFFERENT hash. | ||
| 4895 | // "deadbeef..." became 0xdeadbeef, a hash no block ever had, which the | ||
| 4896 | // orphan check reads as an orphaned block and the next flush VOIDS the | ||
| 4897 | // debt; a value with no hex prefix became 0, which is grandfathered past | ||
| 4898 | // the orphan check instead. Either way a corrupt file silently changed | ||
| 4899 | // what the pool owed. It is a load failure now, handled like every other | ||
| 4900 | // parse error: quarantine, and never rewrite. | ||
| 4901 | { | ||
| 4902 | PayoutTestResetState(); | ||
| 4903 | remove(ledgerPath.c_str()); | ||
| 4904 | remove(payingPath.c_str()); | ||
| 4905 | |||
| 4906 | const std::string badLedger = strprintf( | ||
| 4907 | "[{\"amounts\":{\"%s\":100000000}," | ||
| 4908 | "\"blockHash\":\"deadbeef oops, not a hash\"," | ||
| 4909 | "\"blockHeight\":10,\"matureAtHeight\":10}]", addrA.c_str()); | ||
| 4910 | check(PayoutTestWriteFile(ledgerPath, badLedger), | ||
| 4911 | "wrote a ledger whose blockHash is not a 64-digit hex string"); | ||
| 4912 | LoadPendingPayouts(); | ||
| 4913 | { | ||
| 4914 | std::lock_guard<std::mutex> lk(gPayoutLedgerMutex); | ||
| 4915 | check(gPendingPayouts.empty(), | ||
| 4916 | "no entry is loaded with a guessed hash"); | ||
| 4917 | } | ||
| 4918 | std::string gone; | ||
| 4919 | check(!PayoutTestReadFile(ledgerPath, gone), | ||
| 4920 | "the ledger with the corrupt hash is quarantined"); | ||
| 4921 | |||
| 4922 | const std::string badPaying = strprintf( | ||
| 4923 | "[{\"address\":\"%s\",\"amount\":100000000," | ||
| 4924 | "\"blockHash\":\"00000000cafe\",\"blockHeight\":10," | ||
| 4925 | "\"matureAtHeight\":10}]", addrA.c_str()); | ||
| 4926 | check(PayoutTestWriteFile(payingPath, badPaying), | ||
| 4927 | "wrote an in-flight list whose blockHash is truncated"); | ||
| 4928 | LoadPaying(); | ||
| 4929 | { | ||
| 4930 | std::lock_guard<std::mutex> lk(gPayoutExecMutex); | ||
| 4931 | check(gPaying.empty(), | ||
| 4932 | "no in-flight record is loaded with a guessed hash"); | ||
| 4933 | } | ||
| 4934 | check(!PayoutTestReadFile(payingPath, gone), | ||
| 4935 | "the in-flight list with the corrupt hash is quarantined"); | ||
| 4936 | |||
| 4937 | // The two spellings every real file uses still load: the 64-digit | ||
| 4938 | // hash of a keyed round, and the 64 zeros this build writes for a | ||
| 4939 | // grandfathered entry. | ||
| 4940 | PayoutTestResetState(); | ||
| 4941 | remove(ledgerPath.c_str()); | ||
| 4942 | const std::string goodLedger = strprintf( | ||
| 4943 | "[{\"amounts\":{\"%s\":100000000}," | ||
| 4944 | "\"blockHash\":\"00000000000000000000000000000000000000000000000000000000000000cd\"," | ||
| 4945 | "\"blockHeight\":10,\"matureAtHeight\":10}," | ||
| 4946 | "{\"amounts\":{\"%s\":200000000}," | ||
| 4947 | "\"blockHash\":\"%s\"," | ||
| 4948 | "\"blockHeight\":20,\"matureAtHeight\":20}]", | ||
| 4949 | addrA.c_str(), addrB.c_str(), uint256(0).ToString().c_str()); | ||
| 4950 | check(PayoutTestWriteFile(ledgerPath, goodLedger), | ||
| 4951 | "wrote a ledger in both real hash spellings"); | ||
| 4952 | LoadPendingPayouts(); | ||
| 4953 | { | ||
| 4954 | std::lock_guard<std::mutex> lk(gPayoutLedgerMutex); | ||
| 4955 | check(gPendingPayouts.size() == 2 && | ||
| 4956 | gPendingPayouts[0].blockHash != 0 && | ||
| 4957 | gPendingPayouts[1].blockHash == 0, | ||
| 4958 | "a real hash and a serialised zero hash both still load"); | ||
| 4959 | } | ||
| 4960 | remove(ledgerPath.c_str()); | ||
| 4961 | } | ||
| 4962 | |||
| 4604 | // -- clean up ------------------------------------------------------------ | 4963 | // -- clean up ------------------------------------------------------------ |
| 4605 | PayoutTestResetState(); | 4964 | PayoutTestResetState(); |
| 4606 | CRITICAL_BLOCK(cs_mapWallet) | 4965 | CRITICAL_BLOCK(cs_mapWallet) |