diff options
Diffstat (limited to 'src/rpc.cpp')
| -rw-r--r-- | src/rpc.cpp | 182 |
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 | ||
| 1135 | static 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. | ||
| 1174 | enum 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 | |||
| 1180 | static 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. | ||
| 1253 | static std::map<std::string, std::string> gLegacyClaimedTxids; | ||
| 1254 | static 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. | ||
| 1259 | static 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 | |||
| 1282 | static 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; |