From 0c8a83a30ead83d50d76b1b72f0c2aa0f40a60fb Mon Sep 17 00:00:00 2001 From: frstrtr Date: Sun, 12 Jul 2026 11:09:54 +0000 Subject: [PATCH 1/2] test(ltc): desired-version tally KAT via real ShareTracker API LTC is the V36 reference impl but, unlike dgb, does not modularize the desired-version tally into src/impl/ltc/coin/. The accumulation lives inline in ShareTracker::get_desired_version_weights (the consensus 60%-by-work switch gate input, share_check step 2 / AutoRatchet tail guard) and its flat-count diagnostic sibling get_desired_version_counts (share_tracker.hpp:2150/:2179). This KAT drives the tally through the real chain-walk API: it builds a resolved ltc::ShareTracker of V36 MergedMiningShare and asserts both per-version maps against the p2pool data.py:2651 oracle (res[dv] += target_to_average_attempts(share.target)). Non-circular: a work-definition anchor pins chain::target_to_average_attempts to hand-derived constants over 2^k-1 targets; the tracker-tally expectations are re-derived by an independent accumulation applying the same production primitive to the input bits, not read from the maps under test. Covers empty/absent tip, single/repeated/multi-version bucketing, the work-weight-inverts-flat-count case, and the lookbehind clamp. Additive: joins the existing share_test target; no production code touched. --- src/impl/ltc/test/CMakeLists.txt | 2 +- .../ltc/test/desired_version_tally_test.cpp | 209 ++++++++++++++++++ 2 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 src/impl/ltc/test/desired_version_tally_test.cpp diff --git a/src/impl/ltc/test/CMakeLists.txt b/src/impl/ltc/test/CMakeLists.txt index dfd0935ef..3b676a34c 100644 --- a/src/impl/ltc/test/CMakeLists.txt +++ b/src/impl/ltc/test/CMakeLists.txt @@ -1,5 +1,5 @@ if (BUILD_TESTING AND GTest_FOUND) - add_executable(share_test share_test.cpp f11_donation_invariance_test.cpp emergency_decay_overflow_test.cpp wirecompat_runtime_test.cpp) + 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) target_link_libraries(share_test PRIVATE GTest::gtest_main GTest::gtest core ltc diff --git a/src/impl/ltc/test/desired_version_tally_test.cpp b/src/impl/ltc/test/desired_version_tally_test.cpp new file mode 100644 index 000000000..96cdbd237 --- /dev/null +++ b/src/impl/ltc/test/desired_version_tally_test.cpp @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// LTC desired-version tally -- version->work (consensus) / version->count +// (diagnostic) accumulation KAT, exercised through LTC's REAL ShareTracker API. +// +// FENCED, additive (no production code touched this slice). LTC is the V36 +// reference impl but -- unlike dgb -- does NOT modularize this tally into +// src/impl/ltc/coin/; the accumulation lives inline in +// ShareTracker::get_desired_version_weights() (the CONSENSUS 60%-by-work switch +// gate input, share_check step 2 / #288 AutoRatchet tail guard) and its +// flat-count diagnostic sibling ShareTracker::get_desired_version_counts() +// (share_tracker.hpp:2151 / :2181). This KAT therefore drives the tally through +// the real chain-walk API rather than a lifted header -- it builds a resolved +// ltc::ShareTracker of ltc::MergedMiningShare (V36) and asserts the two +// per-version maps against the p2pool oracle. +// +// Oracle: p2pool data.py:2651 get_desired_version_counts is ALREADY +// work-weighted -- each share contributes target_to_average_attempts(share.target), +// NOT 1: +// res = {} +// for share in tracker.get_chain(best_share_hash, dist): +// res[share.desired_version] = res.get(share.desired_version, 0) \ +// + bitcoin_data.target_to_average_attempts(share.target) +// return res +// c2pool splits this into two accessors: get_desired_version_weights (the true +// oracle match -- per-share weight = ShareIndex::work = chain:: +// target_to_average_attempts(chain::bits_to_target(m_bits)), share.hpp:304, the +// CONSENSUS gate input) and get_desired_version_counts (a flat occurrence count, +// diagnostics-only, NEVER the gate -- see the share_tracker.hpp comment / #288). +// +// NON-CIRCULAR: the work-definition anchor pins chain::target_to_average_attempts +// to hand-derived constants over 2^k-1 targets (ttaa(2^k-1) = 2^256/2^k = +// 2^(256-k)); the tracker-tally expectations are then re-derived from first +// principles by an INDEPENDENT accumulation loop applying that same production +// primitive to the input bits -- NOT read from the maps under test. share_tracker.hpp +// is NOT rewired. This target joins the existing `share_test` executable (already +// on the build.yml --target allowlist), so it cannot become a #143 NOT_BUILT sentinel. + +#include +#include +#include +#include + +#include + +#include // uint288 +#include // chain::bits_to_target, chain::target_to_average_attempts +#include +#include + +namespace { + +uint288 u(uint64_t n) { return uint288(n); } + +// A short hex tail -> uint256 (zero-padded to 64 nibbles), matching the dgb +// share_test hx() convention so share identities are compact and readable. +uint256 hx(const std::string& tail) { + uint256 v; + v.SetHex(std::string(64 - tail.size(), '0') + tail); + return v; +} + +// One synthetic V36 share's tally-relevant inputs: its desired_version and the +// compact bits that (via ShareIndex::work) fix its work weight. +struct In { uint64_t dv; uint32_t bits; }; + +// Build a resolved ltc::ShareTracker from `shares` (share i's parent is i-1; +// share 0 has a null parent = resolved chain genesis) and return the tip hash. +uint256 seed_tracker(ltc::ShareTracker& tracker, const std::vector& shares) { + uint256 prev; + prev.SetNull(); + uint256 tip; + for (size_t i = 0; i < shares.size(); ++i) { + char buf[16]; + std::snprintf(buf, sizeof(buf), "%zx", static_cast(0x1c70 + i)); + uint256 h = hx(buf); + auto* sh = new ltc::MergedMiningShare(); + sh->m_hash = h; + if (i == 0) sh->m_prev_hash.SetNull(); else sh->m_prev_hash = prev; + sh->m_desired_version = shares[i].dv; + sh->m_bits = shares[i].bits; + sh->m_max_bits = shares[i].bits; + ltc::ShareType st; + st = sh; + tracker.add(st); + prev = h; + tip = h; + } + return tip; +} + +// Independent (first-principles) oracle: the flat count and the work-weighted +// sum per desired_version, computed WITHOUT touching the maps under test. +void expected_maps(const std::vector& shares, + std::map& counts, + std::map& weights) { + for (const auto& in : shares) { + counts[in.dv] += 1; + const uint288 w = chain::target_to_average_attempts(chain::bits_to_target(in.bits)); + weights[in.dv] = weights[in.dv] + w; + } +} + +} // namespace + +// --- work-definition anchor: ttaa(2^k-1) = 2^(256-k), hand-derived ---------- +// t = 2^255-1 -> ttaa = 2 ; t = 2^254-1 -> ttaa = 4 ; t = 2^253-1 -> ttaa = 8 +// Pins the production work primitive NON-CIRCULARLY, so the tracker-tally +// expectations below (which re-apply this primitive to the input bits) rest on +// a hand-verified base rather than the code under test. +TEST(LtcDesiredVersionTally, WorkPrimitiveMatchesOracleAttempts) { + uint256 t255, t254, t253; + t255.SetHex("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); // 2^255-1 + t254.SetHex("3fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); // 2^254-1 + t253.SetHex("1fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); // 2^253-1 + EXPECT_EQ(chain::target_to_average_attempts(t255), u(2)); + EXPECT_EQ(chain::target_to_average_attempts(t254), u(4)); + EXPECT_EQ(chain::target_to_average_attempts(t253), u(8)); +} + +// --- empty / absent tip -> empty maps (the actual<=0 / !contains early-return) -- +TEST(LtcDesiredVersionTally, EmptyOrAbsentTipYieldsEmptyMaps) { + ltc::ShareTracker tracker; + // Absent tip: chain.contains(hash) == false -> empty. + EXPECT_TRUE(tracker.get_desired_version_counts(hx("deadbeef"), 1000).empty()); + EXPECT_TRUE(tracker.get_desired_version_weights(hx("deadbeef"), 1000).empty()); + + // Present tip but lookbehind<=0 -> min(lookbehind,height)<=0 -> empty. + std::vector shares = {{36, 0x1e0fffff}, {36, 0x1e0fffff}}; + const uint256 tip = seed_tracker(tracker, shares); + EXPECT_TRUE(tracker.get_desired_version_counts(tip, 0).empty()); + EXPECT_TRUE(tracker.get_desired_version_weights(tip, 0).empty()); +} + +// --- single share: one bucket, weight == its work, count == 1 --------------- +TEST(LtcDesiredVersionTally, SingleShareSingleBucket) { + ltc::ShareTracker tracker; + std::vector shares = {{36, 0x1e0fffff}}; + const uint256 tip = seed_tracker(tracker, shares); + + const uint288 w36 = chain::target_to_average_attempts(chain::bits_to_target(0x1e0fffff)); + + auto c = tracker.get_desired_version_counts(tip, 1000); + ASSERT_EQ(c.size(), 1u); + EXPECT_EQ(c.at(36u), 1); + + auto w = tracker.get_desired_version_weights(tip, 1000); + ASSERT_EQ(w.size(), 1u); + EXPECT_EQ(w.at(36u), w36); +} + +// --- same version, many shares: weights sum, count increments --------------- +TEST(LtcDesiredVersionTally, SameVersionAccumulates) { + ltc::ShareTracker tracker; + std::vector shares = {{36, 0x1e0fffff}, {36, 0x1e07ffff}, {36, 0x1d00ffff}}; + const uint256 tip = seed_tracker(tracker, shares); + + std::map want_counts; + std::map want_weights; + expected_maps(shares, want_counts, want_weights); + + EXPECT_EQ(tracker.get_desired_version_counts(tip, 1000), want_counts); + EXPECT_EQ(tracker.get_desired_version_weights(tip, 1000), want_weights); + // The three works genuinely differ (non-trivial sum), not a degenerate 3x same. + EXPECT_EQ(want_counts.at(36u), 3); +} + +// --- multiple versions bucketed independently; work-weight can INVERT count -- +// The exact case where a flat count (36 leads 3:1) inverts under work-weighting +// (the single hard dv=35 share outweighs the three easy dv=36 shares). +TEST(LtcDesiredVersionTally, MultipleVersionsBucketedAndWorkInverts) { + ltc::ShareTracker tracker; + std::vector shares = {{36, 0x1e0fffff}, {36, 0x1e0fffff}, + {35, 0x1d00ffff}, {36, 0x1e0fffff}}; + const uint256 tip = seed_tracker(tracker, shares); + + std::map want_counts; + std::map want_weights; + expected_maps(shares, want_counts, want_weights); + + auto c = tracker.get_desired_version_counts(tip, 1000); + auto w = tracker.get_desired_version_weights(tip, 1000); + EXPECT_EQ(c, want_counts); + EXPECT_EQ(w, want_weights); + + // Flat count: 36 leads 3:1 ... + ASSERT_EQ(c.at(36u), 3); + ASSERT_EQ(c.at(35u), 1); + // ... but work-weighting inverts it: the lone hard dv=35 share outweighs the + // three easy dv=36 shares (0x1d00ffff is far more work than 0x1e0fffff). + EXPECT_GT(w.at(35u), w.at(36u)); +} + +// --- lookbehind clamp stays inline & correct: a window shorter than the chain +// tallies ONLY the clamped tail --------------------------------------- +TEST(LtcDesiredVersionTally, LookbehindClampsToTail) { + ltc::ShareTracker tracker; + // g(dv35) <- a(dv36) <- b(dv36): a lookbehind of 2 sees only {a,b} = 2x dv36. + std::vector shares = {{35, 0x1e0fffff}, {36, 0x1e0fffff}, {36, 0x1e0fffff}}; + const uint256 tip = seed_tracker(tracker, shares); + + auto c2 = tracker.get_desired_version_counts(tip, 2); + ASSERT_EQ(c2.size(), 1u); + EXPECT_EQ(c2.at(36u), 2); // dv35 genesis is outside the 2-deep tail + + auto w2 = tracker.get_desired_version_weights(tip, 2); + ASSERT_EQ(w2.size(), 1u); + const uint288 w36 = chain::target_to_average_attempts(chain::bits_to_target(0x1e0fffff)); + EXPECT_EQ(w2.at(36u), w36 + w36); +} From 5fb702b275950ccc7087182886ec32018ed9cb94 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Sun, 12 Jul 2026 11:27:19 +0000 Subject: [PATCH 2/2] ltc/test: AutoRatchet staged-migration SIM KAT (G2 sim/tail-guard) Port the dgb AutoRatchet sim/tail-guard coverage to LTC, adapted to LTCs 2-arg ctor (baseline derived inline as target-1) and single-algo Scrypt posture (drops dgbs multi-algo C5). 7 cases through the real ltc::AutoRatchet + ltc::ShareTracker API: thresholds, C2 VOTING-mints-v35-baseline, C3 60%-by-work gate + mint-cannot-outrun-accept + monotonic staged tally, C4 CONFIRMED restart-persistence. Arms ltc_g2_crossing_staged_migration harness rig-free. Joins share_test (build allowlist). FENCED, additive. --- src/impl/ltc/test/CMakeLists.txt | 2 +- src/impl/ltc/test/auto_ratchet_sim_test.cpp | 238 ++++++++++++++++++++ 2 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 src/impl/ltc/test/auto_ratchet_sim_test.cpp diff --git a/src/impl/ltc/test/CMakeLists.txt b/src/impl/ltc/test/CMakeLists.txt index 3b676a34c..6ac5efb2d 100644 --- a/src/impl/ltc/test/CMakeLists.txt +++ b/src/impl/ltc/test/CMakeLists.txt @@ -1,5 +1,5 @@ if (BUILD_TESTING AND GTest_FOUND) - 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) + 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) target_link_libraries(share_test PRIVATE GTest::gtest_main GTest::gtest core ltc diff --git a/src/impl/ltc/test/auto_ratchet_sim_test.cpp b/src/impl/ltc/test/auto_ratchet_sim_test.cpp new file mode 100644 index 000000000..4978b442c --- /dev/null +++ b/src/impl/ltc/test/auto_ratchet_sim_test.cpp @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// ltc_auto_ratchet_sim_test: FENCED, additive rig-free SIM KAT that arms the +// LTC G2 staged-migration harness +// (scripts/ltc_g2_crossing_staged_migration_harness.sh) crossing rows WITHOUT +// a Scrypt rig or a live litecoind parent -- the C++ counterpart of that +// harness's staged-vote path. It exercises the AutoRatchet staged-gate LOGIC +// over hand-derived oracle votes: +// +// C2 VOTING mints the LTC BASELINE (v35) while voting V36 (bootstrap, empty +// chain) -- a freshly-started node must not skip ahead of the network +// C3 #288 accept gate: 60%-by-WORK, and a 95%-by-COUNT activation can NOT +// outrun it (mint-cannot-outrun-accept) -- the property that keeps a +// minted V36 boundary share from being rejected by every peer +// C3 staged 1-by-1 miner migration: the work-weighted V36 tally advances +// MONOTONICALLY and effective activation flips on exactly once BOTH the +// 95%-by-count AND the 60%-by-work gates hold (the crossing-dynamics proof) +// C4 ratchet state persists across restart (CONFIRMED survives reconstruct; +// a fresh VOTING node mints the v35 baseline on bootstrap) +// +// LTC ORACLE PIN: LTC is the V36 REFERENCE impl and transitions its own +// v35 -> v36 against frstrtr/p2pool-merged-v36. Unlike dgb (an explicit +// base_version constructor parameter over a terminal-v35 oracle), LTC DERIVES +// the minted baseline inline as `current_version = target_version_ - 1` +// (auto_ratchet.hpp get_share_version), so the VOTING bootstrap mints 35 and +// the CONFIRMED bootstrap mints 36 with only the 2-arg ctor. LTC is also +// SINGLE-ALGO (Scrypt PoW), so this port carries NO multi-algo disposition case +// (dgb's C5 scrypt-only-by-continuity) -- every LTC share credits Scrypt work. +// +// Consensus surface is NOT mutated. C3 pins the live 60%-by-WORK gate via a +// VERBATIM replica of the inline tail-guard expression in +// AutoRatchet::get_share_version (auto_ratchet.hpp:171), localising the gate the +// same non-circular way the dgb sim/tail-guard KATs do. This target joins the +// existing `share_test` executable (already on the build.yml --target allowlist) +// so it cannot become a #143 NOT_BUILT sentinel. Test-only / ltc-tree-local. + +#include + +#include // also pulls share_tracker.hpp + config_pool.hpp +#include // uint288 + +#include +#include +#include +#include +#include +#include // getpid (restart-persistence temp file) + +using ltc::AutoRatchet; +using ltc::RatchetState; + +namespace { + +// LTC oracle: v35 -> v36 reference transition. The ratchet mints the v35 +// baseline while VOTING and votes for the target. LTC derives the baseline as +// target-1 inline (no base_version ctor param), so BASE == TARGET - 1 here too. +constexpr int64_t LTC_TARGET_VERSION = 36; +constexpr int64_t LTC_BASE_VERSION = LTC_TARGET_VERSION - 1; // 35 + +// Verbatim replica of the LIVE inline work-weighted tail guard in +// AutoRatchet::get_share_version (auto_ratchet.hpp:171): +// tail_ok = !(tail_target * uint32_t(100) < tail_total * uint32_t(SWITCH)); +// Localises the 60%-by-WORK accept gate so C3 pins it WITHOUT driving the +// consensus path or importing a lifted SSOT. uint288 = work-weight accumulator. +bool inline_tail_ok(const uint288& target, const uint288& total, + int thr = AutoRatchet::SWITCH_THRESHOLD) +{ + return !((target * static_cast(100)) < + (total * static_cast(thr))); +} + +// 95%-by-COUNT activation predicate (flat head-count), mirroring the VOTING -> +// ACTIVATED branch's `vote_pct >= ACTIVATION_THRESHOLD` test. +bool count_activates(int target_votes, int total) +{ + if (total == 0) return false; + return (target_votes * 100) / total >= AutoRatchet::ACTIVATION_THRESHOLD; +} + +// EFFECTIVE activation = mint-side count gate AND work-weighted accept gate. +// This is the AND the live state machine enforces: VOTING only advances when +// BOTH the 95%-by-count window AND the 60%-by-work tail guard hold. +bool effective_activation(int votes, int total, + const uint288& w_target, const uint288& w_total) +{ + return count_activates(votes, total) && inline_tail_ok(w_target, w_total); +} + +// One staged-migration sample: after stage k, `votes` of `total` miners signal +// V36, carrying `w_target` of `w_total` work. +struct Stage { int votes; int total; uint288 w_target; uint288 w_total; }; + +} // namespace + +// --------------------------------------------------------------------------- +// Anchor: AutoRatchet thresholds match the canonical p2pool ratchet constants +// (bucket-2 v36-native shared structure, standardized cross-coin). +// --------------------------------------------------------------------------- +TEST(LTC_AutoRatchetSim, ThresholdsMatchCanonical) +{ + EXPECT_EQ(AutoRatchet::ACTIVATION_THRESHOLD, 95); + EXPECT_EQ(AutoRatchet::DEACTIVATION_THRESHOLD, 50); + EXPECT_EQ(AutoRatchet::CONFIRMATION_MULTIPLIER, 2); + EXPECT_EQ(AutoRatchet::SWITCH_THRESHOLD, 60); +} + +// --------------------------------------------------------------------------- +// C2 — VOTING node votes V36 but MINTS the LTC baseline (v35 = target-1) on an +// empty tracker. A freshly-started node must not skip ahead of the network. +// --------------------------------------------------------------------------- +TEST(LTC_AutoRatchetSim, C2_VotingMintsBaselineWhileVoting) +{ + AutoRatchet ar("", LTC_TARGET_VERSION); + ltc::ShareTracker tracker; + auto [mint, vote] = ar.get_share_version(tracker, uint256{}); // null best hash + EXPECT_EQ(mint, LTC_BASE_VERSION); // baseline = v35 (target-1), NOT 36 + EXPECT_EQ(vote, LTC_TARGET_VERSION); // always vote for the target + EXPECT_EQ(ar.state(), RatchetState::VOTING); + EXPECT_EQ(ar.target_version(), LTC_TARGET_VERSION); +} + +// --------------------------------------------------------------------------- +// C3 — #288 accept gate is 60%-by-WORK at the boundary. +// --------------------------------------------------------------------------- +TEST(LTC_AutoRatchetSim, C3_AcceptGateIsSixtyPercentByWork) +{ + const uint288 total(100); // floor(100*60/100)=60 + EXPECT_FALSE(inline_tail_ok(uint288(59), total)); // just under -> hold + EXPECT_TRUE (inline_tail_ok(uint288(60), total)); // at the gate -> pass + EXPECT_TRUE (inline_tail_ok(uint288(61), total)); + EXPECT_TRUE (inline_tail_ok(uint288(100), total)); + // All-old window never passes (target work = 0). + EXPECT_FALSE(inline_tail_ok(uint288(0), uint288(1000))); +} + +// --------------------------------------------------------------------------- +// C3 — the load-bearing #288 property: a 95%-by-COUNT activation can NOT +// outrun the 60%-by-WORK accept gate under heterogeneous hashrate. 19 tiny V36 +// voters (work 1 each) + 1 huge V35 miner (work 1000): 95% by count, but only +// ~1.9% by work. If mint activated on count alone it would emit a V36 boundary +// share that every peer (running the work-weighted accept gate) rejects, and +// the crossing wedges. effective_activation must therefore be FALSE. +// --------------------------------------------------------------------------- +TEST(LTC_AutoRatchetSim, C3_MintCannotOutrunAccept) +{ + const int votes = 19, total = 20; // 95% by count + const uint288 w_target(19); // 19 * work-1 + const uint288 w_total(19 + 1000); // + one huge V35 miner + + EXPECT_TRUE (count_activates(votes, total)); // count gate WOULD fire + EXPECT_FALSE(inline_tail_ok(w_target, w_total)); // but work gate holds + EXPECT_FALSE(effective_activation(votes, total, w_target, w_total)); + + // Homogeneous hashrate: when the 95%-by-count window is ALSO >=60%-by-work, + // the two gates agree and activation is permitted. + EXPECT_TRUE(effective_activation(95, 100, uint288(95), uint288(100))); +} + +// --------------------------------------------------------------------------- +// C3/monotonic — staged 1-by-1 miner migration: the work-weighted V36 tally +// advances MONOTONICALLY per stage, and effective activation flips on exactly +// once BOTH the 95%-by-count AND the 60%-by-work gates hold. Mirrors the +// harness's per-stage RESULT-line assertion, rig-free. 5 equal-work miners; +// stage k migrates miner k from V35 to V36. +// --------------------------------------------------------------------------- +TEST(LTC_AutoRatchetSim, C3_StagedTallyAdvancesMonotonically) +{ + const uint32_t W = 7; // per-miner work unit + std::vector stages; + for (int k = 0; k <= 5; ++k) + stages.push_back(Stage{ k, 5, + uint288(uint32_t(k) * W), + uint288(uint32_t(5) * W) }); + + uint288 prev_target(0); + bool seen_activation = false; + int first_active_stage = -1; + for (int k = 0; k <= 5; ++k) { + const Stage& s = stages[k]; + // Monotonic non-decreasing work tally. + EXPECT_FALSE(s.w_target < prev_target) << "stage=" << k; + prev_target = s.w_target; + + bool active = effective_activation(s.votes, s.total, s.w_target, s.w_total); + if (active && !seen_activation) { seen_activation = true; first_active_stage = k; } + // Once active on this monotone ramp, it must stay active. + if (seen_activation) EXPECT_TRUE(active) << "regressed at stage=" << k; + } + // 95%-by-count is only reached at full migration (5/5=100%); 3/5=60% and + // 4/5=80% are below 95. So activation fires at stage 5, not before. + EXPECT_EQ(first_active_stage, 5); +} + +// --------------------------------------------------------------------------- +// C4 — ratchet state persists across restart. A CONFIRMED state file +// reconstructs as CONFIRMED, and a CONFIRMED bootstrap mints the target. The +// baseline is derived from target_version, not the JSON, so a restarted +// CONFIRMED node mints V36 regardless of its v35 base. +// --------------------------------------------------------------------------- +TEST(LTC_AutoRatchetSim, C4_StatePersistsAcrossRestart) +{ + const std::string path = std::string("/tmp/ltc_autoratchet_sim_kat_") + + std::to_string(::getpid()) + ".json"; + std::remove(path.c_str()); + { + nlohmann::json j; + j["state"] = "confirmed"; + j["activated_at"] = 1; + j["activated_height"] = 2; + j["confirmed_at"] = 3; + j["confirm_count"] = 4; + j["target_version"] = LTC_TARGET_VERSION; + std::ofstream f(path); + f << j.dump(2); + } + AutoRatchet ar(path, LTC_TARGET_VERSION); + EXPECT_EQ(ar.state(), RatchetState::CONFIRMED); + + ltc::ShareTracker tracker; + auto [mint, vote] = ar.get_share_version(tracker, uint256{}); + EXPECT_EQ(mint, LTC_TARGET_VERSION); // CONFIRMED bootstrap mints the target + EXPECT_EQ(vote, LTC_TARGET_VERSION); + std::remove(path.c_str()); +} + +// --------------------------------------------------------------------------- +// C4/baseline — a fresh (VOTING) node with NO state file mints the LTC v35 +// baseline on bootstrap, the mirror of the CONFIRMED case: persistence is what +// distinguishes "produce baseline" from "produce target" across a restart. +// --------------------------------------------------------------------------- +TEST(LTC_AutoRatchetSim, C4_FreshNodeMintsBaselineOnBootstrap) +{ + AutoRatchet ar("", LTC_TARGET_VERSION); + EXPECT_EQ(ar.state(), RatchetState::VOTING); + ltc::ShareTracker tracker; + auto [mint, vote] = ar.get_share_version(tracker, uint256{}); + EXPECT_EQ(mint, LTC_BASE_VERSION); + EXPECT_EQ(vote, LTC_TARGET_VERSION); +}