diff options
| -rw-r--r-- | src/Makefile | 1 | ||||
| -rw-r--r-- | src/rpc.cpp | 671 | ||||
| -rw-r--r-- | src/selftest.cpp | 5 |
3 files changed, 676 insertions, 1 deletions
diff --git a/src/Makefile b/src/Makefile index 1166daf..89ba559 100644 --- a/src/Makefile +++ b/src/Makefile | |||
| @@ -192,6 +192,7 @@ tests: test_bip32 bitflash-node | |||
| 192 | ./bitflash-node -selftest=net-message | 192 | ./bitflash-node -selftest=net-message |
| 193 | ./bitflash-node -selftest=consensus-limits | 193 | ./bitflash-node -selftest=consensus-limits |
| 194 | ./bitflash-node -selftest=pool-stratum | 194 | ./bitflash-node -selftest=pool-stratum |
| 195 | ./bitflash-node -selftest=payout-ledger | ||
| 195 | ./bitflash-node -selftest=parse-money | 196 | ./bitflash-node -selftest=parse-money |
| 196 | ./bitflash-node -selftest=socks5-proxy | 197 | ./bitflash-node -selftest=socks5-proxy |
| 197 | 198 | ||
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 | |||
| 2762 | static 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 | |||
| 2778 | static 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. | ||
| 2788 | static 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. | ||
| 2811 | static 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 | |||
| 2819 | static 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. | ||
| 2830 | static 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 | |||
| 2856 | int 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 | |||
| 2752 | int RunPoolStratumSelfTest() | 3423 | int RunPoolStratumSelfTest() |
| 2753 | { | 3424 | { |
| 2754 | int nFail = 0; | 3425 | int nFail = 0; |
diff --git a/src/selftest.cpp b/src/selftest.cpp index eed6779..034e1f7 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp | |||
| @@ -13,6 +13,7 @@ | |||
| 13 | #include "walletcmd.h" | 13 | #include "walletcmd.h" |
| 14 | 14 | ||
| 15 | extern int RunPoolStratumSelfTest(); | 15 | extern int RunPoolStratumSelfTest(); |
| 16 | extern int RunPayoutLedgerSelfTest(); | ||
| 16 | 17 | ||
| 17 | // Test results go to the terminal, not to debug.log. | 18 | // Test results go to the terminal, not to debug.log. |
| 18 | // | 19 | // |
| @@ -1573,12 +1574,14 @@ int RunSelfTest(const std::string& name) | |||
| 1573 | return RunConsensusLimitsSelfTest(); | 1574 | return RunConsensusLimitsSelfTest(); |
| 1574 | if (name == "pool-stratum") | 1575 | if (name == "pool-stratum") |
| 1575 | return RunPoolStratumSelfTest(); | 1576 | return RunPoolStratumSelfTest(); |
| 1577 | if (name == "payout-ledger") | ||
| 1578 | return RunPayoutLedgerSelfTest(); | ||
| 1576 | if (name == "parse-money") | 1579 | if (name == "parse-money") |
| 1577 | return RunParseMoneySelfTest(); | 1580 | return RunParseMoneySelfTest(); |
| 1578 | if (name == "socks5-proxy") | 1581 | if (name == "socks5-proxy") |
| 1579 | return RunSocks5ProxySelfTest(); | 1582 | return RunSocks5ProxySelfTest(); |
| 1580 | 1583 | ||
| 1581 | printf("Unknown self-test '%s'\n", name.c_str()); | 1584 | printf("Unknown self-test '%s'\n", name.c_str()); |
| 1582 | printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy\n"); | 1585 | printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt, net-message, consensus-limits, pool-stratum, payout-ledger, parse-money, socks5-proxy\n"); |
| 1583 | return 1; | 1586 | return 1; |
| 1584 | } | 1587 | } |