|
| 1 | +// ltc_auto_ratchet_tail_guard_test: FENCED, additive conformance KATs for the |
| 2 | +// AutoRatchet WORK-WEIGHTED 60% tail guard, ported from the dgb boundary suite |
| 3 | +// (src/impl/dgb/test/auto_ratchet_tail_guard_test.cpp) to fill the LTC unit |
| 4 | +// coverage gap. LTC already has the staged-migration integration sim |
| 5 | +// (auto_ratchet_sim_test.cpp) and the desired-version tally KATs |
| 6 | +// (desired_version_tally_test.cpp); this file adds the focused *arithmetic* |
| 7 | +// boundary pins those two do not carry. |
| 8 | +// |
| 9 | +// Unlike dgb, LTC has NO lifted auto_ratchet_tail_guard.hpp SSOT header: the |
| 10 | +// tail guard is the INLINE expression in AutoRatchet::get_share_version |
| 11 | +// (auto_ratchet.hpp:171): |
| 12 | +// |
| 13 | +// tail_ok = !(tail_target * uint32_t(100) < tail_total * uint32_t(SWITCH)) |
| 14 | +// |
| 15 | +// which is the EXACT-RATIONAL test target < total*60/100. These KATs pin that |
| 16 | +// LIVE inline form NON-CIRCULARLY, against (a) hand-derived oracle values and |
| 17 | +// (b) an independent replica of the p2pool canonical FLOOR rule |
| 18 | +// (data.py:1399): SWITCHED iff target >= floor(total*60/100). The two forms |
| 19 | +// diverge in exactly one documented case -- target == floor(total*60/100) with |
| 20 | +// (total*60) % 100 != 0 -- where the LTC inline waits one extra work-quantum |
| 21 | +// (negligibly STRICTER). The FloorBoundary / ExactFloor / LargeWeights cases |
| 22 | +// are the point of the port: they catch quantization / uint288 overflow bugs. |
| 23 | +// |
| 24 | +// The consensus path is NOT exercised here -- this is a pure arithmetic pin of |
| 25 | +// the live inline rule. Test-only / ltc-tree-local. Joins the share_test |
| 26 | +// executable (already on the build.yml --target allowlist). |
| 27 | + |
| 28 | +#include <gtest/gtest.h> |
| 29 | + |
| 30 | +#include <impl/ltc/auto_ratchet.hpp> // ltc::AutoRatchet::SWITCH_THRESHOLD |
| 31 | +#include <core/uint256.hpp> // uint288 work-weight accumulator |
| 32 | + |
| 33 | +#include <cstdint> |
| 34 | +#include <map> |
| 35 | +#include <utility> |
| 36 | + |
| 37 | +using ltc::AutoRatchet; |
| 38 | + |
| 39 | +namespace { |
| 40 | + |
| 41 | +// Verbatim replica of the LIVE inline work-weighted tail guard in |
| 42 | +// AutoRatchet::get_share_version (auto_ratchet.hpp:171): |
| 43 | +// tail_ok = !(tail_target * uint32_t(100) < tail_total * uint32_t(SWITCH)); |
| 44 | +// This is LTC's REAL consensus rule (EXACT-RATIONAL form). Kept local so the |
| 45 | +// boundary tests pin the inline split WITHOUT driving the consensus path or |
| 46 | +// importing a lifted SSOT. uint288 = work-weight accumulator. |
| 47 | +bool inline_tail_ok(const uint288& target, const uint288& total, |
| 48 | + int thr = AutoRatchet::SWITCH_THRESHOLD) |
| 49 | +{ |
| 50 | + return !((target * static_cast<uint32_t>(100)) < |
| 51 | + (total * static_cast<uint32_t>(thr))); |
| 52 | +} |
| 53 | + |
| 54 | +// Independent replica of the p2pool CANONICAL switch oracle (data.py:1399) |
| 55 | +// with EXACT FLOOR semantics: SWITCHED iff target >= floor(total*thr/100). |
| 56 | +// This is NOT what LTC runs -- it is the reference the live inline form is |
| 57 | +// pinned against, so the one-quantum divergence is localised deliberately. |
| 58 | +bool canonical_floor_switched(const uint288& target, const uint288& total, |
| 59 | + int thr = AutoRatchet::SWITCH_THRESHOLD) |
| 60 | +{ |
| 61 | + // floor(total * thr / 100) on the unsigned work accumulator. base_uint |
| 62 | + // multiply takes uint32_t; divide takes W, so wrap 100 in uint288. |
| 63 | + const uint288 floor_gate = |
| 64 | + (total * static_cast<uint32_t>(thr)) / uint288(static_cast<uint64_t>(100)); |
| 65 | + return !(target < floor_gate); |
| 66 | +} |
| 67 | + |
| 68 | +// Mirror of the LTC inline accumulation loop (auto_ratchet.hpp:164-169): |
| 69 | +// reduce a desired-version -> work-weight map into {weight voting >= target, |
| 70 | +// total weight}. uint288 default-initialises to 0; the sum is order-independent. |
| 71 | +std::pair<uint288, uint288> |
| 72 | +reduce_target_total(const std::map<uint64_t, uint288>& weights, |
| 73 | + int64_t target_version) |
| 74 | +{ |
| 75 | + uint288 target{}, total{}; |
| 76 | + for (const auto& [ver, w] : weights) { |
| 77 | + total = total + w; |
| 78 | + if (static_cast<int64_t>(ver) >= target_version) |
| 79 | + target = target + w; |
| 80 | + } |
| 81 | + return {target, total}; |
| 82 | +} |
| 83 | + |
| 84 | +// LTC end-to-end tail guard over a weight map: reduce, then apply the LIVE |
| 85 | +// inline rule (NOT the floor oracle -- this is what the pool actually runs). |
| 86 | +bool tail_guard_passes(const std::map<uint64_t, uint288>& weights, |
| 87 | + int64_t target_version) |
| 88 | +{ |
| 89 | + const auto tt = reduce_target_total(weights, target_version); |
| 90 | + return inline_tail_ok(tt.first, tt.second); |
| 91 | +} |
| 92 | + |
| 93 | +} // namespace |
| 94 | + |
| 95 | +// --- reduce_target_total mirrors the inline accumulation ------------------- |
| 96 | +TEST(LtcAutoRatchetTailGuard, ReduceSplitsAtTargetVersion) |
| 97 | +{ |
| 98 | + std::map<uint64_t, uint288> w{ |
| 99 | + {34, uint288(5)}, // below target -> total only |
| 100 | + {35, uint288(7)}, // below target -> total only |
| 101 | + {36, uint288(11)}, // >= target -> target + total |
| 102 | + {37, uint288(13)}, // >= target -> target + total |
| 103 | + }; |
| 104 | + auto tt = reduce_target_total(w, /*target_version=*/36); |
| 105 | + EXPECT_EQ(tt.first, uint288(24)); // 11 + 13 |
| 106 | + EXPECT_EQ(tt.second, uint288(36)); // 5 + 7 + 11 + 13 |
| 107 | +} |
| 108 | + |
| 109 | +TEST(LtcAutoRatchetTailGuard, EmptyMapIsZeroZero) |
| 110 | +{ |
| 111 | + std::map<uint64_t, uint288> w; |
| 112 | + auto tt = reduce_target_total(w, 36); |
| 113 | + EXPECT_EQ(tt.first, uint288(0)); |
| 114 | + EXPECT_EQ(tt.second, uint288(0)); |
| 115 | +} |
| 116 | + |
| 117 | +// --- 60%-by-WORK gate at an EXACT floor (total=100) ------------------------ |
| 118 | +TEST(LtcAutoRatchetTailGuard, SwitchedHonoursSixtyPercentFloor) |
| 119 | +{ |
| 120 | + // total=100 -> floor(100*60/100)=60. Exact floor: inline == canonical. |
| 121 | + const uint288 total(100); |
| 122 | + EXPECT_FALSE(inline_tail_ok(uint288(59), total)); |
| 123 | + EXPECT_TRUE (inline_tail_ok(uint288(60), total)); |
| 124 | + EXPECT_TRUE (inline_tail_ok(uint288(61), total)); |
| 125 | + EXPECT_TRUE (inline_tail_ok(uint288(100), total)); |
| 126 | + // Independent floor oracle agrees at every one of these points. |
| 127 | + EXPECT_EQ(canonical_floor_switched(uint288(59), total), inline_tail_ok(uint288(59), total)); |
| 128 | + EXPECT_EQ(canonical_floor_switched(uint288(60), total), inline_tail_ok(uint288(60), total)); |
| 129 | + EXPECT_EQ(canonical_floor_switched(uint288(100), total), inline_tail_ok(uint288(100), total)); |
| 130 | +} |
| 131 | + |
| 132 | +TEST(LtcAutoRatchetTailGuard, AllOldNeverSwitches) |
| 133 | +{ |
| 134 | + // No V>=target work at all: target=0 -> target*100=0 < total*60 for any |
| 135 | + // total>=1 -> inline holds (never passes the gate). |
| 136 | + EXPECT_FALSE(inline_tail_ok(uint288(0), uint288(2))); |
| 137 | + EXPECT_FALSE(inline_tail_ok(uint288(0), uint288(1000))); |
| 138 | +} |
| 139 | + |
| 140 | +TEST(LtcAutoRatchetTailGuard, ZeroTotalSwitches) |
| 141 | +{ |
| 142 | + // Empty window: 0*100 < 0*60 is False, so !False = tail_ok=true. The guard |
| 143 | + // is vacuously satisfied (matches p2pool: 0 < 0 is False -> not "old"). |
| 144 | + EXPECT_TRUE(inline_tail_ok(uint288(0), uint288(0))); |
| 145 | + EXPECT_TRUE(canonical_floor_switched(uint288(0), uint288(0))); |
| 146 | +} |
| 147 | + |
| 148 | +// --- non-circular boundary: LTC live inline vs canonical FLOOR oracle ------- |
| 149 | +// The SOLE divergence: target == floor(total*60/100) AND (total*60) % 100 != 0. |
| 150 | +// total=7: total*60=420, floor(420/100)=4. |
| 151 | +// canonical floor : target>=4 -> SWITCHED at target==4. |
| 152 | +// LTC live inline : 4*100=400 < 420 -> tail_ok=false (waits) at 4. |
| 153 | +// LTC runs the inline form, so LTC is STRICTER by one work-quantum here. Pinned |
| 154 | +// so a future byte-faithful rewrite is a deliberate, reviewed change. |
| 155 | +TEST(LtcAutoRatchetTailGuard, FloorBoundaryDivergesFromInlineByOneQuantum) |
| 156 | +{ |
| 157 | + const uint288 total(7); |
| 158 | + |
| 159 | + // Below the floor: both agree -> not switched. |
| 160 | + EXPECT_FALSE(canonical_floor_switched(uint288(3), total)); |
| 161 | + EXPECT_FALSE(inline_tail_ok(uint288(3), total)); |
| 162 | + |
| 163 | + // AT the floor (=4): canonical switches, LTC inline still waits. |
| 164 | + EXPECT_TRUE (canonical_floor_switched(uint288(4), total)); // 4 >= floor(4.2)=4 |
| 165 | + EXPECT_FALSE(inline_tail_ok(uint288(4), total)); // 400 < 420 |
| 166 | + |
| 167 | + // Above the floor: both agree -> switched / tail_ok. |
| 168 | + EXPECT_TRUE(canonical_floor_switched(uint288(5), total)); |
| 169 | + EXPECT_TRUE(inline_tail_ok(uint288(5), total)); |
| 170 | +} |
| 171 | + |
| 172 | +// Where (total*60) % 100 == 0 the floor is exact and the two forms AGREE for |
| 173 | +// every target -> the divergence really is confined to the non-exact case. |
| 174 | +TEST(LtcAutoRatchetTailGuard, ExactFloorAgreesWithInline) |
| 175 | +{ |
| 176 | + const uint288 total(100); // 100*60=6000, divisible by 100, floor exact. |
| 177 | + for (uint32_t t = 55; t <= 65; ++t) { |
| 178 | + const uint288 target(t); |
| 179 | + EXPECT_EQ(canonical_floor_switched(target, total), inline_tail_ok(target, total)) |
| 180 | + << "target=" << t; |
| 181 | + } |
| 182 | +} |
| 183 | + |
| 184 | +// --- tail_guard_passes end-to-end over a weight map (LTC live rule) --------- |
| 185 | +TEST(LtcAutoRatchetTailGuard, TailGuardPassesOnWorkWeightedMajority) |
| 186 | +{ |
| 187 | + // 70 work-units vote V36, 30 vote V35 -> 70*100 >= 100*60 -> pass. |
| 188 | + std::map<uint64_t, uint288> pass{{35, uint288(30)}, {36, uint288(70)}}; |
| 189 | + EXPECT_TRUE(tail_guard_passes(pass, 36)); |
| 190 | + |
| 191 | + // 55 vote V36, 45 vote V35 -> 5500 < 6000 -> guard holds (do NOT activate). |
| 192 | + std::map<uint64_t, uint288> wait{{35, uint288(45)}, {36, uint288(55)}}; |
| 193 | + EXPECT_FALSE(tail_guard_passes(wait, 36)); |
| 194 | +} |
| 195 | + |
| 196 | +// Large uint288 work weights (realistic target_to_average_attempts scale): |
| 197 | +// canonical floor and LTC inline agree -- the boundary case is measure-zero in |
| 198 | +// practice, and this exercises the multiply/divide path against overflow. |
| 199 | +TEST(LtcAutoRatchetTailGuard, LargeWeightsAgreeWithInline) |
| 200 | +{ |
| 201 | + const uint288 big = uint288(uint64_t(1) << 40); // ~1.1e12 work-units |
| 202 | + const uint288 total = big * static_cast<uint32_t>(100); |
| 203 | + for (uint32_t pct : {0u, 1u, 59u, 60u, 61u, 99u, 100u}) { |
| 204 | + const uint288 target = big * pct; |
| 205 | + EXPECT_EQ(canonical_floor_switched(target, total), inline_tail_ok(target, total)) |
| 206 | + << "pct=" << pct; |
| 207 | + } |
| 208 | +} |
0 commit comments