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:56:46 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-09-02 08:56:46 +0000
commita2e1f27b32442bcea92eeda3ac09432f6c5ec2c5 (patch)
tree8f4d829a68b11f8b9804ae0963fdc2dff5232fe5
parent6c37f59ea136cbace8b0cfccefdad555ff3d619f (diff)
payout: record the full funding block hash and require it to agree
pool_payout_id keeps only the first sixteen hex digits of the block hash, which is the end proof-of-work fills with zeros -- so twin rounds at one height can carry the SAME id, and one twin's payment then settles the other's debt through the structured probe: the same-height collision the comment match had, one layer down. New sends record the full hash as pool_payout_block and a structured match must agree with it. A transaction without the key predates it and is matched by id as before; anything stricter would make every payment already sent look unpaid and pay it again.
-rw-r--r--src/rpc.cpp96
1 files changed, 76 insertions, 20 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp
index f9471c2..dbbf06d 100644
--- a/src/rpc.cpp
+++ b/src/rpc.cpp
@@ -1373,10 +1373,15 @@ static void MarkPoolRoundForfeited(const std::string& id,
1373// 1373//
1374// Two kinds of evidence, in order of quality. 1374// Two kinds of evidence, in order of quality.
1375// 1375//
1376// Structured (pool_payout_id / pool_payout_address / pool_payout_amount) is 1376// Structured (pool_payout_id / pool_payout_address / pool_payout_amount,
1377// upstream's, and it is exact: the id is derived from the block hash, so it 1377// plus pool_payout_block since this series) is upstream's, extended. The id
1378// names one round and one round only. Every transaction this build sends 1378// is derived from the block hash, but only from the FIRST sixteen hex digits
1379// carries it. 1379// of it -- and proof-of-work concentrates zeros exactly there, so the id can
1380// name two same-height rounds identically. New sends therefore record the
1381// full hash as pool_payout_block, and a structured match must agree with it
1382// wherever both sides have one; a transaction without the key was sent
1383// before it existed, and for those the id is the best evidence there is.
1384// Every transaction this build sends carries all four keys.
1380// 1385//
1381// But NO transaction this pool has ever sent carries it. Every payout in the 1386// But NO transaction this pool has ever sent carries it. Every payout in the
1382// existing wallet has only a "comment". So on the first boot after this 1387// existing wallet has only a "comment". So on the first boot after this
@@ -1437,6 +1442,7 @@ enum PayoutProbeResult {
1437 1442
1438static PayoutProbeResult FindRecordedPoolPayout(const std::string& id, 1443static PayoutProbeResult FindRecordedPoolPayout(const std::string& id,
1439 int blockHeight, 1444 int blockHeight,
1445 const uint256& blockHash,
1440 const std::string& address, 1446 const std::string& address,
1441 int64 amount, 1447 int64 amount,
1442 const std::set<std::string>& excluded, 1448 const std::set<std::string>& excluded,
@@ -1482,6 +1488,19 @@ static PayoutProbeResult FindRecordedPoolPayout(const std::string& id,
1482 map<string, string>::const_iterator amtIt = wtx.mapValue.find("pool_payout_amount"); 1488 map<string, string>::const_iterator amtIt = wtx.mapValue.find("pool_payout_amount");
1483 if (amtIt == wtx.mapValue.end() || amtIt->second != strprintf("%lld", amount)) 1489 if (amtIt == wtx.mapValue.end() || amtIt->second != strprintf("%lld", amount))
1484 continue; 1490 continue;
1491 // The id alone cannot tell twin rounds at one height apart
1492 // (see above); the full hash can, where the transaction
1493 // recorded one. Where it did not, the id decides, as it
1494 // always has -- refusing those transactions would make every
1495 // payment sent before the key existed look unpaid and pay it
1496 // again.
1497 if (blockHash != 0) {
1498 map<string, string>::const_iterator blkIt =
1499 wtx.mapValue.find("pool_payout_block");
1500 if (blkIt != wtx.mapValue.end()
1501 && blkIt->second != blockHash.ToString())
1502 continue; // a twin round's payment, not this round's
1503 }
1485 txidRet = wtx.GetHash().GetHex(); 1504 txidRet = wtx.GetHash().GetHex();
1486 return PAYOUT_PROBE_STRUCTURED; 1505 return PAYOUT_PROBE_STRUCTURED;
1487 } 1506 }
@@ -2125,7 +2144,8 @@ static void PayoutThreadFn(void*)
2125 claimKey, excluded); 2144 claimKey, excluded);
2126 r.fProbeWallet = false; 2145 r.fProbeWallet = false;
2127 PayoutProbeResult probe = FindRecordedPoolPayout( 2146 PayoutProbeResult probe = FindRecordedPoolPayout(
2128 id, r.blockHeight, r.address, r.amount, excluded, recovered); 2147 id, r.blockHeight, r.blockHash, r.address, r.amount,
2148 excluded, recovered);
2129 if (probe != PAYOUT_PROBE_NONE) { 2149 if (probe != PAYOUT_PROBE_NONE) {
2130 if (probe == PAYOUT_PROBE_LEGACY) { 2150 if (probe == PAYOUT_PROBE_LEGACY) {
2131 // Loud on purpose: this is the upgrade path, where the 2151 // Loud on purpose: this is the upgrade path, where the
@@ -2219,6 +2239,11 @@ static void PayoutThreadFn(void*)
2219 wtx.mapValue["pool_payout_id"] = id; 2239 wtx.mapValue["pool_payout_id"] = id;
2220 wtx.mapValue["pool_payout_address"] = r.address; 2240 wtx.mapValue["pool_payout_address"] = r.address;
2221 wtx.mapValue["pool_payout_amount"] = strprintf("%lld", r.amount); 2241 wtx.mapValue["pool_payout_amount"] = strprintf("%lld", r.amount);
2242 // The full hash, not only the id's sixteen-digit prefix: the
2243 // prefix of a proof-of-work hash is where the zeros live, and
2244 // the probe needs to tell twin rounds at one height apart.
2245 if (r.blockHash != 0)
2246 wtx.mapValue["pool_payout_block"] = r.blockHash.ToString();
2222 2247
2223 SendMoneyOutcome outcome; 2248 SendMoneyOutcome outcome;
2224 if (SendMoney(sc, r.amount, wtx, outcome)) { 2249 if (SendMoney(sc, r.amount, wtx, outcome)) {
@@ -3285,7 +3310,8 @@ static std::string PayoutTestAddWalletTx(const std::string& comment,
3285 int64 amount, 3310 int64 amount,
3286 int nonce, 3311 int nonce,
3287 const std::string& structuredId, 3312 const std::string& structuredId,
3288 bool fConfirmed = true) 3313 bool fConfirmed = true,
3314 const std::string& structuredBlock = "")
3289{ 3315{
3290 CWalletTx wtx; 3316 CWalletTx wtx;
3291 wtx.vin.resize(1); 3317 wtx.vin.resize(1);
@@ -3300,6 +3326,8 @@ static std::string PayoutTestAddWalletTx(const std::string& comment,
3300 wtx.mapValue["pool_payout_id"] = structuredId; 3326 wtx.mapValue["pool_payout_id"] = structuredId;
3301 wtx.mapValue["pool_payout_address"] = address; 3327 wtx.mapValue["pool_payout_address"] = address;
3302 wtx.mapValue["pool_payout_amount"] = strprintf("%lld", amount); 3328 wtx.mapValue["pool_payout_amount"] = strprintf("%lld", amount);
3329 if (!structuredBlock.empty())
3330 wtx.mapValue["pool_payout_block"] = structuredBlock;
3303 } 3331 }
3304 uint256 hash = wtx.GetHash(); 3332 uint256 hash = wtx.GetHash();
3305 if (fConfirmed) { 3333 if (fConfirmed) {
@@ -3853,30 +3881,30 @@ int RunPayoutLedgerSelfTest()
3853 std::set<std::string> none; 3881 std::set<std::string> none;
3854 std::string found; 3882 std::string found;
3855 3883
3856 check(FindRecordedPoolPayout(id, height, addrA, amount, none, found) 3884 check(FindRecordedPoolPayout(id, height, 0, addrA, amount, none, found)
3857 == PAYOUT_PROBE_NONE, 3885 == PAYOUT_PROBE_NONE,
3858 "an empty wallet is not evidence that anything was paid"); 3886 "an empty wallet is not evidence that anything was paid");
3859 3887
3860 const std::string legacyTxid = 3888 const std::string legacyTxid =
3861 PayoutTestAddWalletTx(comment, addrA, amount, 1, ""); 3889 PayoutTestAddWalletTx(comment, addrA, amount, 1, "");
3862 check(FindRecordedPoolPayout(id, height, addrA, amount, none, found) 3890 check(FindRecordedPoolPayout(id, height, 0, addrA, amount, none, found)
3863 == PAYOUT_PROBE_LEGACY && found == legacyTxid, 3891 == PAYOUT_PROBE_LEGACY && found == legacyTxid,
3864 "a payment carrying only a pre-upgrade comment is recognised as" 3892 "a payment carrying only a pre-upgrade comment is recognised as"
3865 " already paid"); 3893 " already paid");
3866 3894
3867 check(FindRecordedPoolPayout(id, height, addrB, amount, none, found) 3895 check(FindRecordedPoolPayout(id, height, 0, addrB, amount, none, found)
3868 == PAYOUT_PROBE_NONE, 3896 == PAYOUT_PROBE_NONE,
3869 "the legacy match requires the output to pay THIS recipient"); 3897 "the legacy match requires the output to pay THIS recipient");
3870 check(FindRecordedPoolPayout(id, height, addrA, amount + COIN, none, found) 3898 check(FindRecordedPoolPayout(id, height, 0, addrA, amount + COIN, none, found)
3871 == PAYOUT_PROBE_NONE, 3899 == PAYOUT_PROBE_NONE,
3872 "the legacy match requires the output to pay exactly this amount"); 3900 "the legacy match requires the output to pay exactly this amount");
3873 check(FindRecordedPoolPayout(id, height + 1, addrA, amount, none, found) 3901 check(FindRecordedPoolPayout(id, height + 1, 0, addrA, amount, none, found)
3874 == PAYOUT_PROBE_NONE, 3902 == PAYOUT_PROBE_NONE,
3875 "the legacy match requires the comment for this block height"); 3903 "the legacy match requires the comment for this block height");
3876 3904
3877 std::set<std::string> spent; 3905 std::set<std::string> spent;
3878 spent.insert(legacyTxid); 3906 spent.insert(legacyTxid);
3879 check(FindRecordedPoolPayout(id, height, addrA, amount, spent, found) 3907 check(FindRecordedPoolPayout(id, height, 0, addrA, amount, spent, found)
3880 == PAYOUT_PROBE_NONE, 3908 == PAYOUT_PROBE_NONE,
3881 "one legacy transaction cannot settle a second debt"); 3909 "one legacy transaction cannot settle a second debt");
3882 3910
@@ -3885,7 +3913,7 @@ int RunPayoutLedgerSelfTest()
3885 // exactly what the structured metadata exists to prevent. 3913 // exactly what the structured metadata exists to prevent.
3886 const std::string otherId = PayoutIdForBlock(height, 0) + "-other"; 3914 const std::string otherId = PayoutIdForBlock(height, 0) + "-other";
3887 PayoutTestAddWalletTx(comment, addrB, amount, 2, otherId); 3915 PayoutTestAddWalletTx(comment, addrB, amount, 2, otherId);
3888 check(FindRecordedPoolPayout(id, height, addrB, amount, none, found) 3916 check(FindRecordedPoolPayout(id, height, 0, addrB, amount, none, found)
3889 == PAYOUT_PROBE_NONE, 3917 == PAYOUT_PROBE_NONE,
3890 "another round's structured payment is not legacy evidence for" 3918 "another round's structured payment is not legacy evidence for"
3891 " this one"); 3919 " this one");
@@ -3893,7 +3921,7 @@ int RunPayoutLedgerSelfTest()
3893 // The structured path still wins where it applies. 3921 // The structured path still wins where it applies.
3894 const std::string structuredTxid = 3922 const std::string structuredTxid =
3895 PayoutTestAddWalletTx(comment, addrB, amount, 3, id); 3923 PayoutTestAddWalletTx(comment, addrB, amount, 3, id);
3896 check(FindRecordedPoolPayout(id, height, addrB, amount, none, found) 3924 check(FindRecordedPoolPayout(id, height, 0, addrB, amount, none, found)
3897 == PAYOUT_PROBE_STRUCTURED && found == structuredTxid, 3925 == PAYOUT_PROBE_STRUCTURED && found == structuredTxid,
3898 "structured payout metadata is matched in preference to a comment"); 3926 "structured payout metadata is matched in preference to a comment");
3899 3927
@@ -3907,7 +3935,7 @@ int RunPayoutLedgerSelfTest()
3907 PayoutTestClearBlockIndex(); 3935 PayoutTestClearBlockIndex();
3908 const std::string addrC = PayoutTestAddress(0x33); 3936 const std::string addrC = PayoutTestAddress(0x33);
3909 PayoutTestAddWalletTx(comment, addrC, amount, 4, "", false); 3937 PayoutTestAddWalletTx(comment, addrC, amount, 4, "", false);
3910 check(FindRecordedPoolPayout(id, height, addrC, amount, none, found) 3938 check(FindRecordedPoolPayout(id, height, 0, addrC, amount, none, found)
3911 == PAYOUT_PROBE_NONE, 3939 == PAYOUT_PROBE_NONE,
3912 "a pre-upgrade payout that never confirmed is not read as" 3940 "a pre-upgrade payout that never confirmed is not read as"
3913 " payment"); 3941 " payment");
@@ -3917,7 +3945,7 @@ int RunPayoutLedgerSelfTest()
3917 mapWallet.clear(); 3945 mapWallet.clear();
3918 const std::string confirmedTxid = 3946 const std::string confirmedTxid =
3919 PayoutTestAddWalletTx(comment, addrC, amount, 4, "", true); 3947 PayoutTestAddWalletTx(comment, addrC, amount, 4, "", true);
3920 check(FindRecordedPoolPayout(id, height, addrC, amount, none, found) 3948 check(FindRecordedPoolPayout(id, height, 0, addrC, amount, none, found)
3921 == PAYOUT_PROBE_LEGACY && found == confirmedTxid, 3949 == PAYOUT_PROBE_LEGACY && found == confirmedTxid,
3922 "the same payout, confirmed, is"); 3950 "the same payout, confirmed, is");
3923 3951
@@ -3928,7 +3956,7 @@ int RunPayoutLedgerSelfTest()
3928 PayoutTestAddWalletTx(comment, addrC, amount, 5, "", false); 3956 PayoutTestAddWalletTx(comment, addrC, amount, 5, "", false);
3929 std::set<std::string> spentFirst; 3957 std::set<std::string> spentFirst;
3930 spentFirst.insert(confirmedTxid); 3958 spentFirst.insert(confirmedTxid);
3931 check(FindRecordedPoolPayout(id, height, addrC, amount, spentFirst, found) 3959 check(FindRecordedPoolPayout(id, height, 0, addrC, amount, spentFirst, found)
3932 == PAYOUT_PROBE_LEGACY && found == alsoConfirmed, 3960 == PAYOUT_PROBE_LEGACY && found == alsoConfirmed,
3933 "an unconfirmed transaction does not hide a confirmed one behind" 3961 "an unconfirmed transaction does not hide a confirmed one behind"
3934 " it"); 3962 " it");
@@ -4484,10 +4512,10 @@ int RunPayoutLedgerSelfTest()
4484 std::set<std::string> none; 4512 std::set<std::string> none;
4485 std::string found; 4513 std::string found;
4486 PayoutTestAddWalletTx(PayoutComment(height, hashA), addrA, amount, 1, ""); 4514 PayoutTestAddWalletTx(PayoutComment(height, hashA), addrA, amount, 1, "");
4487 check(FindRecordedPoolPayout(idB, height, addrA, amount, none, found) 4515 check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount, none, found)
4488 == PAYOUT_PROBE_NONE, 4516 == PAYOUT_PROBE_NONE,
4489 "a new-format comment is never legacy evidence for a twin round"); 4517 "a new-format comment is never legacy evidence for a twin round");
4490 check(FindRecordedPoolPayout(idA, height, addrA, amount, none, found) 4518 check(FindRecordedPoolPayout(idA, height, hashA, addrA, amount, none, found)
4491 == PAYOUT_PROBE_NONE, 4519 == PAYOUT_PROBE_NONE,
4492 "nor a match channel for its own round -- structured metadata is"); 4520 "nor a match channel for its own round -- structured metadata is");
4493 4521
@@ -4499,10 +4527,38 @@ int RunPayoutLedgerSelfTest()
4499 const std::string paidA = 4527 const std::string paidA =
4500 PayoutTestAddWalletTx(PayoutComment(height, hashA), addrA, amount, 4528 PayoutTestAddWalletTx(PayoutComment(height, hashA), addrA, amount,
4501 2, idA); 4529 2, idA);
4502 check(FindRecordedPoolPayout(idA, height, addrA, amount, none, found) 4530 check(FindRecordedPoolPayout(idA, height, hashA, addrA, amount, none, found)
4503 == PAYOUT_PROBE_STRUCTURED && found == paidA, 4531 == PAYOUT_PROBE_STRUCTURED && found == paidA,
4504 "the winning twin's own payment is found by its structured id"); 4532 "the winning twin's own payment is found by its structured id");
4505 4533
4534 // The id is derived from the FIRST sixteen hex digits of the hash --
4535 // the end proof-of-work fills with zeros -- so twin hashes that agree
4536 // there collide in id, and a payment carrying no pool_payout_block
4537 // answers for either twin. That is the accepted trade for payments
4538 // sent before the key existed, pinned here so it stays a decision.
4539 check(idA == idB,
4540 "twin hashes differing only past the sixteenth hex digit collide"
4541 " in id -- the reason the full hash must decide");
4542 check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount, none, found)
4543 == PAYOUT_PROBE_STRUCTURED,
4544 "a payment without the block key answers for a colliding twin --"
4545 " the accepted residual for pre-key transactions");
4546
4547 // With the full hash recorded -- as every send now records it -- the
4548 // twins are told apart and the re-won height is paid.
4549 CRITICAL_BLOCK(cs_mapWallet)
4550 mapWallet.clear();
4551 PayoutTestClearBlockIndex();
4552 const std::string keyedA =
4553 PayoutTestAddWalletTx(PayoutComment(height, hashA), addrA, amount,
4554 3, idA, true, hashA.ToString());
4555 check(FindRecordedPoolPayout(idA, height, hashA, addrA, amount, none, found)
4556 == PAYOUT_PROBE_STRUCTURED && found == keyedA,
4557 "a payment carrying the full block hash settles its own round");
4558 check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount, none, found)
4559 == PAYOUT_PROBE_NONE,
4560 "and is no evidence for the twin, so the re-won height is paid");
4561
4506 CRITICAL_BLOCK(cs_mapWallet) 4562 CRITICAL_BLOCK(cs_mapWallet)
4507 mapWallet.clear(); 4563 mapWallet.clear();
4508 PayoutTestClearBlockIndex(); 4564 PayoutTestClearBlockIndex();