upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-09-02 08:54:01 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-09-02 08:54:01 +0000
commit6c37f59ea136cbace8b0cfccefdad555ff3d619f (patch)
tree82d770ffa87d8a916a8dab384160689be35d555c
parente4f898c4a6b9900553c64b9804889b5fdae3e55f (diff)
payout: name the funding block in new payout comments
A height the pool wins, loses to a reorg, and wins again yields two rounds whose height-only comments are identical -- and identical amounts too when the same miner held both rounds. New sends now write the funding block hash into the comment. The legacy fallback's needle stays frozen at the pre-upgrade format: it exists precisely for transactions that carry nothing else, and a needle in any other format would make every one of those payments look unpaid and pay it again.
-rw-r--r--src/rpc.cpp97
1 files changed, 93 insertions, 4 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp
index a4b53ca..f9471c2 100644
--- a/src/rpc.cpp
+++ b/src/rpc.cpp
@@ -1186,11 +1186,35 @@ static bool PayoutBlockStillOnChain(const uint256& blockHash)
1186 return false; 1186 return false;
1187} 1187}
1188 1188
1189static std::string PayoutComment(int blockHeight) 1189// The comment every pre-upgrade payout transaction carries, and therefore the
1190// ONLY needle the legacy fallback in FindRecordedPoolPayout may search with.
1191// This format is frozen: change it and every payment those builds made stops
1192// matching, every one of those debts looks unpaid, and reconciliation pays
1193// them all a second time.
1194static std::string LegacyPayoutComment(int blockHeight)
1190{ 1195{
1191 return strprintf("Pool payout height %d", blockHeight); 1196 return strprintf("Pool payout height %d", blockHeight);
1192} 1197}
1193 1198
1199// The comment written on new sends. It names the funding block, because the
1200// height alone cannot: a height that is won, lost to a reorg, and won again
1201// yields two rounds whose height-only comments are identical -- and identical
1202// amounts too, whenever the same miner held both rounds. The structured
1203// pool_payout_* metadata already keys the payment by hash for the code; this
1204// puts the same fact where an operator reading the wallet can see it, and
1205// makes a new transaction unmatchable by the legacy needle even if the
1206// structured keys are ever lost.
1207//
1208// A zero hash keeps the legacy form: those records predate hash keying, and
1209// there is no hash to name.
1210static std::string PayoutComment(int blockHeight, const uint256& blockHash)
1211{
1212 if (blockHash == 0)
1213 return LegacyPayoutComment(blockHeight);
1214 return strprintf("Pool payout height %d block %s", blockHeight,
1215 blockHash.ToString().c_str());
1216}
1217
1194// Startup reconciliation. 1218// Startup reconciliation.
1195// 1219//
1196// This used to be the crash-recovery mechanism: it scanned the wallet with 1220// This used to be the crash-recovery mechanism: it scanned the wallet with
@@ -1425,7 +1449,7 @@ static PayoutProbeResult FindRecordedPoolPayout(const std::string& id,
1425 CScript want; 1449 CScript want;
1426 if (fHaveScript) 1450 if (fHaveScript)
1427 want << OP_DUP << OP_HASH160 << h160 << OP_EQUALVERIFY << OP_CHECKSIG; 1451 want << OP_DUP << OP_HASH160 << h160 << OP_EQUALVERIFY << OP_CHECKSIG;
1428 const std::string comment = PayoutComment(blockHeight); 1452 const std::string comment = LegacyPayoutComment(blockHeight);
1429 1453
1430 // Candidate legacy matches, adjudicated after cs_mapWallet is released. 1454 // Candidate legacy matches, adjudicated after cs_mapWallet is released.
1431 // 1455 //
@@ -2186,7 +2210,7 @@ static void PayoutThreadFn(void*)
2186 } 2210 }
2187 2211
2188 CWalletTx wtx; 2212 CWalletTx wtx;
2189 wtx.mapValue["comment"] = PayoutComment(r.blockHeight); 2213 wtx.mapValue["comment"] = PayoutComment(r.blockHeight, r.blockHash);
2190 // Upstream's structured metadata. This is what makes the wallet 2214 // Upstream's structured metadata. This is what makes the wallet
2191 // probe above possible at all -- the old comment-only form could 2215 // probe above possible at all -- the old comment-only form could
2192 // not distinguish two rounds paying the same address the same 2216 // not distinguish two rounds paying the same address the same
@@ -3825,7 +3849,7 @@ int RunPayoutLedgerSelfTest()
3825 const int height = 8080; 3849 const int height = 8080;
3826 const int64 amount = 7 * COIN; 3850 const int64 amount = 7 * COIN;
3827 const std::string id = PayoutIdForBlock(height, 0); 3851 const std::string id = PayoutIdForBlock(height, 0);
3828 const std::string comment = PayoutComment(height); 3852 const std::string comment = LegacyPayoutComment(height);
3829 std::set<std::string> none; 3853 std::set<std::string> none;
3830 std::string found; 3854 std::string found;
3831 3855
@@ -4419,6 +4443,71 @@ int RunPayoutLedgerSelfTest()
4419 } 4443 }
4420#endif 4444#endif
4421 4445
4446 // -- 14. The payout comment names the funding block ----------------------
4447 //
4448 // Same height, two hashes: the shape a reorg leaves when the pool wins a
4449 // height, loses the block, and wins the height again. The legacy needle is
4450 // frozen at the pre-upgrade format, so pre-upgrade payments keep matching;
4451 // a new-format comment never matches it, so a new send can never be read
4452 // as a pre-upgrade payment for a twin round.
4453 {
4454 PayoutTestResetState();
4455 CRITICAL_BLOCK(cs_mapWallet)
4456 mapWallet.clear();
4457 PayoutTestClearBlockIndex();
4458
4459 const int height = 9090;
4460 const int64 amount = 3 * COIN;
4461 uint256 hashA = 0, hashB = 0;
4462 hashA.SetHex("00000000000000000000000000000000000000000000000000000000000000aa");
4463 hashB.SetHex("00000000000000000000000000000000000000000000000000000000000000bb");
4464
4465 check(LegacyPayoutComment(height) ==
4466 strprintf("Pool payout height %d", height),
4467 "the legacy needle is frozen at the format pre-upgrade builds"
4468 " wrote");
4469 check(PayoutComment(height, 0) == LegacyPayoutComment(height),
4470 "a grandfathered zero-hash record still writes the legacy form");
4471 check(PayoutComment(height, hashA) ==
4472 strprintf("Pool payout height %d block %s", height,
4473 hashA.ToString().c_str()),
4474 "a hash-keyed record's comment names its funding block");
4475 check(PayoutComment(height, hashA) != PayoutComment(height, hashB),
4476 "twin rounds at one height carry distinguishable comments");
4477
4478 // A transaction wearing the NEW comment format and -- hypothetically
4479 // -- no structured metadata must not be matched by the legacy needle:
4480 // the fallback exists for pre-upgrade transactions, and the new format
4481 // proves this is not one.
4482 const std::string idA = PayoutIdForBlock(height, hashA);
4483 const std::string idB = PayoutIdForBlock(height, hashB);
4484 std::set<std::string> none;
4485 std::string found;
4486 PayoutTestAddWalletTx(PayoutComment(height, hashA), addrA, amount, 1, "");
4487 check(FindRecordedPoolPayout(idB, height, addrA, amount, none, found)
4488 == PAYOUT_PROBE_NONE,
4489 "a new-format comment is never legacy evidence for a twin round");
4490 check(FindRecordedPoolPayout(idA, height, addrA, amount, none, found)
4491 == PAYOUT_PROBE_NONE,
4492 "nor a match channel for its own round -- structured metadata is");
4493
4494 // Round A's own payment, sent by this build, is found by its
4495 // structured id.
4496 CRITICAL_BLOCK(cs_mapWallet)
4497 mapWallet.clear();
4498 PayoutTestClearBlockIndex();
4499 const std::string paidA =
4500 PayoutTestAddWalletTx(PayoutComment(height, hashA), addrA, amount,
4501 2, idA);
4502 check(FindRecordedPoolPayout(idA, height, addrA, amount, none, found)
4503 == PAYOUT_PROBE_STRUCTURED && found == paidA,
4504 "the winning twin's own payment is found by its structured id");
4505
4506 CRITICAL_BLOCK(cs_mapWallet)
4507 mapWallet.clear();
4508 PayoutTestClearBlockIndex();
4509 }
4510
4422 // -- clean up ------------------------------------------------------------ 4511 // -- clean up ------------------------------------------------------------
4423 PayoutTestResetState(); 4512 PayoutTestResetState();
4424 CRITICAL_BLOCK(cs_mapWallet) 4513 CRITICAL_BLOCK(cs_mapWallet)