Skip to content

Commit 9aa431f

Browse files
authored
ltc: port AutoRatchet tail-guard boundary KATs from dgb (fill LTC coverage gap) (#701)
Adds src/impl/ltc/test/auto_ratchet_tail_guard_test.cpp: 9 focused arithmetic boundary cases for the work-weighted 60% tail guard, ported from the dgb suite. LTC has no lifted SSOT header, so the KATs pin the LIVE inline exact-rational rule (auto_ratchet.hpp:171) non-circularly against hand-derived oracle values and an independent replica of the p2pool canonical floor rule (data.py:1399). FloorBoundary/ExactFloor/LargeWeights cases localise the one-quantum floor divergence and exercise the uint288 multiply/divide path against overflow. Joins the share_test executable. Co-authored-by: frstrtr <frstrtr@users.noreply.github.com>
1 parent 9e4ba86 commit 9aa431f

2 files changed

Lines changed: 209 additions & 1 deletion

File tree

src/impl/ltc/test/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
if (BUILD_TESTING AND GTest_FOUND)
2-
add_executable(share_test share_test.cpp f11_donation_invariance_test.cpp emergency_decay_overflow_test.cpp wirecompat_runtime_test.cpp desired_version_tally_test.cpp auto_ratchet_sim_test.cpp)
2+
add_executable(share_test share_test.cpp f11_donation_invariance_test.cpp emergency_decay_overflow_test.cpp wirecompat_runtime_test.cpp desired_version_tally_test.cpp auto_ratchet_sim_test.cpp auto_ratchet_tail_guard_test.cpp)
33
target_link_libraries(share_test PRIVATE
44
GTest::gtest_main GTest::gtest
55
core ltc
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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

Comments
 (0)