upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/rpc.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/rpc.cpp')
-rw-r--r--src/rpc.cpp214
1 files changed, 175 insertions, 39 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp
index b71457c..6f6d101 100644
--- a/src/rpc.cpp
+++ b/src/rpc.cpp
@@ -386,6 +386,12 @@ struct PoolRoundProof {
386 std::map<std::string, uint64> shares; 386 std::map<std::string, uint64> shares;
387 std::map<std::string, int64> payouts; 387 std::map<std::string, int64> payouts;
388 std::map<std::string, std::string> paidTxids; 388 std::map<std::string, std::string> paidTxids;
389 // Recipients the ledger has written off, and why. Without this the public
390 // round proof goes on advertising an address as owed with "txid": null
391 // forever, while the ledger has already dropped the obligation -- the two
392 // records of the same round permanently disagreeing, with the operator
393 // holding the money.
394 std::map<std::string, std::string> forfeited; // address -> reason
389 395
390 PoolRoundProof() 396 PoolRoundProof()
391 : blockHeight(0), matureAtHeight(0), createdAt(0), feePercent(0.0), 397 : blockHeight(0), matureAtHeight(0), createdAt(0), feePercent(0.0),
@@ -890,6 +896,13 @@ static nlohmann::json PoolRoundToJson(const PoolRoundProof& r)
890 p["txid"] = tx->second; 896 p["txid"] = tx->second;
891 else 897 else
892 p["txid"] = nullptr; 898 p["txid"] = nullptr;
899 // Only emitted when it happened, so the common shape of the file is
900 // unchanged for anything already parsing it.
901 std::map<std::string, std::string>::const_iterator fo = r.forfeited.find(kv.first);
902 if (fo != r.forfeited.end()) {
903 p["forfeited"] = true;
904 p["forfeitReason"] = fo->second;
905 }
893 j["payouts"][kv.first] = p; 906 j["payouts"][kv.first] = p;
894 } 907 }
895 return j; 908 return j;
@@ -966,6 +979,9 @@ static void LoadPoolRounds()
966 r.payouts[it.key()] = it.value().value("amountSatoshis", (int64)0); 979 r.payouts[it.key()] = it.value().value("amountSatoshis", (int64)0);
967 if (it.value().contains("txid") && it.value()["txid"].is_string()) 980 if (it.value().contains("txid") && it.value()["txid"].is_string())
968 r.paidTxids[it.key()] = it.value()["txid"].get<std::string>(); 981 r.paidTxids[it.key()] = it.value()["txid"].get<std::string>();
982 if (it.value().value("forfeited", false))
983 r.forfeited[it.key()] =
984 it.value().value("forfeitReason", std::string("unspecified"));
969 } else if (it.value().is_number_integer()) { 985 } else if (it.value().is_number_integer()) {
970 r.payouts[it.key()] = it.value().get<int64>(); 986 r.payouts[it.key()] = it.value().get<int64>();
971 } 987 }
@@ -1132,6 +1148,33 @@ static void MarkPoolRoundPaid(const std::string& id,
1132 PublishPoolRounds(snap); 1148 PublishPoolRounds(snap);
1133} 1149}
1134 1150
1151// Publish the fact that a recipient was written off, so the round proof and
1152// the ledger agree. The ledger erases the amount (it is not owed any more and
1153// must not be reported as sent); the proof keeps the amount visible and marks
1154// it forfeited with a reason, because a miner who was promised a share is
1155// entitled to see what happened to it.
1156static void MarkPoolRoundForfeited(const std::string& id,
1157 const std::string& address,
1158 const std::string& reason)
1159{
1160 VersionedBlob snap;
1161 {
1162 std::lock_guard<std::mutex> lk(gPoolRoundsMutex);
1163 bool found = false;
1164 for (PoolRoundProof& r : gPoolRounds) {
1165 if (r.id != id)
1166 continue;
1167 r.forfeited[address] = reason;
1168 found = true;
1169 break;
1170 }
1171 if (!found)
1172 return;
1173 snap = SerializePoolRoundsLocked();
1174 }
1175 PublishPoolRounds(snap);
1176}
1177
1135// Which wallet transaction, if any, already paid this? 1178// Which wallet transaction, if any, already paid this?
1136// 1179//
1137// Two kinds of evidence, in order of quality. 1180// Two kinds of evidence, in order of quality.
@@ -1634,14 +1677,21 @@ static void RecordPayoutTxid(const PayingRecord& r, const std::string& txid)
1634 LogPrint("payout", "[payout] block %d fully settled\n", r.blockHeight); 1677 LogPrint("payout", "[payout] block %d fully settled\n", r.blockHeight);
1635} 1678}
1636 1679
1637// Write a recipient off. Only reached after the full retry budget against a 1680// Write a recipient off. Only reached after a full budget of failures that
1638// block that is still on the main chain with coin available to spend, i.e. a 1681// were NOT the wallet being locked or short of coin -- see the classification
1639// wallet fault rather than a maturity or reorg question. 1682// in PayoutThreadFn -- against a block still on the main chain.
1640// 1683//
1641// The amount is removed from the entry rather than marked paid, so the money 1684// The amount is removed from the entry rather than marked paid, so the money
1642// is not reported as sent. Once nothing is left owed the entry retires. 1685// is not reported as sent. Once nothing is left owed the entry retires. The
1643static void ForfeitPayoutRecipient(const PayingRecord& r) 1686// round proof is told separately, because dropping the obligation from the
1687// ledger while pool_rounds.json goes on publishing the address as owed with
1688// "txid": null leaves the pool's two records of the same round permanently
1689// disagreeing.
1690static void ForfeitPayoutRecipient(const PayingRecord& r, const std::string& reason)
1644{ 1691{
1692 LogPrint("payout", "[payout] writing off %s owed to %s for block %d: %s\n",
1693 FormatMoney(r.amount).c_str(), r.address.c_str(), r.blockHeight,
1694 reason.c_str());
1645 VersionedBlob blob; 1695 VersionedBlob blob;
1646 { 1696 {
1647 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex); 1697 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
@@ -1657,6 +1707,8 @@ static void ForfeitPayoutRecipient(const PayingRecord& r)
1657 blob = SerializePendingPayoutsLocked(); 1707 blob = SerializePendingPayoutsLocked();
1658 } 1708 }
1659 PublishPendingPayouts(blob); 1709 PublishPendingPayouts(blob);
1710 MarkPoolRoundForfeited(PayoutIdForBlock(r.blockHeight, r.blockHash),
1711 r.address, reason);
1660} 1712}
1661 1713
1662// Payout execution thread: works through gPaying, calls SendMoney. 1714// Payout execution thread: works through gPaying, calls SendMoney.
@@ -1766,17 +1818,51 @@ static void PayoutThreadFn(void*)
1766 // payment the pool fully intends to honour can hit GIVING UP 1818 // payment the pool fully intends to honour can hit GIVING UP
1767 // 24h later purely because the subsidy funding it had not 1819 // 24h later purely because the subsidy funding it had not
1768 // matured yet -- the miner is then silently never paid. 1820 // matured yet -- the miner is then silently never paid.
1821 //
1822 // The comparison includes the fee. It used to be against the
1823 // amount alone, so a balance anywhere between amount and
1824 // amount+fee sailed past this gate, failed in SendMoney, and was
1825 // counted as a send FAILURE rather than a wait -- 1440 of those is
1826 // 24 hours, and the recipient was erased with the money left in
1827 // the operator's wallet and nothing on disk saying it was owed.
1828 //
1829 // nTransactionFee is the part of the fee that can be known before
1830 // the transaction exists. The rest is GetMinFee(), a function of
1831 // the serialised size and therefore of how many inputs SelectCoins
1832 // ends up picking, which cannot be predicted from here -- so this
1833 // gate deliberately does NOT try to reserve it. Refusing to attempt
1834 // a payment the wallet could actually have made would be its own
1835 // way of not paying a miner. That residual is handled after the
1836 // fact, by classifying the failure below.
1769 int64 nSpendable = GetBalance(); 1837 int64 nSpendable = GetBalance();
1770 if (nSpendable < r.amount) { 1838 if (nSpendable < r.amount + nTransactionFee) {
1771 r.deferrals++; 1839 r.deferrals++;
1772 r.retryAfter = GetTime() + 60; 1840 r.retryAfter = GetTime() + 60;
1773 // Hourly, not every minute: a dry pool would otherwise 1841 // Hourly, not every minute: a dry pool would otherwise
1774 // write one line per payment per minute forever. 1842 // write one line per payment per minute forever.
1775 if (r.deferrals == 1 || r.deferrals % 60 == 0) 1843 if (r.deferrals == 1 || r.deferrals % 60 == 0)
1776 LogPrint("payout", "[payout] deferring %s to %s -- only %s" 1844 LogPrint("payout", "[payout] deferring %s to %s -- only %s"
1777 " spendable (waiting %d min so far)\n", 1845 " spendable, need %s with fee (waiting %d min so far)\n",
1846 FormatMoney(r.amount).c_str(), r.address.c_str(),
1847 FormatMoney(nSpendable).c_str(),
1848 FormatMoney(r.amount + nTransactionFee).c_str(),
1849 r.deferrals);
1850 ReschedulePayment(r);
1851 continue;
1852 }
1853
1854 // Likewise a locked wallet: it cannot sign, so every attempt fails
1855 // for a reason that has nothing to do with this payment being
1856 // payable. Left to the retry budget, an operator who locks the
1857 // wallet overnight comes back to erased debts.
1858 if (IsWalletLocked()) {
1859 r.deferrals++;
1860 r.retryAfter = GetTime() + 60;
1861 if (r.deferrals == 1 || r.deferrals % 60 == 0)
1862 LogPrint("payout", "[payout] deferring %s to %s -- wallet is"
1863 " locked (waiting %d min so far)\n",
1778 FormatMoney(r.amount).c_str(), r.address.c_str(), 1864 FormatMoney(r.amount).c_str(), r.address.c_str(),
1779 FormatMoney(nSpendable).c_str(), r.deferrals); 1865 r.deferrals);
1780 ReschedulePayment(r); 1866 ReschedulePayment(r);
1781 continue; 1867 continue;
1782 } 1868 }
@@ -1803,36 +1889,76 @@ static void PayoutThreadFn(void*)
1803 LogPrint("payout", "[payout] paid %s to %s (tx %s)\n", 1889 LogPrint("payout", "[payout] paid %s to %s (tx %s)\n",
1804 FormatMoney(r.amount).c_str(), r.address.c_str(), 1890 FormatMoney(r.amount).c_str(), r.address.c_str(),
1805 wtx.GetHash().ToString().substr(0, 12).c_str()); 1891 wtx.GetHash().ToString().substr(0, 12).c_str());
1806 } else if (r.retries < 1440) {
1807 // Transient failures (reorg, locked wallet) must not silently
1808 // burn the payment -- retry with backoff. Insufficient funds
1809 // never lands here; it defers above.
1810 r.retries++;
1811 r.retryAfter = GetTime() + 60;
1812 ReschedulePayment(r);
1813 LogPrint("payout", "[payout] FAILED to pay %s to %s"
1814 " (%s spendable at the time) -- retry %d/1440 in 60s\n",
1815 FormatMoney(r.amount).c_str(), r.address.c_str(),
1816 FormatMoney(nSpendable).c_str(), r.retries);
1817 } else { 1892 } else {
1818 // Reaching here means 1440 attempts that were NOT 1893 // Why did it fail? The answer decides whether this costs a
1819 // insufficient-funds waits (those defer instead) against a 1894 // retry, and the retry budget is what eventually erases a
1820 // block still on the main chain -- i.e. a real wallet fault, 1895 // miner's money -- so guessing here is how a pool keeps coin
1821 // not a maturity or reorg question. 1896 // it owes.
1822 LogPrint("payout", "[payout] GIVING UP on %s to %s after %d" 1897 //
1823 " retries -- block %d is still on-chain and %s was" 1898 // Re-read the conditions rather than trusting the ones checked
1824 " spendable, so this is a wallet fault, not maturity" 1899 // before the attempt: SendMoney takes cs_main and can block for
1825 " -- payment forfeited\n", 1900 // a long time, so the balance and the lock state may both have
1826 FormatMoney(r.amount).c_str(), r.address.c_str(), 1901 // moved underneath it.
1827 r.retries, r.blockHeight, 1902 //
1828 FormatMoney(nSpendable).c_str()); 1903 // Short of coin covers the size-dependent part of the fee that
1829 // Write the debt off in the ledger as well as dropping the 1904 // the gate above deliberately does not reserve: if the balance
1830 // in-flight record. Under the survives-until-fully-paid model 1905 // no longer covers amount + configured fee + one fee tier, the
1831 // the entry would otherwise never complete, and every later 1906 // failure is a funding shortfall, not a fault.
1832 // flush would explode it again -- a forfeit that silently 1907 int64 nAfter = GetBalance();
1833 // becomes an infinite retry. 1908 bool fLocked = IsWalletLocked();
1834 ForfeitPayoutRecipient(r); 1909 bool fUnfunded = nAfter < r.amount + nTransactionFee + CENT;
1835 ForgetPayment(r); 1910
1911 if (fLocked || fUnfunded) {
1912 // Conditions are temporarily wrong. Wait; do not spend the
1913 // budget that leads to a permanent write-off.
1914 r.deferrals++;
1915 r.retryAfter = GetTime() + 60;
1916 if (r.deferrals == 1 || r.deferrals % 60 == 0)
1917 LogPrint("payout", "[payout] deferring %s to %s -- send"
1918 " failed with %s (%s spendable, %s needed with"
1919 " fee headroom); waiting %d min so far\n",
1920 FormatMoney(r.amount).c_str(), r.address.c_str(),
1921 fLocked ? "the wallet locked" : "too little coin",
1922 FormatMoney(nAfter).c_str(),
1923 FormatMoney(r.amount + nTransactionFee + CENT).c_str(),
1924 r.deferrals);
1925 ReschedulePayment(r);
1926 } else if (r.retries < 1440) {
1927 // A genuine fault: funded, unlocked, block on the main
1928 // chain, and the wallet still would not build or accept
1929 // the transaction. Retry with backoff.
1930 r.retries++;
1931 r.retryAfter = GetTime() + 60;
1932 ReschedulePayment(r);
1933 LogPrint("payout", "[payout] FAILED to pay %s to %s"
1934 " (%s spendable, wallet unlocked) -- retry %d/1440"
1935 " in 60s\n",
1936 FormatMoney(r.amount).c_str(), r.address.c_str(),
1937 FormatMoney(nAfter).c_str(), r.retries);
1938 } else {
1939 // 1440 failures that were each individually impossible to
1940 // explain by maturity, funding, the wallet being locked, or
1941 // a reorg. That is the only state in which this payment can
1942 // be called impossible rather than merely blocked.
1943 const std::string reason = strprintf(
1944 "wallet refused this payment %d times while funded"
1945 " (%s spendable), unlocked, and block %d on the main"
1946 " chain", r.retries, FormatMoney(nAfter).c_str(),
1947 r.blockHeight);
1948 LogPrint("payout", "[payout] GIVING UP on %s to %s: %s"
1949 " -- payment forfeited\n",
1950 FormatMoney(r.amount).c_str(), r.address.c_str(),
1951 reason.c_str());
1952 // Write the debt off in the ledger as well as dropping the
1953 // in-flight record. Under the survives-until-fully-paid
1954 // model the entry would otherwise never complete, and every
1955 // later flush would explode it again -- a forfeit that
1956 // silently becomes an infinite retry. The round proof is
1957 // told too, so the public record does not keep claiming the
1958 // address is owed.
1959 ForfeitPayoutRecipient(r, reason);
1960 ForgetPayment(r);
1961 }
1836 } 1962 }
1837 } 1963 }
1838 1964
@@ -1942,10 +2068,19 @@ static void WritePoolStatusJsonLocked()
1942 size_t start = gPoolRounds.size() > 10 ? gPoolRounds.size() - 10 : 0; 2068 size_t start = gPoolRounds.size() > 10 ? gPoolRounds.size() - 10 : 0;
1943 for (size_t i = start; i < gPoolRounds.size(); i++) { 2069 for (size_t i = start; i < gPoolRounds.size(); i++) {
1944 const PoolRoundProof& r = gPoolRounds[i]; 2070 const PoolRoundProof& r = gPoolRounds[i];
2071 // A forfeited recipient is not "unpaid and pending" -- the ledger
2072 // has already dropped the obligation. Counting it as unpaid is
2073 // what made the round proof and the ledger disagree for good.
1945 int unpaid = 0; 2074 int unpaid = 0;
1946 for (const auto& kv : r.payouts) 2075 int forfeited = 0;
1947 if (r.paidTxids.count(kv.first) == 0) 2076 for (const auto& kv : r.payouts) {
2077 if (r.paidTxids.count(kv.first))
2078 continue;
2079 if (r.forfeited.count(kv.first))
2080 forfeited++;
2081 else
1948 unpaid++; 2082 unpaid++;
2083 }
1949 j["recentRounds"].push_back({ 2084 j["recentRounds"].push_back({
1950 {"id", r.id}, 2085 {"id", r.id},
1951 {"blockHeight", r.blockHeight}, 2086 {"blockHeight", r.blockHeight},
@@ -1954,6 +2089,7 @@ static void WritePoolStatusJsonLocked()
1954 {"totalShares", r.totalShares}, 2089 {"totalShares", r.totalShares},
1955 {"payouts", (int)r.payouts.size()}, 2090 {"payouts", (int)r.payouts.size()},
1956 {"unpaidPayouts", unpaid}, 2091 {"unpaidPayouts", unpaid},
2092 {"forfeitedPayouts", forfeited},
1957 {"coinbaseValueBtf", FormatMoney(r.coinbaseValue)}, 2093 {"coinbaseValueBtf", FormatMoney(r.coinbaseValue)},
1958 {"minerRewardBtf", FormatMoney(r.minerReward)} 2094 {"minerRewardBtf", FormatMoney(r.minerReward)}
1959 }); 2095 });