diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-08-23 07:43:20 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-08-23 07:43:20 +0000 |
| commit | 5892302449e1299f39812d0bb422c4184224b0f1 (patch) | |
| tree | c9d382474a3212b756757a08c8020ffb99495129 /src | |
| parent | c4754fb505be75b8eedee3ce230a8991762e18c5 (diff) | |
payout: do not write off a debt the wallet is only temporarily unable to pay
ForfeitPayoutRecipient permanently erases a miner's money: the amount leaves
the ledger, the operator keeps the coin, and nothing on disk records that it
was ever owed. It was reachable from two entirely ordinary conditions.
The deferral gate compared GetBalance() against r.amount, ignoring the fee.
A balance anywhere between amount and amount+fee therefore passed the gate,
failed inside SendMoney, and was counted as a send FAILURE -- a retry. At one
a minute, 1440 of those is 24 hours, and then the recipient is gone. A wallet
left locked overnight did exactly the same thing: every attempt fails because
nothing can be signed, and each failure spent budget.
Three changes.
The gate now requires amount + nTransactionFee, which is the part of the fee
knowable before the transaction exists. It deliberately does not try to
reserve GetMinFee(), which depends on the serialised size and so on how many
inputs SelectCoins happens to pick: refusing to attempt a payment the wallet
could actually have made is its own way of not paying a miner. A locked wallet
is now a deferral at the same gate, for the same reason maturity is.
Failures are classified after the fact instead of all counting as faults. The
balance and lock state are re-read after SendMoney returns -- it holds cs_main
and can block for a long time, so both may have moved -- and if the wallet is
locked, or the balance no longer covers amount + fee + one fee tier of
headroom, the attempt is recorded as a deferral. That headroom is where the
size-dependent part of the fee lands, so a fee shortfall is a wait rather than
a fault, which is what it is.
The retry budget therefore now only counts failures that happened while the
payment was funded, the wallet was unlocked, and the funding block was on the
main chain. Reaching 1440 of those means the wallet refuses a payment it has
every means to make, which is the only state in which the payment can be
called impossible rather than merely blocked. The give-up log says which
conditions held, rather than implying it.
Finally, the forfeit is reconciled with the public record. Dropping the amount
from the ledger while pool_rounds.json goes on advertising the address as owed
with "txid": null left the pool's two accounts of the same round permanently
disagreeing, with the operator holding the difference. PoolRoundProof gains a
per-address forfeited/forfeitReason, written by MarkPoolRoundForfeited when
the ledger writes the debt off, and pool_status.json counts those separately
from genuinely unpaid ones instead of reporting them as still pending.
On-disk: pool_rounds.json gains optional "forfeited" and "forfeitReason" keys
inside a payout object, and pool_status.json gains "forfeitedPayouts" per
round. Both are emitted only when a forfeit has actually happened, both are
read back with defaults, and files written by any earlier build load unchanged.
Diffstat (limited to 'src')
| -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 | }); |