|
| 1 | +// btc_auto_ratchet_sim_test: FENCED, additive rig-free SIM KAT that arms the |
| 2 | +// BTC G2 staged-migration harness (scripts/btc_g2_ratchet_staged_migration_harness.sh) |
| 3 | +// C2/C3/C4 rows WITHOUT a SHA256d rig — the mirror of DGB #427's --sim-votes. |
| 4 | +// It exercises the AutoRatchet staged-gate LOGIC over hand-derived oracle votes: |
| 5 | +// |
| 6 | +// C2 VOTING mints the V35 baseline while voting V36 (bootstrap, empty chain) |
| 7 | +// C3 #288 accept gate: 60%-by-WORK, and a 95%-by-COUNT activation can NOT |
| 8 | +// outrun it (mint-cannot-outrun-accept) — the property that keeps a |
| 9 | +// minted V36 boundary share from being rejected by every peer |
| 10 | +// C4 ratchet state persists across restart (CONFIRMED survives reconstruct) |
| 11 | +// |
| 12 | +// Consensus surface is NOT mutated. C3 pins the live 60%-by-WORK gate via a |
| 13 | +// VERBATIM replica of the inline tail-guard expression in |
| 14 | +// AutoRatchet::get_share_version (auto_ratchet.hpp), localising the gate the |
| 15 | +// same non-circular way DGB's auto_ratchet_tail_guard_test does (BTC keeps the |
| 16 | +// guard inline, no lifted SSOT). Test-only / btc-tree-local. |
| 17 | + |
| 18 | +#include <gtest/gtest.h> |
| 19 | + |
| 20 | +#include <impl/btc/auto_ratchet.hpp> // also pulls share_tracker.hpp + nlohmann/json |
| 21 | +#include <core/uint256.hpp> |
| 22 | +#include <core/pack_types.hpp> |
| 23 | + |
| 24 | +#include <cstdint> |
| 25 | +#include <cstdio> |
| 26 | +#include <fstream> |
| 27 | +#include <string> |
| 28 | +#include <vector> |
| 29 | +#include <unistd.h> // getpid (restart-persistence temp file) |
| 30 | + |
| 31 | +using btc::AutoRatchet; |
| 32 | +using btc::RatchetState; |
| 33 | + |
| 34 | +namespace { |
| 35 | + |
| 36 | +// Verbatim replica of the LIVE inline tail-guard in |
| 37 | +// AutoRatchet::get_share_version: tail_ok = !(target*100 < total*SWITCH). |
| 38 | +// Localises the 60%-by-WORK accept gate so C3 pins it WITHOUT driving the |
| 39 | +// consensus path or importing a lifted SSOT. |
| 40 | +bool inline_tail_ok(const uint288& target, const uint288& total, |
| 41 | + int thr = AutoRatchet::SWITCH_THRESHOLD) |
| 42 | +{ |
| 43 | + return !((target * static_cast<uint32_t>(100)) < |
| 44 | + (total * static_cast<uint32_t>(thr))); |
| 45 | +} |
| 46 | + |
| 47 | +// 95%-by-COUNT activation predicate (flat head-count), mirroring the VOTING -> |
| 48 | +// ACTIVATED branch's `vote_pct >= ACTIVATION_THRESHOLD` test. |
| 49 | +bool count_activates(int target_votes, int total) |
| 50 | +{ |
| 51 | + if (total == 0) return false; |
| 52 | + return (target_votes * 100) / total >= AutoRatchet::ACTIVATION_THRESHOLD; |
| 53 | +} |
| 54 | + |
| 55 | +// EFFECTIVE activation = mint-side count gate AND work-weighted accept gate. |
| 56 | +// This is the AND the live state machine enforces: VOTING only advances when |
| 57 | +// BOTH the 95%-by-count window AND the 60%-by-work tail guard hold. |
| 58 | +bool effective_activation(int votes, int total, |
| 59 | + const uint288& w_target, const uint288& w_total) |
| 60 | +{ |
| 61 | + return count_activates(votes, total) && inline_tail_ok(w_target, w_total); |
| 62 | +} |
| 63 | + |
| 64 | +// One staged-migration sample: after stage k, `votes` of `total` miners signal |
| 65 | +// V36, carrying `w_target` of `w_total` work. |
| 66 | +struct Stage { int votes; int total; uint288 w_target; uint288 w_total; }; |
| 67 | + |
| 68 | +} // namespace |
| 69 | + |
| 70 | +// --------------------------------------------------------------------------- |
| 71 | +// Anchor: AutoRatchet thresholds match the canonical p2pool ratchet constants. |
| 72 | +// --------------------------------------------------------------------------- |
| 73 | +TEST(BTC_AutoRatchetSim, ThresholdsMatchCanonical) |
| 74 | +{ |
| 75 | + EXPECT_EQ(AutoRatchet::ACTIVATION_THRESHOLD, 95); |
| 76 | + EXPECT_EQ(AutoRatchet::DEACTIVATION_THRESHOLD, 50); |
| 77 | + EXPECT_EQ(AutoRatchet::CONFIRMATION_MULTIPLIER, 2); |
| 78 | + EXPECT_EQ(AutoRatchet::SWITCH_THRESHOLD, 60); |
| 79 | +} |
| 80 | + |
| 81 | +// --------------------------------------------------------------------------- |
| 82 | +// C2 — VOTING node votes V36 but MINTS the V35 baseline on an empty tracker. |
| 83 | +// A freshly-started node must not skip ahead of the network. |
| 84 | +// --------------------------------------------------------------------------- |
| 85 | +TEST(BTC_AutoRatchetSim, C2_VotingMintsBaselineWhileVoting) |
| 86 | +{ |
| 87 | + AutoRatchet ar("", /*target_version=*/36); |
| 88 | + btc::ShareTracker tracker; |
| 89 | + auto [mint, vote] = ar.get_share_version(tracker, uint256{}); // null best hash |
| 90 | + EXPECT_EQ(mint, 35); // baseline = target-1, NOT 36 |
| 91 | + EXPECT_EQ(vote, 36); // always vote for the target |
| 92 | + EXPECT_EQ(ar.state(), RatchetState::VOTING); |
| 93 | +} |
| 94 | + |
| 95 | +// --------------------------------------------------------------------------- |
| 96 | +// C3 — #288 accept gate is 60%-by-WORK at the boundary. |
| 97 | +// --------------------------------------------------------------------------- |
| 98 | +TEST(BTC_AutoRatchetSim, C3_AcceptGateIsSixtyPercentByWork) |
| 99 | +{ |
| 100 | + const uint288 total(100); // floor(100*60/100)=60 |
| 101 | + EXPECT_FALSE(inline_tail_ok(uint288(59), total)); // just under -> hold |
| 102 | + EXPECT_TRUE (inline_tail_ok(uint288(60), total)); // at the gate -> pass |
| 103 | + EXPECT_TRUE (inline_tail_ok(uint288(61), total)); |
| 104 | + EXPECT_TRUE (inline_tail_ok(uint288(100), total)); |
| 105 | + // All-old window never passes (target work = 0). |
| 106 | + EXPECT_FALSE(inline_tail_ok(uint288(0), uint288(1000))); |
| 107 | +} |
| 108 | + |
| 109 | +// --------------------------------------------------------------------------- |
| 110 | +// C3 — the load-bearing #288 property: a 95%-by-COUNT activation can NOT |
| 111 | +// outrun the 60%-by-WORK accept gate under heterogeneous hashrate. 19 tiny V36 |
| 112 | +// voters (work 1 each) + 1 huge V35 miner (work 1000): 95% by count, but only |
| 113 | +// ~1.9% by work. If mint activated on count alone it would emit a V36 boundary |
| 114 | +// share that every peer (running the work-weighted accept gate) rejects, and |
| 115 | +// the crossing wedges. effective_activation must therefore be FALSE. |
| 116 | +// --------------------------------------------------------------------------- |
| 117 | +TEST(BTC_AutoRatchetSim, C3_MintCannotOutrunAccept) |
| 118 | +{ |
| 119 | + const int votes = 19, total = 20; // 95% by count |
| 120 | + const uint288 w_target(19); // 19 * work-1 |
| 121 | + const uint288 w_total(19 + 1000); // + one huge V35 miner |
| 122 | + |
| 123 | + EXPECT_TRUE (count_activates(votes, total)); // count gate WOULD fire |
| 124 | + EXPECT_FALSE(inline_tail_ok(w_target, w_total)); // but work gate holds |
| 125 | + EXPECT_FALSE(effective_activation(votes, total, w_target, w_total)); |
| 126 | + |
| 127 | + // Homogeneous hashrate: when the 95%-by-count window is ALSO >=60%-by-work, |
| 128 | + // the two gates agree and activation is permitted. |
| 129 | + EXPECT_TRUE(effective_activation(95, 100, uint288(95), uint288(100))); |
| 130 | +} |
| 131 | + |
| 132 | +// --------------------------------------------------------------------------- |
| 133 | +// C3/monotonic — staged 1-by-1 miner migration: the work-weighted V36 tally |
| 134 | +// advances MONOTONICALLY per stage, and effective activation flips on exactly |
| 135 | +// once BOTH the 95%-by-count AND the 60%-by-work gates hold. Mirrors the |
| 136 | +// harness's per-stage RESULT-line assertion, rig-free. 5 equal-work miners; |
| 137 | +// stage k migrates miner k from V35 to V36. |
| 138 | +// --------------------------------------------------------------------------- |
| 139 | +TEST(BTC_AutoRatchetSim, C3_StagedTallyAdvancesMonotonically) |
| 140 | +{ |
| 141 | + const uint32_t W = 7; // per-miner work unit |
| 142 | + std::vector<Stage> stages; |
| 143 | + for (int k = 0; k <= 5; ++k) |
| 144 | + stages.push_back(Stage{ k, 5, |
| 145 | + uint288(uint32_t(k) * W), |
| 146 | + uint288(uint32_t(5) * W) }); |
| 147 | + |
| 148 | + uint288 prev_target(0); |
| 149 | + bool seen_activation = false; |
| 150 | + int first_active_stage = -1; |
| 151 | + for (int k = 0; k <= 5; ++k) { |
| 152 | + const Stage& s = stages[k]; |
| 153 | + // Monotonic non-decreasing work tally. |
| 154 | + EXPECT_FALSE(s.w_target < prev_target) << "stage=" << k; |
| 155 | + prev_target = s.w_target; |
| 156 | + |
| 157 | + bool active = effective_activation(s.votes, s.total, s.w_target, s.w_total); |
| 158 | + if (active && !seen_activation) { seen_activation = true; first_active_stage = k; } |
| 159 | + // Once active on this monotone ramp, it must stay active. |
| 160 | + if (seen_activation) EXPECT_TRUE(active) << "regressed at stage=" << k; |
| 161 | + } |
| 162 | + // 95%-by-count is only reached at full migration (5/5=100%); 3/5=60% and |
| 163 | + // 4/5=80% are below 95. So activation fires at stage 5, not before. |
| 164 | + EXPECT_EQ(first_active_stage, 5); |
| 165 | +} |
| 166 | + |
| 167 | +// --------------------------------------------------------------------------- |
| 168 | +// C4 — ratchet state persists across restart. A CONFIRMED state file |
| 169 | +// reconstructs as CONFIRMED, and a CONFIRMED bootstrap mints the target. |
| 170 | +// --------------------------------------------------------------------------- |
| 171 | +TEST(BTC_AutoRatchetSim, C4_StatePersistsAcrossRestart) |
| 172 | +{ |
| 173 | + const std::string path = std::string("/tmp/btc_autoratchet_sim_kat_") + |
| 174 | + std::to_string(::getpid()) + ".json"; |
| 175 | + std::remove(path.c_str()); |
| 176 | + { |
| 177 | + nlohmann::json j; |
| 178 | + j["state"] = "confirmed"; |
| 179 | + j["activated_at"] = 1; |
| 180 | + j["activated_height"] = 2; |
| 181 | + j["confirmed_at"] = 3; |
| 182 | + j["confirm_count"] = 4; |
| 183 | + j["target_version"] = 36; |
| 184 | + std::ofstream f(path); |
| 185 | + f << j.dump(2); |
| 186 | + } |
| 187 | + AutoRatchet ar(path, /*target_version=*/36); |
| 188 | + EXPECT_EQ(ar.state(), RatchetState::CONFIRMED); |
| 189 | + |
| 190 | + btc::ShareTracker tracker; |
| 191 | + auto [mint, vote] = ar.get_share_version(tracker, uint256{}); |
| 192 | + EXPECT_EQ(mint, 36); // CONFIRMED bootstrap mints the target |
| 193 | + EXPECT_EQ(vote, 36); |
| 194 | + std::remove(path.c_str()); |
| 195 | +} |
0 commit comments