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.cpp173
1 files changed, 165 insertions, 8 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp
index b0436ba..7d7cc8f 100644
--- a/src/rpc.cpp
+++ b/src/rpc.cpp
@@ -583,6 +583,55 @@ static bool WriteFileAtomic(const std::string& path, const std::string& data,
583 return true; 583 return true;
584} 584}
585 585
586// Paths whose loader threw part-way through, and which must therefore never be
587// written again by this process.
588//
589// All three loaders push entries into the live structure INSIDE the try, so an
590// exception half way down a file leaves a PARTIAL ledger in memory -- the
591// entries before the bad one, and nothing after it. The first publish after
592// that then writes the partial state over the file, and the only record of
593// what was owed is gone. ReconcilePayouts publishes unconditionally at
594// startup, so "the first publish" is a few milliseconds later, before an
595// operator could possibly intervene. a8e2f58 named this failure mode in its
596// own commit message and then only fixed the write side.
597//
598// The file is renamed out of the way instead of being left in place, so the
599// evidence survives even if the operator restarts into the same fault, and so
600// a node that comes up owing nobody anything is visibly missing its ledger
601// rather than quietly appearing to have none.
602static std::set<std::string> gRefusedFiles;
603static std::mutex gRefusedFilesMutex;
604
605static bool FileIsRefused(const std::string& path)
606{
607 std::lock_guard<std::mutex> lk(gRefusedFilesMutex);
608 return gRefusedFiles.count(path) > 0;
609}
610
611// Quarantine `path` and refuse to write it for the rest of the process.
612//
613// Prints unconditionally: an operator who has just lost a payout ledger must
614// be told at whatever log settings they were already running with, and the
615// name it was moved to is the only way back to the data.
616static void RefuseFileAfterLoadFailure(const std::string& path, const char* what,
617 const char* why)
618{
619 {
620 std::lock_guard<std::mutex> lk(gRefusedFilesMutex);
621 gRefusedFiles.insert(path);
622 }
623 std::string bad = strprintf("%s.bad.%lld", path.c_str(), (long long)GetTime());
624 bool moved = (rename(path.c_str(), bad.c_str()) == 0);
625 printf("[payout] ERROR: %s (%s) did not parse: %s\n"
626 "[payout] ERROR: what loaded before the error is a PARTIAL picture,"
627 " so this file will not be written again by this process --"
628 " restart once the cause is fixed\n"
629 "[payout] ERROR: the file %s\n",
630 path.c_str(), what, why,
631 moved ? strprintf("has been kept as %s", bad.c_str()).c_str()
632 : "could NOT be renamed out of the way -- copy it now");
633}
634
586// Publish a versioned image of some in-memory state to `path`. 635// Publish a versioned image of some in-memory state to `path`.
587// 636//
588// `fileMutex` serialises writers of `path` and guards `publishedSeq`. A blob 637// `fileMutex` serialises writers of `path` and guards `publishedSeq`. A blob
@@ -595,6 +644,11 @@ static void PublishVersioned(const std::string& path, const VersionedBlob& v,
595 std::mutex& fileMutex, uint64& publishedSeq, 644 std::mutex& fileMutex, uint64& publishedSeq,
596 const char* what) 645 const char* what)
597{ 646{
647 // A file whose loader threw is not a file this process knows the contents
648 // of. Writing it would replace a full record of what was owed with
649 // whatever happened to parse before the error.
650 if (FileIsRefused(path))
651 return;
598 std::lock_guard<std::mutex> lk(fileMutex); 652 std::lock_guard<std::mutex> lk(fileMutex);
599 if (v.seq <= publishedSeq) 653 if (v.seq <= publishedSeq)
600 return; 654 return;
@@ -760,8 +814,7 @@ static void LoadPendingPayouts()
760 LogPrint("payout", "[payout] loaded %zu pending payout(s) from disk\n", 814 LogPrint("payout", "[payout] loaded %zu pending payout(s) from disk\n",
761 gPendingPayouts.size()); 815 gPendingPayouts.size());
762 } catch (const std::exception& e) { 816 } catch (const std::exception& e) {
763 LogPrint("payout", "[payout] WARNING: failed to parse %s: %s\n", 817 RefuseFileAfterLoadFailure(path, "pending payout ledger", e.what());
764 path.c_str(), e.what());
765 } 818 }
766} 819}
767 820
@@ -948,8 +1001,7 @@ static void LoadPaying()
948 LogPrint("payout", "[payout] loaded %zu in-flight payment(s) from disk\n", 1001 LogPrint("payout", "[payout] loaded %zu in-flight payment(s) from disk\n",
949 gPaying.size()); 1002 gPaying.size());
950 } catch (const std::exception& e) { 1003 } catch (const std::exception& e) {
951 LogPrint("payout", "[payout] WARNING: failed to parse %s: %s\n", 1004 RefuseFileAfterLoadFailure(path, "in-flight payment list", e.what());
952 path.c_str(), e.what());
953 } 1005 }
954} 1006}
955 1007
@@ -1095,8 +1147,7 @@ static void LoadPoolRounds()
1095 LogPrint("payout", "[payout] loaded %zu pool round proof(s) from disk\n", 1147 LogPrint("payout", "[payout] loaded %zu pool round proof(s) from disk\n",
1096 gPoolRounds.size()); 1148 gPoolRounds.size());
1097 } catch (const std::exception& e) { 1149 } catch (const std::exception& e) {
1098 LogPrint("payout", "[payout] WARNING: failed to parse %s: %s\n", 1150 RefuseFileAfterLoadFailure(path, "pool round proofs", e.what());
1099 path.c_str(), e.what());
1100 } 1151 }
1101} 1152}
1102 1153
@@ -3021,6 +3072,12 @@ static void PayoutTestResetState()
3021 std::lock_guard<std::mutex> lk(gClaimedTxidMutex); 3072 std::lock_guard<std::mutex> lk(gClaimedTxidMutex);
3022 gLegacyClaimedTxids.clear(); 3073 gLegacyClaimedTxids.clear();
3023 } 3074 }
3075 {
3076 // A quarantine is meant to last the life of the process; each section
3077 // here is a fresh process as far as the code under test is concerned.
3078 std::lock_guard<std::mutex> lk(gRefusedFilesMutex);
3079 gRefusedFiles.clear();
3080 }
3024} 3081}
3025 3082
3026// A BTF address whose hash160 is entirely under the test's control, so the 3083// A BTF address whose hash160 is entirely under the test's control, so the
@@ -3785,7 +3842,107 @@ int RunPayoutLedgerSelfTest()
3785 strPoolRoundsFile = savedRoundsFile; 3842 strPoolRoundsFile = savedRoundsFile;
3786 } 3843 }
3787 3844
3788 // -- 11. A send that failed for want of coin is not a fault -------------- 3845 // -- 11. A ledger that failed to load is never written back --------------
3846 //
3847 // The loaders push entries into the live vector inside the try, so an
3848 // exception part way down a file leaves a PARTIAL ledger in memory: the
3849 // entries before the bad one and nothing after it. Publishing that -- and
3850 // ReconcilePayouts publishes unconditionally, milliseconds into startup --
3851 // writes the partial state over the file and destroys the only record of
3852 // what was owed.
3853 {
3854 PayoutTestResetState();
3855 remove(ledgerPath.c_str());
3856
3857 // Two entries, the second with a matureAtHeight that is not a number.
3858 // The first loads; the second throws where it is read.
3859 const std::string corrupt = strprintf(
3860 "[{\"amounts\":{\"%s\":100000000},\"blockHash\":\"%s\","
3861 "\"blockHeight\":10,\"matureAtHeight\":10},"
3862 "{\"amounts\":{\"%s\":900000000},\"blockHash\":\"%s\","
3863 "\"blockHeight\":20,\"matureAtHeight\":\"twenty\"}]",
3864 addrA.c_str(), uint256(0).ToString().c_str(),
3865 addrB.c_str(), uint256(0).ToString().c_str());
3866 check(PayoutTestWriteFile(ledgerPath, corrupt),
3867 "wrote a ledger whose second entry does not parse");
3868
3869 const int64 t0 = GetTime();
3870 LoadPendingPayouts();
3871 const int64 t1 = GetTime();
3872
3873 size_t loaded = 0;
3874 {
3875 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
3876 loaded = gPendingPayouts.size();
3877 }
3878 check(loaded == 1,
3879 "a mid-file parse error does leave a partial ledger in memory --"
3880 " which is the reason for everything below");
3881
3882 std::string gone;
3883 check(!PayoutTestReadFile(ledgerPath, gone),
3884 "the file that did not parse is moved out of the way");
3885
3886 // The rename stamps GetTime(); it was taken between t0 and t1.
3887 std::string kept, keptPath;
3888 for (int64 t = t0; t <= t1 && keptPath.empty(); t++) {
3889 const std::string candidate =
3890 strprintf("%s.bad.%lld", ledgerPath.c_str(), (long long)t);
3891 if (PayoutTestReadFile(candidate, kept))
3892 keptPath = candidate;
3893 }
3894 check(!keptPath.empty() && kept == corrupt,
3895 "it is kept, byte for byte, as <path>.bad.<timestamp>");
3896
3897 // The publish that used to destroy it.
3898 VersionedBlob partial;
3899 {
3900 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
3901 partial = SerializePendingPayoutsLocked();
3902 }
3903 PublishPendingPayouts(partial);
3904 check(!PayoutTestReadFile(ledgerPath, gone),
3905 "and the partial ledger is not published over it");
3906
3907 // The refusal is per path and lasts the process: a second attempt,
3908 // with more state, is refused too.
3909 {
3910 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
3911 PendingPayout pp;
3912 pp.blockHeight = 30; pp.blockHash = 0; pp.matureAtHeight = 30;
3913 pp.amounts[addrA] = 5 * COIN;
3914 gPendingPayouts.push_back(pp);
3915 partial = SerializePendingPayoutsLocked();
3916 }
3917 PublishPendingPayouts(partial);
3918 check(!PayoutTestReadFile(ledgerPath, gone),
3919 "the refusal is not a one-shot -- the path stays unwritable");
3920
3921 if (!keptPath.empty())
3922 remove(keptPath.c_str());
3923 PayoutTestResetState();
3924 remove(ledgerPath.c_str());
3925
3926 // And a file that parses is not quarantined, so the ordinary path is
3927 // still the ordinary path.
3928 check(PayoutTestWriteFile(ledgerPath, "[]"),
3929 "wrote a ledger that parses");
3930 LoadPendingPayouts();
3931 {
3932 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
3933 PendingPayout pp;
3934 pp.blockHeight = 40; pp.blockHash = 0; pp.matureAtHeight = 40;
3935 pp.amounts[addrA] = 2 * COIN;
3936 gPendingPayouts.push_back(pp);
3937 partial = SerializePendingPayoutsLocked();
3938 }
3939 PublishPendingPayouts(partial);
3940 std::string written;
3941 check(PayoutTestReadFile(ledgerPath, written) && written == partial.blob,
3942 "a ledger that loaded cleanly is still published normally");
3943 }
3944
3945 // -- 12. A send that failed for want of coin is not a fault --------------
3789 // 3946 //
3790 // The defect this replaces: the classifier decided "was this a funding 3947 // The defect this replaces: the classifier decided "was this a funding
3791 // problem?" by re-reading GetBalance() and allowing amount + 3948 // problem?" by re-reading GetBalance() and allowing amount +
@@ -3919,7 +4076,7 @@ int RunPayoutLedgerSelfTest()
3919 mapWallet.clear(); 4076 mapWallet.clear();
3920 } 4077 }
3921 4078
3922 // -- 12. No payout mutex is held across the status-file write ------------ 4079 // -- 13. No payout mutex is held across the status-file write ------------
3923 // 4080 //
3924 // RefreshStats runs on the event loop -- the thread that owns every 4081 // RefreshStats runs on the event loop -- the thread that owns every
3925 // miner's socket -- and used to hold gStatsMutex and gPayoutLedgerMutex at 4082 // miner's socket -- and used to hold gStatsMutex and gPayoutLedgerMutex at