Skip to content

Commit 12de90f

Browse files
authored
Merge pull request #676 from frstrtr/ltc-doge/ltc-desired-version-tally-kat
test(ltc): AutoRatchet sim + desired-version tally KATs (G2)
2 parents 8d61741 + 5fb702b commit 12de90f

3 files changed

Lines changed: 448 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)
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)
33
target_link_libraries(share_test PRIVATE
44
GTest::gtest_main GTest::gtest
55
core ltc
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
// ltc_auto_ratchet_sim_test: FENCED, additive rig-free SIM KAT that arms the
3+
// LTC G2 staged-migration harness
4+
// (scripts/ltc_g2_crossing_staged_migration_harness.sh) crossing rows WITHOUT
5+
// a Scrypt rig or a live litecoind parent -- the C++ counterpart of that
6+
// harness's staged-vote path. It exercises the AutoRatchet staged-gate LOGIC
7+
// over hand-derived oracle votes:
8+
//
9+
// C2 VOTING mints the LTC BASELINE (v35) while voting V36 (bootstrap, empty
10+
// chain) -- a freshly-started node must not skip ahead of the network
11+
// C3 #288 accept gate: 60%-by-WORK, and a 95%-by-COUNT activation can NOT
12+
// outrun it (mint-cannot-outrun-accept) -- the property that keeps a
13+
// minted V36 boundary share from being rejected by every peer
14+
// C3 staged 1-by-1 miner migration: the work-weighted V36 tally advances
15+
// MONOTONICALLY and effective activation flips on exactly once BOTH the
16+
// 95%-by-count AND the 60%-by-work gates hold (the crossing-dynamics proof)
17+
// C4 ratchet state persists across restart (CONFIRMED survives reconstruct;
18+
// a fresh VOTING node mints the v35 baseline on bootstrap)
19+
//
20+
// LTC ORACLE PIN: LTC is the V36 REFERENCE impl and transitions its own
21+
// v35 -> v36 against frstrtr/p2pool-merged-v36. Unlike dgb (an explicit
22+
// base_version constructor parameter over a terminal-v35 oracle), LTC DERIVES
23+
// the minted baseline inline as `current_version = target_version_ - 1`
24+
// (auto_ratchet.hpp get_share_version), so the VOTING bootstrap mints 35 and
25+
// the CONFIRMED bootstrap mints 36 with only the 2-arg ctor. LTC is also
26+
// SINGLE-ALGO (Scrypt PoW), so this port carries NO multi-algo disposition case
27+
// (dgb's C5 scrypt-only-by-continuity) -- every LTC share credits Scrypt work.
28+
//
29+
// Consensus surface is NOT mutated. C3 pins the live 60%-by-WORK gate via a
30+
// VERBATIM replica of the inline tail-guard expression in
31+
// AutoRatchet::get_share_version (auto_ratchet.hpp:171), localising the gate the
32+
// same non-circular way the dgb sim/tail-guard KATs do. This target joins the
33+
// existing `share_test` executable (already on the build.yml --target allowlist)
34+
// so it cannot become a #143 NOT_BUILT sentinel. Test-only / ltc-tree-local.
35+
36+
#include <gtest/gtest.h>
37+
38+
#include <impl/ltc/auto_ratchet.hpp> // also pulls share_tracker.hpp + config_pool.hpp
39+
#include <core/uint256.hpp> // uint288
40+
41+
#include <cstdint>
42+
#include <cstdio>
43+
#include <fstream>
44+
#include <string>
45+
#include <vector>
46+
#include <unistd.h> // getpid (restart-persistence temp file)
47+
48+
using ltc::AutoRatchet;
49+
using ltc::RatchetState;
50+
51+
namespace {
52+
53+
// LTC oracle: v35 -> v36 reference transition. The ratchet mints the v35
54+
// baseline while VOTING and votes for the target. LTC derives the baseline as
55+
// target-1 inline (no base_version ctor param), so BASE == TARGET - 1 here too.
56+
constexpr int64_t LTC_TARGET_VERSION = 36;
57+
constexpr int64_t LTC_BASE_VERSION = LTC_TARGET_VERSION - 1; // 35
58+
59+
// Verbatim replica of the LIVE inline work-weighted tail guard in
60+
// AutoRatchet::get_share_version (auto_ratchet.hpp:171):
61+
// tail_ok = !(tail_target * uint32_t(100) < tail_total * uint32_t(SWITCH));
62+
// Localises the 60%-by-WORK accept gate so C3 pins it WITHOUT driving the
63+
// consensus path or importing a lifted SSOT. uint288 = work-weight accumulator.
64+
bool inline_tail_ok(const uint288& target, const uint288& total,
65+
int thr = AutoRatchet::SWITCH_THRESHOLD)
66+
{
67+
return !((target * static_cast<uint32_t>(100)) <
68+
(total * static_cast<uint32_t>(thr)));
69+
}
70+
71+
// 95%-by-COUNT activation predicate (flat head-count), mirroring the VOTING ->
72+
// ACTIVATED branch's `vote_pct >= ACTIVATION_THRESHOLD` test.
73+
bool count_activates(int target_votes, int total)
74+
{
75+
if (total == 0) return false;
76+
return (target_votes * 100) / total >= AutoRatchet::ACTIVATION_THRESHOLD;
77+
}
78+
79+
// EFFECTIVE activation = mint-side count gate AND work-weighted accept gate.
80+
// This is the AND the live state machine enforces: VOTING only advances when
81+
// BOTH the 95%-by-count window AND the 60%-by-work tail guard hold.
82+
bool effective_activation(int votes, int total,
83+
const uint288& w_target, const uint288& w_total)
84+
{
85+
return count_activates(votes, total) && inline_tail_ok(w_target, w_total);
86+
}
87+
88+
// One staged-migration sample: after stage k, `votes` of `total` miners signal
89+
// V36, carrying `w_target` of `w_total` work.
90+
struct Stage { int votes; int total; uint288 w_target; uint288 w_total; };
91+
92+
} // namespace
93+
94+
// ---------------------------------------------------------------------------
95+
// Anchor: AutoRatchet thresholds match the canonical p2pool ratchet constants
96+
// (bucket-2 v36-native shared structure, standardized cross-coin).
97+
// ---------------------------------------------------------------------------
98+
TEST(LTC_AutoRatchetSim, ThresholdsMatchCanonical)
99+
{
100+
EXPECT_EQ(AutoRatchet::ACTIVATION_THRESHOLD, 95);
101+
EXPECT_EQ(AutoRatchet::DEACTIVATION_THRESHOLD, 50);
102+
EXPECT_EQ(AutoRatchet::CONFIRMATION_MULTIPLIER, 2);
103+
EXPECT_EQ(AutoRatchet::SWITCH_THRESHOLD, 60);
104+
}
105+
106+
// ---------------------------------------------------------------------------
107+
// C2 — VOTING node votes V36 but MINTS the LTC baseline (v35 = target-1) on an
108+
// empty tracker. A freshly-started node must not skip ahead of the network.
109+
// ---------------------------------------------------------------------------
110+
TEST(LTC_AutoRatchetSim, C2_VotingMintsBaselineWhileVoting)
111+
{
112+
AutoRatchet ar("", LTC_TARGET_VERSION);
113+
ltc::ShareTracker tracker;
114+
auto [mint, vote] = ar.get_share_version(tracker, uint256{}); // null best hash
115+
EXPECT_EQ(mint, LTC_BASE_VERSION); // baseline = v35 (target-1), NOT 36
116+
EXPECT_EQ(vote, LTC_TARGET_VERSION); // always vote for the target
117+
EXPECT_EQ(ar.state(), RatchetState::VOTING);
118+
EXPECT_EQ(ar.target_version(), LTC_TARGET_VERSION);
119+
}
120+
121+
// ---------------------------------------------------------------------------
122+
// C3 — #288 accept gate is 60%-by-WORK at the boundary.
123+
// ---------------------------------------------------------------------------
124+
TEST(LTC_AutoRatchetSim, C3_AcceptGateIsSixtyPercentByWork)
125+
{
126+
const uint288 total(100); // floor(100*60/100)=60
127+
EXPECT_FALSE(inline_tail_ok(uint288(59), total)); // just under -> hold
128+
EXPECT_TRUE (inline_tail_ok(uint288(60), total)); // at the gate -> pass
129+
EXPECT_TRUE (inline_tail_ok(uint288(61), total));
130+
EXPECT_TRUE (inline_tail_ok(uint288(100), total));
131+
// All-old window never passes (target work = 0).
132+
EXPECT_FALSE(inline_tail_ok(uint288(0), uint288(1000)));
133+
}
134+
135+
// ---------------------------------------------------------------------------
136+
// C3 — the load-bearing #288 property: a 95%-by-COUNT activation can NOT
137+
// outrun the 60%-by-WORK accept gate under heterogeneous hashrate. 19 tiny V36
138+
// voters (work 1 each) + 1 huge V35 miner (work 1000): 95% by count, but only
139+
// ~1.9% by work. If mint activated on count alone it would emit a V36 boundary
140+
// share that every peer (running the work-weighted accept gate) rejects, and
141+
// the crossing wedges. effective_activation must therefore be FALSE.
142+
// ---------------------------------------------------------------------------
143+
TEST(LTC_AutoRatchetSim, C3_MintCannotOutrunAccept)
144+
{
145+
const int votes = 19, total = 20; // 95% by count
146+
const uint288 w_target(19); // 19 * work-1
147+
const uint288 w_total(19 + 1000); // + one huge V35 miner
148+
149+
EXPECT_TRUE (count_activates(votes, total)); // count gate WOULD fire
150+
EXPECT_FALSE(inline_tail_ok(w_target, w_total)); // but work gate holds
151+
EXPECT_FALSE(effective_activation(votes, total, w_target, w_total));
152+
153+
// Homogeneous hashrate: when the 95%-by-count window is ALSO >=60%-by-work,
154+
// the two gates agree and activation is permitted.
155+
EXPECT_TRUE(effective_activation(95, 100, uint288(95), uint288(100)));
156+
}
157+
158+
// ---------------------------------------------------------------------------
159+
// C3/monotonic — staged 1-by-1 miner migration: the work-weighted V36 tally
160+
// advances MONOTONICALLY per stage, and effective activation flips on exactly
161+
// once BOTH the 95%-by-count AND the 60%-by-work gates hold. Mirrors the
162+
// harness's per-stage RESULT-line assertion, rig-free. 5 equal-work miners;
163+
// stage k migrates miner k from V35 to V36.
164+
// ---------------------------------------------------------------------------
165+
TEST(LTC_AutoRatchetSim, C3_StagedTallyAdvancesMonotonically)
166+
{
167+
const uint32_t W = 7; // per-miner work unit
168+
std::vector<Stage> stages;
169+
for (int k = 0; k <= 5; ++k)
170+
stages.push_back(Stage{ k, 5,
171+
uint288(uint32_t(k) * W),
172+
uint288(uint32_t(5) * W) });
173+
174+
uint288 prev_target(0);
175+
bool seen_activation = false;
176+
int first_active_stage = -1;
177+
for (int k = 0; k <= 5; ++k) {
178+
const Stage& s = stages[k];
179+
// Monotonic non-decreasing work tally.
180+
EXPECT_FALSE(s.w_target < prev_target) << "stage=" << k;
181+
prev_target = s.w_target;
182+
183+
bool active = effective_activation(s.votes, s.total, s.w_target, s.w_total);
184+
if (active && !seen_activation) { seen_activation = true; first_active_stage = k; }
185+
// Once active on this monotone ramp, it must stay active.
186+
if (seen_activation) EXPECT_TRUE(active) << "regressed at stage=" << k;
187+
}
188+
// 95%-by-count is only reached at full migration (5/5=100%); 3/5=60% and
189+
// 4/5=80% are below 95. So activation fires at stage 5, not before.
190+
EXPECT_EQ(first_active_stage, 5);
191+
}
192+
193+
// ---------------------------------------------------------------------------
194+
// C4 — ratchet state persists across restart. A CONFIRMED state file
195+
// reconstructs as CONFIRMED, and a CONFIRMED bootstrap mints the target. The
196+
// baseline is derived from target_version, not the JSON, so a restarted
197+
// CONFIRMED node mints V36 regardless of its v35 base.
198+
// ---------------------------------------------------------------------------
199+
TEST(LTC_AutoRatchetSim, C4_StatePersistsAcrossRestart)
200+
{
201+
const std::string path = std::string("/tmp/ltc_autoratchet_sim_kat_") +
202+
std::to_string(::getpid()) + ".json";
203+
std::remove(path.c_str());
204+
{
205+
nlohmann::json j;
206+
j["state"] = "confirmed";
207+
j["activated_at"] = 1;
208+
j["activated_height"] = 2;
209+
j["confirmed_at"] = 3;
210+
j["confirm_count"] = 4;
211+
j["target_version"] = LTC_TARGET_VERSION;
212+
std::ofstream f(path);
213+
f << j.dump(2);
214+
}
215+
AutoRatchet ar(path, LTC_TARGET_VERSION);
216+
EXPECT_EQ(ar.state(), RatchetState::CONFIRMED);
217+
218+
ltc::ShareTracker tracker;
219+
auto [mint, vote] = ar.get_share_version(tracker, uint256{});
220+
EXPECT_EQ(mint, LTC_TARGET_VERSION); // CONFIRMED bootstrap mints the target
221+
EXPECT_EQ(vote, LTC_TARGET_VERSION);
222+
std::remove(path.c_str());
223+
}
224+
225+
// ---------------------------------------------------------------------------
226+
// C4/baseline — a fresh (VOTING) node with NO state file mints the LTC v35
227+
// baseline on bootstrap, the mirror of the CONFIRMED case: persistence is what
228+
// distinguishes "produce baseline" from "produce target" across a restart.
229+
// ---------------------------------------------------------------------------
230+
TEST(LTC_AutoRatchetSim, C4_FreshNodeMintsBaselineOnBootstrap)
231+
{
232+
AutoRatchet ar("", LTC_TARGET_VERSION);
233+
EXPECT_EQ(ar.state(), RatchetState::VOTING);
234+
ltc::ShareTracker tracker;
235+
auto [mint, vote] = ar.get_share_version(tracker, uint256{});
236+
EXPECT_EQ(mint, LTC_BASE_VERSION);
237+
EXPECT_EQ(vote, LTC_TARGET_VERSION);
238+
}

0 commit comments

Comments
 (0)