diff options
Diffstat (limited to 'src/rpc.cpp')
| -rw-r--r-- | src/rpc.cpp | 214 |
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. | ||
| 1156 | static 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 |
| 1643 | static 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. | ||
| 1690 | static 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 | }); |