|
| 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