upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/rpc.cpp413
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.
826static 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
815static void LoadPendingPayouts() 838static 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
1225static 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.
1255static 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.
1271static 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.
1442static 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
1450static PayoutProbeResult FindRecordedPoolPayout(const std::string& id, 1533static 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)