upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/rpc.cpp182
1 files changed, 164 insertions, 18 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp
index 7badb7c..b71457c 100644
--- a/src/rpc.cpp
+++ b/src/rpc.cpp
@@ -1132,11 +1132,71 @@ static void MarkPoolRoundPaid(const std::string& id,
1132 PublishPoolRounds(snap); 1132 PublishPoolRounds(snap);
1133} 1133}
1134 1134
1135static bool FindRecordedPoolPayout(const std::string& id, 1135// Which wallet transaction, if any, already paid this?
1136 const std::string& address, 1136//
1137 int64 amount, 1137// Two kinds of evidence, in order of quality.
1138 std::string& txidRet) 1138//
1139// Structured (pool_payout_id / pool_payout_address / pool_payout_amount) is
1140// upstream's, and it is exact: the id is derived from the block hash, so it
1141// names one round and one round only. Every transaction this build sends
1142// carries it.
1143//
1144// But NO transaction this pool has ever sent carries it. Every payout in the
1145// existing wallet has only a "comment". So on the first boot after this
1146// upgrade -- precisely the boot where the binary is swapped after an unclean
1147// shutdown with payments in flight -- a structured-only probe finds nothing
1148// and every one of those recipients is paid a second time. The deleted
1149// WalletAlreadyPaid() matched on what the payment WAS, and that evidence is
1150// still there; it just has to be read.
1151//
1152// The legacy fallback is therefore restored, with three constraints that
1153// together stop it answering for a round that is not this one:
1154//
1155// 1. it is only consulted when the structured lookup found nothing;
1156// 2. it ignores any transaction carrying pool_payout_id. That key means this
1157// build wrote the transaction, and this build's transactions are
1158// adjudicated by the exact path -- so a NEW round's payment can never be
1159// offered as evidence for an old one. Only pre-upgrade transactions,
1160// which is exactly the population the fallback exists for, are eligible;
1161// 3. `excluded` holds every txid already credited to some OTHER
1162// (round, address) pair -- from the ledger's paidTxids, which is durable,
1163// and from what this process has already claimed. One legacy transaction
1164// can therefore settle at most one debt, so N transactions can never
1165// satisfy more than N obligations.
1166//
1167// What is left unresolved by construction is two pre-upgrade rounds at the
1168// SAME height paying the same address the same amount, since the comment
1169// carries only the height. That needs a reorg, and the losing round's ledger
1170// entry is voided by the orphan check before it can be paid, so the pair
1171// cannot both be live. Where the evidence genuinely runs out the fallback
1172// simply does not match, and the payment is made -- paying a debt we cannot
1173// prove was settled is the correct default for a pool.
1174enum PayoutProbeResult {
1175 PAYOUT_PROBE_NONE = 0, // the wallet holds no record of this payment
1176 PAYOUT_PROBE_STRUCTURED, // matched this build's pool_payout_* metadata
1177 PAYOUT_PROBE_LEGACY // matched a pre-upgrade comment and output
1178};
1179
1180static PayoutProbeResult FindRecordedPoolPayout(const std::string& id,
1181 int blockHeight,
1182 const std::string& address,
1183 int64 amount,
1184 const std::set<std::string>& excluded,
1185 std::string& txidRet)
1139{ 1186{
1187 // Built before cs_mapWallet is taken: an invalid address has no script to
1188 // compare against, and the caller has already rejected those.
1189 uint160 h160;
1190 bool fHaveScript = AddressToHash160(address, h160);
1191 CScript want;
1192 if (fHaveScript)
1193 want << OP_DUP << OP_HASH160 << h160 << OP_EQUALVERIFY << OP_CHECKSIG;
1194 const std::string comment = PayoutComment(blockHeight);
1195
1196 std::string legacyTxid;
1197
1198 // One pass, not two: this scan runs under cs_mapWallet, which the whole
1199 // node contends on, and the merge is already paying for it once per record.
1140 CRITICAL_BLOCK(cs_mapWallet) 1200 CRITICAL_BLOCK(cs_mapWallet)
1141 { 1201 {
1142 for (map<uint256, CWalletTx>::const_iterator it = mapWallet.begin(); 1202 for (map<uint256, CWalletTx>::const_iterator it = mapWallet.begin();
@@ -1144,19 +1204,85 @@ static bool FindRecordedPoolPayout(const std::string& id,
1144 { 1204 {
1145 const CWalletTx& wtx = it->second; 1205 const CWalletTx& wtx = it->second;
1146 map<string, string>::const_iterator idIt = wtx.mapValue.find("pool_payout_id"); 1206 map<string, string>::const_iterator idIt = wtx.mapValue.find("pool_payout_id");
1147 if (idIt == wtx.mapValue.end() || idIt->second != id) 1207 if (idIt != wtx.mapValue.end()) {
1148 continue; 1208 // Written by this build. Adjudicated exactly, and never
1149 map<string, string>::const_iterator addrIt = wtx.mapValue.find("pool_payout_address"); 1209 // offered to the fallback below.
1150 if (addrIt == wtx.mapValue.end() || addrIt->second != address) 1210 if (idIt->second != id)
1211 continue;
1212 map<string, string>::const_iterator addrIt = wtx.mapValue.find("pool_payout_address");
1213 if (addrIt == wtx.mapValue.end() || addrIt->second != address)
1214 continue;
1215 map<string, string>::const_iterator amtIt = wtx.mapValue.find("pool_payout_amount");
1216 if (amtIt == wtx.mapValue.end() || amtIt->second != strprintf("%lld", amount))
1217 continue;
1218 txidRet = wtx.GetHash().GetHex();
1219 return PAYOUT_PROBE_STRUCTURED;
1220 }
1221
1222 if (!fHaveScript || !legacyTxid.empty())
1151 continue; 1223 continue;
1152 map<string, string>::const_iterator amtIt = wtx.mapValue.find("pool_payout_amount"); 1224 // The comment is written only by the payout path, so its presence
1153 if (amtIt == wtx.mapValue.end() || amtIt->second != strprintf("%lld", amount)) 1225 // already identifies the transaction as one of ours.
1226 map<string, string>::const_iterator ci = wtx.mapValue.find("comment");
1227 if (ci == wtx.mapValue.end() || ci->second != comment)
1154 continue; 1228 continue;
1155 txidRet = wtx.GetHash().GetHex(); 1229 std::string txid = wtx.GetHash().GetHex();
1156 return true; 1230 if (excluded.count(txid))
1231 continue; // already credited to a different debt
1232 foreach (const CTxOut& txout, wtx.vout)
1233 if (txout.nValue == amount && txout.scriptPubKey == want) {
1234 legacyTxid = txid;
1235 break;
1236 }
1157 } 1237 }
1158 } 1238 }
1159 return false; 1239
1240 if (!legacyTxid.empty()) {
1241 txidRet = legacyTxid;
1242 return PAYOUT_PROBE_LEGACY;
1243 }
1244 return PAYOUT_PROBE_NONE;
1245}
1246
1247// txid -> "<round id>|<address>", for payments this run has already credited
1248// to a debt on legacy evidence. Stops one pre-upgrade transaction being read
1249// as proof that two different obligations were met. The ledger's paidTxids
1250// covers the same ground durably; this covers the window before the recovered
1251// txid reaches the ledger, and the case where the ledger entry has already
1252// been retired.
1253static std::map<std::string, std::string> gLegacyClaimedTxids;
1254static std::mutex gClaimedTxidMutex;
1255
1256// Every txid already spoken for by some payment OTHER than this one. Collected
1257// before cs_mapWallet is taken: the ledger mutex must not be acquired under a
1258// wallet lock.
1259static void CollectForeignPayoutTxids(int blockHeight, const uint256& blockHash,
1260 const std::string& address,
1261 const std::string& claimKey,
1262 std::set<std::string>& out)
1263{
1264 {
1265 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
1266 for (const PendingPayout& pp : gPendingPayouts) {
1267 bool sameRound = SamePayoutRound(pp.blockHeight, pp.blockHash,
1268 blockHeight, blockHash);
1269 for (const auto& kv : pp.paidTxids)
1270 if (!(sameRound && kv.first == address))
1271 out.insert(kv.second);
1272 }
1273 }
1274 {
1275 std::lock_guard<std::mutex> lk(gClaimedTxidMutex);
1276 for (const auto& kv : gLegacyClaimedTxids)
1277 if (kv.second != claimKey)
1278 out.insert(kv.first);
1279 }
1280}
1281
1282static void ClaimLegacyPayoutTxid(const std::string& txid, const std::string& claimKey)
1283{
1284 std::lock_guard<std::mutex> lk(gClaimedTxidMutex);
1285 gLegacyClaimedTxids[txid] = claimKey;
1160} 1286}
1161 1287
1162// blockHash stays a uint256 and not upstream's display string: it is the key 1288// blockHash stays a uint256 and not upstream's display string: it is the key
@@ -1603,12 +1729,32 @@ static void PayoutThreadFn(void*)
1603 // Only asked once per record -- see PayingRecord::fProbeWallet. 1729 // Only asked once per record -- see PayingRecord::fProbeWallet.
1604 if (r.fProbeWallet) { 1730 if (r.fProbeWallet) {
1605 std::string recovered; 1731 std::string recovered;
1732 const std::string claimKey = id + "|" + r.address;
1733 std::set<std::string> excluded;
1734 CollectForeignPayoutTxids(r.blockHeight, r.blockHash, r.address,
1735 claimKey, excluded);
1606 r.fProbeWallet = false; 1736 r.fProbeWallet = false;
1607 if (FindRecordedPoolPayout(id, r.address, r.amount, recovered)) { 1737 PayoutProbeResult probe = FindRecordedPoolPayout(
1608 LogPrint("payout", "[payout] recovered recorded payout %s to %s" 1738 id, r.blockHeight, r.address, r.amount, excluded, recovered);
1609 " from wallet tx %s -- not resending\n", 1739 if (probe != PAYOUT_PROBE_NONE) {
1610 FormatMoney(r.amount).c_str(), r.address.c_str(), 1740 if (probe == PAYOUT_PROBE_LEGACY) {
1611 recovered.c_str()); 1741 // Loud on purpose: this is the upgrade path, where the
1742 // only evidence a payment went out is a comment written
1743 // by a binary that is no longer running.
1744 LogPrint("payout", "[payout] %s to %s was already paid by"
1745 " a pre-upgrade build (wallet tx %s carries the"
1746 " payout comment for height %d and an output of"
1747 " exactly this amount, and no structured payout"
1748 " metadata) -- not resending\n",
1749 FormatMoney(r.amount).c_str(), r.address.c_str(),
1750 recovered.c_str(), r.blockHeight);
1751 ClaimLegacyPayoutTxid(recovered, claimKey);
1752 } else {
1753 LogPrint("payout", "[payout] recovered recorded payout %s to %s"
1754 " from wallet tx %s -- not resending\n",
1755 FormatMoney(r.amount).c_str(), r.address.c_str(),
1756 recovered.c_str());
1757 }
1612 RecordPayoutTxid(r, recovered); 1758 RecordPayoutTxid(r, recovered);
1613 ForgetPayment(r); 1759 ForgetPayment(r);
1614 continue; 1760 continue;