diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/rpc.cpp | 173 |
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. | ||
| 602 | static std::set<std::string> gRefusedFiles; | ||
| 603 | static std::mutex gRefusedFilesMutex; | ||
| 604 | |||
| 605 | static 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. | ||
| 616 | static 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 |