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.cpp671
1 files changed, 671 insertions, 0 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp
index 45130ff..cebc349 100644
--- a/src/rpc.cpp
+++ b/src/rpc.cpp
@@ -38,6 +38,7 @@
38 38
39#include <atomic> 39#include <atomic>
40#include <mutex> 40#include <mutex>
41#include <thread>
41#include <map> 42#include <map>
42#include <set> 43#include <set>
43#include <deque> 44#include <deque>
@@ -2749,6 +2750,676 @@ static bool HandleLine(Miner* m, const std::string& rawLine,
2749 return true; 2750 return true;
2750} 2751}
2751 2752
2753// ---------------------------------------------------------------------------
2754// payout-ledger self-test
2755//
2756// Regression cover for the payout defects fixed in this series. Everything
2757// here runs against a scratch data directory and an in-memory wallet map; it
2758// never opens wallet.dat, never touches a real datadir, and never sends
2759// anything.
2760// ---------------------------------------------------------------------------
2761
2762static bool PayoutTestReadFile(const std::string& path, std::string& out)
2763{
2764 FILE* f = fopen(path.c_str(), "r");
2765 if (!f) return false;
2766 fseek(f, 0, SEEK_END);
2767 long sz = ftell(f);
2768 fseek(f, 0, SEEK_SET);
2769 out.assign((size_t)(sz > 0 ? sz : 0), '\0');
2770 if (sz > 0 && fread(&out[0], 1, (size_t)sz, f) != (size_t)sz) {
2771 fclose(f);
2772 return false;
2773 }
2774 fclose(f);
2775 return true;
2776}
2777
2778static bool PayoutTestWriteFile(const std::string& path, const std::string& data)
2779{
2780 FILE* f = fopen(path.c_str(), "w");
2781 if (!f) return false;
2782 bool ok = data.empty() || fwrite(data.c_str(), 1, data.size(), f) == data.size();
2783 return fclose(f) == 0 && ok;
2784}
2785
2786// Wipe every piece of payout state this test touches, so each section starts
2787// from the same place and a "restart" can be simulated by reloading from disk.
2788static void PayoutTestResetState()
2789{
2790 {
2791 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
2792 gPendingPayouts.clear();
2793 }
2794 {
2795 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
2796 gPaying.clear();
2797 gQueuedPayouts.clear();
2798 }
2799 {
2800 std::lock_guard<std::mutex> lk(gPoolRoundsMutex);
2801 gPoolRounds.clear();
2802 }
2803 {
2804 std::lock_guard<std::mutex> lk(gClaimedTxidMutex);
2805 gLegacyClaimedTxids.clear();
2806 }
2807}
2808
2809// A BTF address whose hash160 is entirely under the test's control, so the
2810// output script the probe compares against can be built without a wallet.
2811static std::string PayoutTestAddress(unsigned char seed)
2812{
2813 uint160 h = 0;
2814 for (int i = 0; i < 20; i++)
2815 ((unsigned char*)&h)[i] = (unsigned char)(seed + i);
2816 return Hash160ToAddress(h);
2817}
2818
2819static CScript PayoutTestScriptFor(const std::string& address)
2820{
2821 uint160 h160;
2822 CScript sc;
2823 if (AddressToHash160(address, h160))
2824 sc << OP_DUP << OP_HASH160 << h160 << OP_EQUALVERIFY << OP_CHECKSIG;
2825 return sc;
2826}
2827
2828// Put a transaction in the wallet map that looks exactly like one this pool
2829// sent. `nonce` only exists to keep the hashes distinct.
2830static std::string PayoutTestAddWalletTx(const std::string& comment,
2831 const std::string& address,
2832 int64 amount,
2833 int nonce,
2834 const std::string& structuredId)
2835{
2836 CWalletTx wtx;
2837 wtx.vin.resize(1);
2838 wtx.vin[0].prevout.SetNull();
2839 wtx.vin[0].scriptSig << nonce;
2840 wtx.vout.resize(1);
2841 wtx.vout[0].nValue = amount;
2842 wtx.vout[0].scriptPubKey = PayoutTestScriptFor(address);
2843 if (!comment.empty())
2844 wtx.mapValue["comment"] = comment;
2845 if (!structuredId.empty()) {
2846 wtx.mapValue["pool_payout_id"] = structuredId;
2847 wtx.mapValue["pool_payout_address"] = address;
2848 wtx.mapValue["pool_payout_amount"] = strprintf("%lld", amount);
2849 }
2850 uint256 hash = wtx.GetHash();
2851 CRITICAL_BLOCK(cs_mapWallet)
2852 mapWallet[hash] = wtx;
2853 return hash.GetHex();
2854}
2855
2856int RunPayoutLedgerSelfTest()
2857{
2858 int nFail = 0;
2859 auto check = [&](bool ok, const char* msg) {
2860 fprintf(stdout, " %s %s\n", ok ? "ok" : "FAIL", msg);
2861 if (!ok) nFail++;
2862 };
2863
2864 fprintf(stdout, "payout-ledger self-test\n");
2865
2866 const std::string savedDataDir = strSetDataDir;
2867 const int savedBestHeight = nBestHeight;
2868
2869 // Scratch data directory in the current working directory. GetAppDir()
2870 // follows strSetDataDir, so this is what PayoutFilePath() and
2871 // PayingFilePath() resolve against for the rest of the test.
2872 std::string dir = strprintf("payout_selftest_%d", (int)GetTime());
2873#ifdef _WIN32
2874 _mkdir(dir.c_str());
2875#else
2876 mkdir(dir.c_str(), 0700);
2877#endif
2878 strSetDataDir = dir;
2879 fprintf(stdout, " scratch datadir: %s\n", dir.c_str());
2880
2881 const std::string ledgerPath = PayoutFilePath();
2882 const std::string payingPath = PayingFilePath();
2883 const std::string addrA = PayoutTestAddress(0x11);
2884 const std::string addrB = PayoutTestAddress(0x55);
2885
2886 // -- 1. A restart must not duplicate an in-flight record -----------------
2887 //
2888 // The ledger entry is grandfathered (zero hash) on purpose: it keeps
2889 // PayoutBlockStillOnChain out of the picture, which needs a block index,
2890 // and it exercises the height fallback at the same time.
2891 {
2892 PayoutTestResetState();
2893 remove(ledgerPath.c_str());
2894 remove(payingPath.c_str());
2895
2896 PendingPayout pp;
2897 pp.blockHeight = 4242;
2898 pp.blockHash = 0;
2899 pp.matureAtHeight = 4242;
2900 pp.amounts[addrA] = 10 * COIN;
2901 pp.amounts[addrB] = 20 * COIN;
2902 VersionedBlob snap;
2903 {
2904 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
2905 gPendingPayouts.push_back(pp);
2906 snap = SerializePendingPayoutsLocked();
2907 }
2908 PublishPendingPayouts(snap);
2909
2910 nBestHeight = 5000;
2911 FlushMaturePayouts();
2912 size_t afterFirstFlush = 0;
2913 {
2914 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
2915 afterFirstFlush = gPaying.size();
2916 }
2917 check(afterFirstFlush == 2,
2918 "a matured round explodes into one record per unpaid recipient");
2919
2920 // Restart: drop everything in memory and come back from the files.
2921 for (int restart = 1; restart <= 2; restart++) {
2922 PayoutTestResetState();
2923 LoadPendingPayouts();
2924 LoadPaying();
2925 ReconcilePayouts();
2926
2927 size_t queued = 0;
2928 {
2929 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
2930 queued = gQueuedPayouts.size();
2931 }
2932 if (restart == 1)
2933 check(queued == 1,
2934 "startup rebuilds the exploded-round set from the restored"
2935 " in-flight list");
2936
2937 FlushMaturePayouts();
2938 size_t n = 0;
2939 {
2940 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
2941 n = gPaying.size();
2942 }
2943 check(n == 2, restart == 1
2944 ? "a restart does not duplicate an in-flight record"
2945 : "a second restart does not duplicate one either");
2946 }
2947
2948 // The other half of the guard: even with the round-level dedupe set
2949 // deliberately emptied, the per-recipient check against gPaying must
2950 // stop a second standing order for the same money.
2951 {
2952 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
2953 gQueuedPayouts.clear();
2954 }
2955 FlushMaturePayouts();
2956 size_t n = 0;
2957 {
2958 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
2959 n = gPaying.size();
2960 }
2961 check(n == 2,
2962 "the flush refuses a second record for a (round, address) already"
2963 " in flight even with the round set lost");
2964 }
2965
2966 // -- 2. Duplicates already on disk are collapsed as they are read --------
2967 {
2968 PayoutTestResetState();
2969 nlohmann::json arr = nlohmann::json::array();
2970 for (int i = 0; i < 2; i++) {
2971 nlohmann::json o;
2972 o["blockHeight"] = 4242;
2973 o["blockHash"] = uint256(0).ToString();
2974 o["matureAtHeight"] = 4242;
2975 o["address"] = addrA;
2976 o["amount"] = (int64)(10 * COIN);
2977 o["retries"] = i == 0 ? 7 : 0;
2978 o["deferrals"] = i == 0 ? 0 : 3;
2979 arr.push_back(o);
2980 }
2981 check(PayoutTestWriteFile(payingPath, arr.dump(2)),
2982 "wrote a paying file containing the duplicate the old build made");
2983 LoadPaying();
2984 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
2985 check(gPaying.size() == 1,
2986 "loading collapses a duplicate in-flight record from disk");
2987 check(gPaying.size() == 1 && gPaying[0].retries == 7 && gPaying[0].deferrals == 3,
2988 "collapsing merges the attempt counts upwards, not resetting them");
2989 }
2990
2991 // -- 3. State updates address a slot, not a value -----------------------
2992 //
2993 // Build the exact pathology the old code produced -- two records equal in
2994 // every field -- and prove the update lands on the one asked for. Under
2995 // value matching the first record absorbed everything and the second was
2996 // untouchable.
2997 {
2998 PayoutTestResetState();
2999 PayingRecord a;
3000 a.blockHeight = 4242; a.blockHash = 0; a.matureAtHeight = 4242;
3001 a.address = addrA; a.amount = 10 * COIN;
3002 PayingRecord b = a;
3003 {
3004 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
3005 a.id = ++gPayingNextId;
3006 b.id = ++gPayingNextId;
3007 gPaying.push_back(a);
3008 gPaying.push_back(b);
3009 }
3010 PayingRecord upd = b;
3011 upd.retries = 5;
3012 upd.retryAfter = 1234567;
3013 ReschedulePayment(upd);
3014 {
3015 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
3016 check(gPaying.size() == 2 &&
3017 gPaying[0].retries == 0 && gPaying[1].retries == 5,
3018 "rescheduling updates the record it was given, not the first"
3019 " one that looks like it");
3020 }
3021 ForgetPayment(a);
3022 {
3023 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
3024 check(gPaying.size() == 1 && gPaying[0].id == b.id,
3025 "forgetting erases the record it was given, not the first"
3026 " one that looks like it");
3027 }
3028 }
3029
3030 // -- 4. retryAfter survives a restart ------------------------------------
3031 {
3032 PayoutTestResetState();
3033 remove(payingPath.c_str());
3034 PayingRecord r;
3035 r.blockHeight = 77; r.blockHash = 0; r.matureAtHeight = 77;
3036 r.address = addrA; r.amount = 3 * COIN;
3037 r.retryAfter = 1700000000LL;
3038 r.deferrals = 9;
3039 VersionedBlob snap;
3040 {
3041 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
3042 r.id = ++gPayingNextId;
3043 gPaying.push_back(r);
3044 snap = SerializePayingLocked();
3045 }
3046 PublishPaying(snap);
3047 PayoutTestResetState();
3048 LoadPaying();
3049 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
3050 check(gPaying.size() == 1 && gPaying[0].retryAfter == 1700000000LL &&
3051 gPaying[0].deferrals == 9,
3052 "the retry clock that is actually used survives a restart");
3053 }
3054
3055 // -- 5. The ledger file cannot be published from a stale snapshot --------
3056 {
3057 PayoutTestResetState();
3058 remove(ledgerPath.c_str());
3059
3060 PendingPayout one;
3061 one.blockHeight = 100; one.blockHash = 0; one.matureAtHeight = 100;
3062 one.amounts[addrA] = 1 * COIN;
3063 VersionedBlob older, newer;
3064 {
3065 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
3066 gPendingPayouts.push_back(one);
3067 older = SerializePendingPayoutsLocked();
3068 PendingPayout two;
3069 two.blockHeight = 200; two.blockHash = 0; two.matureAtHeight = 200;
3070 two.amounts[addrB] = 2 * COIN;
3071 gPendingPayouts.push_back(two);
3072 newer = SerializePendingPayoutsLocked();
3073 }
3074 check(newer.seq > older.seq,
3075 "a later serialisation carries a strictly higher version");
3076
3077 // The order the race produced: the payout thread's older image
3078 // arriving after the event loop's newer one.
3079 PublishPendingPayouts(newer);
3080 PublishPendingPayouts(older);
3081 std::string onDisk;
3082 check(PayoutTestReadFile(ledgerPath, onDisk) && onDisk == newer.blob,
3083 "an older snapshot cannot overwrite a newer published ledger");
3084
3085 // And the ordinary direction still works.
3086 VersionedBlob newest;
3087 {
3088 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
3089 gPendingPayouts.clear();
3090 newest = SerializePendingPayoutsLocked();
3091 }
3092 PublishPendingPayouts(newest);
3093 check(PayoutTestReadFile(ledgerPath, onDisk) && onDisk == newest.blob,
3094 "a newer snapshot does replace the published ledger");
3095 }
3096
3097 // -- 6. Concurrent publishers cannot interleave a file -------------------
3098 //
3099 // Two threads renaming the same "<path>.tmp" over the same target was the
3100 // original defect. The images are made big so a torn write shows up as
3101 // unparseable JSON rather than by luck.
3102 {
3103 const std::string racePath = dir + "/publish_race_selftest.json";
3104 std::mutex raceMutex;
3105 uint64 racePublished = 0;
3106
3107 std::vector<VersionedBlob> images;
3108 for (int i = 0; i < 64; i++) {
3109 nlohmann::json arr = nlohmann::json::array();
3110 for (int k = 0; k < 200; k++) {
3111 nlohmann::json o;
3112 o["image"] = i;
3113 o["entry"] = k;
3114 o["address"] = addrA;
3115 o["amount"] = (int64)(k + 1) * COIN;
3116 arr.push_back(o);
3117 }
3118 VersionedBlob v;
3119 v.blob = arr.dump(2);
3120 v.seq = (uint64)i + 1;
3121 images.push_back(v);
3122 }
3123
3124 std::vector<std::thread> threads;
3125 for (int t = 0; t < 4; t++) {
3126 threads.push_back(std::thread([&, t]() {
3127 for (size_t i = 0; i < images.size(); i++) {
3128 size_t idx = (i * 7 + (size_t)t * 13) % images.size();
3129 PublishVersioned(racePath, images[idx], raceMutex,
3130 racePublished, "publish race selftest");
3131 }
3132 }));
3133 }
3134 for (std::thread& th : threads)
3135 th.join();
3136
3137 std::string onDisk;
3138 bool read = PayoutTestReadFile(racePath, onDisk);
3139 bool parses = false;
3140 if (read) {
3141 try { parses = nlohmann::json::parse(onDisk).is_array(); }
3142 catch (...) { parses = false; }
3143 }
3144 check(read && parses,
3145 "four threads publishing the same file never leave it unparseable");
3146 check(read && onDisk == images.back().blob,
3147 "the published file settles on the highest version, whatever the"
3148 " completion order");
3149 remove(racePath.c_str());
3150 remove((racePath + ".tmp").c_str());
3151 }
3152
3153 // -- 7. Grandfathered zero-hash rounds do not answer for each other ------
3154 {
3155 uint256 h1 = 0, h2 = 0;
3156 h1.SetHex("00000000000000000000000000000000000000000000000000000000000000aa");
3157 h2.SetHex("00000000000000000000000000000000000000000000000000000000000000bb");
3158 check(!SamePayoutRound(100, 0, 200, 0),
3159 "two zero-hash entries at different heights are different rounds");
3160 check(SamePayoutRound(100, 0, 100, 0),
3161 "two zero-hash entries at the same height are the same round");
3162 check(SamePayoutRound(100, h1, 200, h1),
3163 "a real hash identifies the round on its own");
3164 check(!SamePayoutRound(100, h1, 100, h2),
3165 "different hashes at one height are different rounds");
3166 check(!SamePayoutRound(100, h1, 100, 0),
3167 "a grandfathered entry never matches a hash-keyed one");
3168 check(!PayoutRoundIdentifiable(0, 0) && PayoutRoundIdentifiable(0, h1) &&
3169 PayoutRoundIdentifiable(7, 0),
3170 "an entry with neither hash nor height is not adjudicable");
3171
3172 // End to end: recording a payment against one grandfathered round must
3173 // not settle the other's debt.
3174 PayoutTestResetState();
3175 remove(ledgerPath.c_str());
3176 {
3177 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
3178 PendingPayout first;
3179 first.blockHeight = 100; first.blockHash = 0; first.matureAtHeight = 100;
3180 first.amounts[addrA] = 1 * COIN;
3181 PendingPayout second;
3182 second.blockHeight = 200; second.blockHash = 0; second.matureAtHeight = 200;
3183 second.amounts[addrA] = 1 * COIN;
3184 gPendingPayouts.push_back(first);
3185 gPendingPayouts.push_back(second);
3186 }
3187 PayingRecord r;
3188 r.blockHeight = 200; r.blockHash = 0; r.matureAtHeight = 200;
3189 r.address = addrA; r.amount = 1 * COIN;
3190 RecordPayoutTxid(r, "txid-for-the-second-round");
3191 {
3192 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
3193 // The second round was fully paid and retired; the first is
3194 // untouched and still owed.
3195 check(gPendingPayouts.size() == 1 &&
3196 gPendingPayouts[0].blockHeight == 100 &&
3197 gPendingPayouts[0].paidTxids.empty(),
3198 "a txid recorded against one zero-hash round leaves the other"
3199 " still owed");
3200 }
3201 }
3202
3203 // -- 8. A payment with only a legacy comment is recognised as paid -------
3204 {
3205 PayoutTestResetState();
3206 const int height = 8080;
3207 const int64 amount = 7 * COIN;
3208 const std::string id = PayoutIdForBlock(height, 0);
3209 const std::string comment = PayoutComment(height);
3210 std::set<std::string> none;
3211 std::string found;
3212
3213 check(FindRecordedPoolPayout(id, height, addrA, amount, none, found)
3214 == PAYOUT_PROBE_NONE,
3215 "an empty wallet is not evidence that anything was paid");
3216
3217 const std::string legacyTxid =
3218 PayoutTestAddWalletTx(comment, addrA, amount, 1, "");
3219 check(FindRecordedPoolPayout(id, height, addrA, amount, none, found)
3220 == PAYOUT_PROBE_LEGACY && found == legacyTxid,
3221 "a payment carrying only a pre-upgrade comment is recognised as"
3222 " already paid");
3223
3224 check(FindRecordedPoolPayout(id, height, addrB, amount, none, found)
3225 == PAYOUT_PROBE_NONE,
3226 "the legacy match requires the output to pay THIS recipient");
3227 check(FindRecordedPoolPayout(id, height, addrA, amount + COIN, none, found)
3228 == PAYOUT_PROBE_NONE,
3229 "the legacy match requires the output to pay exactly this amount");
3230 check(FindRecordedPoolPayout(id, height + 1, addrA, amount, none, found)
3231 == PAYOUT_PROBE_NONE,
3232 "the legacy match requires the comment for this block height");
3233
3234 std::set<std::string> spent;
3235 spent.insert(legacyTxid);
3236 check(FindRecordedPoolPayout(id, height, addrA, amount, spent, found)
3237 == PAYOUT_PROBE_NONE,
3238 "one legacy transaction cannot settle a second debt");
3239
3240 // A transaction written by THIS build carries pool_payout_id, and must
3241 // never be offered as legacy evidence for a different round -- that is
3242 // exactly what the structured metadata exists to prevent.
3243 const std::string otherId = PayoutIdForBlock(height, 0) + "-other";
3244 PayoutTestAddWalletTx(comment, addrB, amount, 2, otherId);
3245 check(FindRecordedPoolPayout(id, height, addrB, amount, none, found)
3246 == PAYOUT_PROBE_NONE,
3247 "another round's structured payment is not legacy evidence for"
3248 " this one");
3249
3250 // The structured path still wins where it applies.
3251 const std::string structuredTxid =
3252 PayoutTestAddWalletTx(comment, addrB, amount, 3, id);
3253 check(FindRecordedPoolPayout(id, height, addrB, amount, none, found)
3254 == PAYOUT_PROBE_STRUCTURED && found == structuredTxid,
3255 "structured payout metadata is matched in preference to a comment");
3256 }
3257
3258 // -- 9. On-disk compatibility with the live production ledger ------------
3259 {
3260 PayoutTestResetState();
3261 // Exactly the shape of the file on the pool node: amounts, blockHash,
3262 // blockHeight, matureAtHeight, and nothing else.
3263 const char* live =
3264 "[{\"amounts\":{\"%s\":1234500000},"
3265 "\"blockHash\":\"00000000000000000000000000000000000000000000000000000000000000cd\","
3266 "\"blockHeight\":1000,"
3267 "\"matureAtHeight\":1120}]";
3268 check(PayoutTestWriteFile(ledgerPath, strprintf(live, addrA.c_str())),
3269 "wrote a ledger file in the live production shape");
3270 LoadPendingPayouts();
3271 {
3272 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
3273 check(gPendingPayouts.size() == 1 &&
3274 gPendingPayouts[0].blockHeight == 1000 &&
3275 gPendingPayouts[0].matureAtHeight == 1120 &&
3276 gPendingPayouts[0].blockHash != 0 &&
3277 gPendingPayouts[0].amounts.size() == 1 &&
3278 gPendingPayouts[0].amounts[addrA] == 1234500000LL,
3279 "the live production pending_payouts.json still loads");
3280 }
3281
3282 // And the shape the merged build writes, retryAfter included, which
3283 // this build no longer writes but must still read past.
3284 PayoutTestResetState();
3285 const char* merged =
3286 "[{\"amounts\":{\"%s\":100000000},"
3287 "\"blockHash\":\"00000000000000000000000000000000000000000000000000000000000000ef\","
3288 "\"blockHeight\":2000,\"matureAtHeight\":2120,\"retryAfter\":0,"
3289 "\"paidTxids\":{\"%s\":\"abc123\"}}]";
3290 check(PayoutTestWriteFile(ledgerPath,
3291 strprintf(merged, addrA.c_str(), addrA.c_str())),
3292 "wrote a ledger file in the merged build's shape");
3293 LoadPendingPayouts();
3294 {
3295 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
3296 check(gPendingPayouts.size() == 1 &&
3297 gPendingPayouts[0].blockHeight == 2000 &&
3298 gPendingPayouts[0].paidTxids.size() == 1 &&
3299 gPendingPayouts[0].paidTxids[addrA] == "abc123",
3300 "a ledger file carrying retryAfter and paidTxids still loads");
3301 }
3302
3303 // A round trip through this build must not lose anything either.
3304 VersionedBlob snap;
3305 {
3306 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
3307 snap = SerializePendingPayoutsLocked();
3308 }
3309 PublishPendingPayouts(snap);
3310 PayoutTestResetState();
3311 LoadPendingPayouts();
3312 {
3313 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
3314 check(gPendingPayouts.size() == 1 &&
3315 gPendingPayouts[0].blockHeight == 2000 &&
3316 gPendingPayouts[0].matureAtHeight == 2120 &&
3317 gPendingPayouts[0].paidTxids[addrA] == "abc123",
3318 "what this build writes, this build reads back unchanged");
3319 }
3320 }
3321
3322 // -- 10. A forfeit is published, not just dropped ------------------------
3323 {
3324 PayoutTestResetState();
3325 remove(ledgerPath.c_str());
3326 const std::string roundsPath = dir + "/pool_rounds_selftest.json";
3327 const std::string savedRoundsFile = strPoolRoundsFile;
3328 strPoolRoundsFile = roundsPath;
3329 remove(roundsPath.c_str());
3330
3331 const int height = 606;
3332 PoolRoundProof round;
3333 round.id = PayoutIdForBlock(height, 0);
3334 round.blockHeight = height;
3335 round.matureAtHeight = height;
3336 round.payouts[addrA] = 5 * COIN;
3337 round.payouts[addrB] = 6 * COIN;
3338 RecordPoolRound(round);
3339
3340 {
3341 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
3342 PendingPayout pp;
3343 pp.blockHeight = height; pp.blockHash = 0; pp.matureAtHeight = height;
3344 pp.amounts[addrA] = 5 * COIN;
3345 pp.amounts[addrB] = 6 * COIN;
3346 gPendingPayouts.push_back(pp);
3347 }
3348
3349 PayingRecord r;
3350 r.blockHeight = height; r.blockHash = 0; r.matureAtHeight = height;
3351 r.address = addrA; r.amount = 5 * COIN;
3352 ForfeitPayoutRecipient(r, "selftest reason");
3353
3354 {
3355 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
3356 check(gPendingPayouts.size() == 1 &&
3357 gPendingPayouts[0].amounts.count(addrA) == 0 &&
3358 gPendingPayouts[0].amounts.count(addrB) == 1,
3359 "a forfeit removes only that recipient from the ledger entry");
3360 }
3361
3362 std::string onDisk;
3363 bool proofOk = false;
3364 if (PayoutTestReadFile(roundsPath, onDisk)) {
3365 try {
3366 nlohmann::json root = nlohmann::json::parse(onDisk);
3367 const nlohmann::json& p = root["rounds"][0]["payouts"];
3368 proofOk = p[addrA].value("forfeited", false) &&
3369 p[addrA].value("forfeitReason", "") == "selftest reason" &&
3370 p[addrA]["txid"].is_null() &&
3371 !p[addrB].contains("forfeited");
3372 } catch (...) {
3373 proofOk = false;
3374 }
3375 }
3376 check(proofOk,
3377 "the published round proof records the forfeit instead of"
3378 " advertising the address as still owed");
3379
3380 // And it survives a reload of the proof file.
3381 {
3382 std::lock_guard<std::mutex> lk(gPoolRoundsMutex);
3383 gPoolRounds.clear();
3384 }
3385 LoadPoolRounds();
3386 {
3387 std::lock_guard<std::mutex> lk(gPoolRoundsMutex);
3388 check(gPoolRounds.size() == 1 &&
3389 gPoolRounds[0].forfeited.count(addrA) == 1 &&
3390 gPoolRounds[0].forfeited[addrA] == "selftest reason",
3391 "the forfeit is read back from pool_rounds.json");
3392 }
3393
3394 remove(roundsPath.c_str());
3395 remove((roundsPath + ".tmp").c_str());
3396 strPoolRoundsFile = savedRoundsFile;
3397 }
3398
3399 // -- clean up ------------------------------------------------------------
3400 PayoutTestResetState();
3401 CRITICAL_BLOCK(cs_mapWallet)
3402 mapWallet.clear();
3403 remove(ledgerPath.c_str());
3404 remove((ledgerPath + ".tmp").c_str());
3405 remove(payingPath.c_str());
3406 remove((payingPath + ".tmp").c_str());
3407 remove((dir + "/pool_status.json").c_str());
3408 remove((dir + "/pool_rounds.json").c_str());
3409#ifdef _WIN32
3410 _rmdir(dir.c_str());
3411#else
3412 rmdir(dir.c_str());
3413#endif
3414 strSetDataDir = savedDataDir;
3415 nBestHeight = savedBestHeight;
3416
3417 fprintf(stdout, "%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
3418 nFail, nFail == 1 ? "" : "s");
3419 fflush(stdout);
3420 return nFail == 0 ? 0 : 1;
3421}
3422
2752int RunPoolStratumSelfTest() 3423int RunPoolStratumSelfTest()
2753{ 3424{
2754 int nFail = 0; 3425 int nFail = 0;