|
| 1 | +#include <gtest/gtest.h> |
| 2 | +#include <core/uint256.hpp> |
| 3 | +#include <cstdint> |
| 4 | +#include <map> |
| 5 | +#include <string> |
| 6 | +#include <vector> |
| 7 | + |
| 8 | +// Replicate the decay PPLNS computation from share_tracker.hpp |
| 9 | +// to verify it matches Python exactly. |
| 10 | + |
| 11 | +struct DecayResult { |
| 12 | + std::map<std::string, uint288> weights; |
| 13 | + uint288 total_weight; |
| 14 | + uint288 total_donation_weight; |
| 15 | +}; |
| 16 | + |
| 17 | +struct ShareData { |
| 18 | + uint64_t att; |
| 19 | + uint32_t donation; |
| 20 | + std::string address; |
| 21 | +}; |
| 22 | + |
| 23 | +DecayResult compute_decayed_weights( |
| 24 | + const std::vector<ShareData>& shares, |
| 25 | + int32_t max_shares, |
| 26 | + const uint288& desired_weight, |
| 27 | + uint32_t chain_length) |
| 28 | +{ |
| 29 | + static constexpr uint64_t DECAY_PRECISION = 40; |
| 30 | + static constexpr uint64_t DECAY_SCALE = uint64_t(1) << DECAY_PRECISION; |
| 31 | + static constexpr uint64_t LN2_MICRO = 693147; |
| 32 | + |
| 33 | + uint32_t half_life = std::max(chain_length / 4, uint32_t(1)); |
| 34 | + uint64_t decay_per = DECAY_SCALE - (DECAY_SCALE * LN2_MICRO) / (uint64_t(1000000) * half_life); |
| 35 | + |
| 36 | + DecayResult result; |
| 37 | + int32_t share_count = 0; |
| 38 | + uint64_t decay_fp = DECAY_SCALE; |
| 39 | + |
| 40 | + for (const auto& share : shares) |
| 41 | + { |
| 42 | + if (share_count >= max_shares) |
| 43 | + break; |
| 44 | + |
| 45 | + uint288 att(share.att); |
| 46 | + uint32_t don = share.donation; |
| 47 | + |
| 48 | + uint288 decayed_att = (att * uint288(decay_fp)) >> DECAY_PRECISION; |
| 49 | + |
| 50 | + auto addr_w = decayed_att * static_cast<uint32_t>(65535 - don); |
| 51 | + auto don_w = decayed_att * don; |
| 52 | + auto this_total = addr_w + don_w; |
| 53 | + |
| 54 | + if (result.total_weight + this_total > desired_weight) { |
| 55 | + auto remaining = desired_weight - result.total_weight; |
| 56 | + if (!this_total.IsNull()) { |
| 57 | + addr_w = addr_w * remaining / this_total; |
| 58 | + don_w = don_w * remaining / this_total; |
| 59 | + } |
| 60 | + this_total = remaining; |
| 61 | + } |
| 62 | + |
| 63 | + result.weights[share.address] += addr_w; |
| 64 | + result.total_weight += this_total; |
| 65 | + result.total_donation_weight += don_w; |
| 66 | + |
| 67 | + ++share_count; |
| 68 | + if (result.total_weight >= desired_weight) |
| 69 | + break; |
| 70 | + |
| 71 | + decay_fp = static_cast<uint64_t>( |
| 72 | + (static_cast<__uint128_t>(decay_fp) * decay_per) >> DECAY_PRECISION); |
| 73 | + } |
| 74 | + |
| 75 | + return result; |
| 76 | +} |
| 77 | + |
| 78 | +TEST(DecayPPLNS, MatchesPythonExactValues) |
| 79 | +{ |
| 80 | + // Same 10-share test case as Python test_output_for_cpp_comparison |
| 81 | + std::vector<ShareData> shares = { |
| 82 | + {1000000, 50, "addr_A"}, |
| 83 | + {2000000, 50, "addr_B"}, |
| 84 | + {1500000, 50, "addr_A"}, |
| 85 | + {1000000, 50, "addr_B"}, |
| 86 | + {3000000, 50, "addr_A"}, |
| 87 | + {1000000, 50, "addr_B"}, |
| 88 | + {1000000, 50, "addr_A"}, |
| 89 | + {2000000, 50, "addr_B"}, |
| 90 | + {1000000, 50, "addr_A"}, |
| 91 | + {1500000, 50, "addr_B"}, |
| 92 | + }; |
| 93 | + |
| 94 | + uint288 unlimited; |
| 95 | + unlimited.SetHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); |
| 96 | + |
| 97 | + auto result = compute_decayed_weights(shares, 10, unlimited, 400); |
| 98 | + |
| 99 | + // Expected values from Python: |
| 100 | + // total_weight=953350555395 |
| 101 | + // total_donation_weight=727359850 |
| 102 | + // weight[addr_A]=478168850600 |
| 103 | + // weight[addr_B]=474454344945 |
| 104 | + EXPECT_EQ(result.total_weight.GetLow64(), uint64_t(953350555395)); |
| 105 | + EXPECT_EQ(result.total_donation_weight.GetLow64(), uint64_t(727359850)); |
| 106 | + EXPECT_EQ(result.weights["addr_A"].GetLow64(), uint64_t(478168850600)); |
| 107 | + EXPECT_EQ(result.weights["addr_B"].GetLow64(), uint64_t(474454344945)); |
| 108 | + |
| 109 | + // Verify: weights + donation = total |
| 110 | + uint288 sum_weights = result.weights["addr_A"] + result.weights["addr_B"]; |
| 111 | + EXPECT_EQ(sum_weights + result.total_donation_weight, result.total_weight); |
| 112 | +} |
| 113 | + |
| 114 | +TEST(DecayPPLNS, TwoMinersAlternating400Shares) |
| 115 | +{ |
| 116 | + // 400 shares, miners alternate, unlimited weight |
| 117 | + std::vector<ShareData> shares; |
| 118 | + for (int i = 0; i < 400; ++i) |
| 119 | + shares.push_back({1541819, 0, (i % 2 == 0) ? "A" : "B"}); |
| 120 | + |
| 121 | + uint288 unlimited; |
| 122 | + unlimited.SetHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); |
| 123 | + |
| 124 | + auto result = compute_decayed_weights(shares, 400, unlimited, 400); |
| 125 | + |
| 126 | + // Python: A=6861325693665, B=6813766223280, total=13675091916945 |
| 127 | + EXPECT_EQ(result.weights["A"].GetLow64(), uint64_t(6861325693665)); |
| 128 | + EXPECT_EQ(result.weights["B"].GetLow64(), uint64_t(6813766223280)); |
| 129 | + EXPECT_EQ(result.total_weight.GetLow64(), uint64_t(13675091916945)); |
| 130 | + |
| 131 | + // Check payout amounts |
| 132 | + uint64_t subsidy = 156250000; |
| 133 | + uint64_t amt_A = (uint288(subsidy) * result.weights["A"] / result.total_weight).GetLow64(); |
| 134 | + uint64_t amt_B = (uint288(subsidy) * result.weights["B"] / result.total_weight).GetLow64(); |
| 135 | + |
| 136 | + // Python: A=78396704, B=77853295 |
| 137 | + EXPECT_EQ(amt_A, uint64_t(78396704)); |
| 138 | + EXPECT_EQ(amt_B, uint64_t(77853295)); |
| 139 | +} |
| 140 | + |
| 141 | +TEST(DecayPPLNS, CappedVsUnlimited) |
| 142 | +{ |
| 143 | + // Verify that capped desired_weight truncates to ~2 shares |
| 144 | + std::vector<ShareData> shares; |
| 145 | + for (int i = 0; i < 400; ++i) |
| 146 | + shares.push_back({1541819, 0, (i % 2 == 0) ? "A" : "B"}); |
| 147 | + |
| 148 | + // Capped: block_att * SPREAD * 65535 |
| 149 | + uint64_t block_att = 1048577; |
| 150 | + uint288 capped_desired = uint288(65535) * uint288(3) * uint288(block_att); |
| 151 | + |
| 152 | + auto result_cap = compute_decayed_weights(shares, 400, capped_desired, 400); |
| 153 | + |
| 154 | + // Capped total should equal desired_weight exactly (cap was hit) |
| 155 | + EXPECT_EQ(result_cap.total_weight, capped_desired); |
| 156 | + |
| 157 | + // With unlimited |
| 158 | + uint288 unlimited; |
| 159 | + unlimited.SetHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); |
| 160 | + auto result_ulim = compute_decayed_weights(shares, 400, unlimited, 400); |
| 161 | + |
| 162 | + // Unlimited total should be much larger |
| 163 | + EXPECT_TRUE(result_ulim.total_weight > capped_desired); |
| 164 | + |
| 165 | + // Both should have 2 addresses |
| 166 | + EXPECT_EQ(result_cap.weights.size(), 2u); |
| 167 | + EXPECT_EQ(result_ulim.weights.size(), 2u); |
| 168 | + |
| 169 | + // Unlimited should give more equal distribution |
| 170 | + uint64_t subsidy = 156250000; |
| 171 | + uint64_t cap_A = (uint288(subsidy) * result_cap.weights["A"] / result_cap.total_weight).GetLow64(); |
| 172 | + uint64_t cap_B = (uint288(subsidy) * result_cap.weights["B"] / result_cap.total_weight).GetLow64(); |
| 173 | + uint64_t ulim_A = (uint288(subsidy) * result_ulim.weights["A"] / result_ulim.total_weight).GetLow64(); |
| 174 | + uint64_t ulim_B = (uint288(subsidy) * result_ulim.weights["B"] / result_ulim.total_weight).GetLow64(); |
| 175 | + |
| 176 | + // Unlimited should be closer to 50/50 |
| 177 | + int64_t cap_diff = std::abs(int64_t(cap_A) - int64_t(cap_B)); |
| 178 | + int64_t ulim_diff = std::abs(int64_t(ulim_A) - int64_t(ulim_B)); |
| 179 | + EXPECT_LT(ulim_diff, cap_diff); |
| 180 | + |
| 181 | + std::cout << "Capped: A=" << cap_A << " B=" << cap_B << " diff=" << cap_diff << std::endl; |
| 182 | + std::cout << "Unlimited: A=" << ulim_A << " B=" << ulim_B << " diff=" << ulim_diff << std::endl; |
| 183 | +} |
| 184 | + |
| 185 | +TEST(DecayPPLNS, DecayConverges) |
| 186 | +{ |
| 187 | + // Total weight should converge as we add more shares |
| 188 | + // (decay makes old shares negligible) |
| 189 | + std::vector<ShareData> shares; |
| 190 | + for (int i = 0; i < 4000; ++i) |
| 191 | + shares.push_back({1541819, 50, "miner"}); |
| 192 | + |
| 193 | + uint288 unlimited; |
| 194 | + unlimited.SetHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); |
| 195 | + |
| 196 | + auto r400 = compute_decayed_weights(shares, 400, unlimited, 400); |
| 197 | + auto r4000 = compute_decayed_weights(shares, 4000, unlimited, 400); |
| 198 | + |
| 199 | + // Ratio should be close to 1.0 (within 10%) |
| 200 | + // Python: ratio = 1.065979 |
| 201 | + double ratio = static_cast<double>(r4000.total_weight.GetLow64()) / |
| 202 | + static_cast<double>(r400.total_weight.GetLow64()); |
| 203 | + EXPECT_GT(ratio, 1.0); |
| 204 | + EXPECT_LT(ratio, 1.1); |
| 205 | + |
| 206 | + std::cout << "400 shares: total_weight=" << r400.total_weight.GetLow64() << std::endl; |
| 207 | + std::cout << "4000 shares: total_weight=" << r4000.total_weight.GetLow64() << std::endl; |
| 208 | + std::cout << "Ratio: " << ratio << std::endl; |
| 209 | +} |
| 210 | + |
| 211 | +TEST(DecayPPLNS, PerShareDecayFactors) |
| 212 | +{ |
| 213 | + // Verify individual decay_fp values match Python |
| 214 | + static constexpr uint64_t DECAY_PRECISION = 40; |
| 215 | + static constexpr uint64_t DECAY_SCALE = uint64_t(1) << DECAY_PRECISION; |
| 216 | + static constexpr uint64_t LN2_MICRO = 693147; |
| 217 | + |
| 218 | + uint32_t chain_length = 400; |
| 219 | + uint32_t half_life = chain_length / 4; // = 100 |
| 220 | + uint64_t decay_per = DECAY_SCALE - (DECAY_SCALE * LN2_MICRO) / (uint64_t(1000000) * half_life); |
| 221 | + |
| 222 | + // Python: decay_per=1091890395914 (hex: fe39bd3b0a) |
| 223 | + EXPECT_EQ(decay_per, uint64_t(1091890395914)); |
| 224 | + |
| 225 | + // Verify first few decay_fp values |
| 226 | + uint64_t decay_fp = DECAY_SCALE; |
| 227 | + uint64_t expected_fps[] = { |
| 228 | + 1099511627776, // share[0] |
| 229 | + 1091890395914, // share[1] |
| 230 | + 1084321990392, // share[2] |
| 231 | + 1076806045045, // share[3] |
| 232 | + 1069342196248, // share[4] |
| 233 | + }; |
| 234 | + |
| 235 | + for (int i = 0; i < 5; ++i) { |
| 236 | + EXPECT_EQ(decay_fp, expected_fps[i]) << "Mismatch at share " << i; |
| 237 | + decay_fp = static_cast<uint64_t>( |
| 238 | + (static_cast<__uint128_t>(decay_fp) * decay_per) >> DECAY_PRECISION); |
| 239 | + } |
| 240 | +} |
| 241 | + |
| 242 | +int main(int argc, char** argv) |
| 243 | +{ |
| 244 | + ::testing::InitGoogleTest(&argc, argv); |
| 245 | + return RUN_ALL_TESTS(); |
| 246 | +} |
0 commit comments