Skip to content

Commit c6d16f9

Browse files
authored
btc(g2): AutoRatchet rig-free SIM KAT — arms staged-migration C2/C3/C4 (#440)
Mirror of DGB #427 --sim-votes for BTC: gives the G2 staged-migration harness (scripts/btc_g2_ratchet_staged_migration_harness.sh) a rig-free verdict source for its C2/C3/C4 rows, which were GATED on a SHA256d rig window because BTC had no AutoRatchet sim KAT (DGB proves the same rows via its existing AutoRatchet KATs). Authorized as path (b). C2 VOTING mints the V35 baseline while voting V36 (empty-tracker bootstrap) C3 #288 accept gate is 60%-by-WORK, and a 95%-by-COUNT activation can NOT outrun it under heterogeneous hashrate (mint-cannot-outrun-accept) — pinned via a verbatim replica of the live inline tail-guard expression in AutoRatchet::get_share_version, plus a staged 1-by-1 migration whose work tally advances monotonically and activates only at full crossing C4 ratchet state persists across restart (CONFIRMED survives reconstruct) Rides the already-allowlisted btc_share_test target (build.yml lines 71/204), so it FIRES in CI with no allowlist/drift-guard change. Test-only, btc-tree local, no consensus / shared-base / build.yml / CMake-shared surface. 6/6 new pass; full btc_share_test 21/21 green. Co-authored-by: frstrtr <frstrtr@users.noreply.github.com>
1 parent d6ed487 commit c6d16f9

2 files changed

Lines changed: 196 additions & 1 deletion

File tree

src/impl/btc/test/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
if (BUILD_TESTING AND GTest_FOUND)
22
# btc twin of ltc share_test — uniquely named to avoid the CMP0002 target
33
# collision with src/impl/ltc/test (both subdirs build in the same tree).
4-
add_executable(btc_share_test share_test.cpp f11_donation_invariance_test.cpp f10b_version_punish_removal_test.cpp g01_share_format_parity_test.cpp)
4+
add_executable(btc_share_test share_test.cpp f11_donation_invariance_test.cpp f10b_version_punish_removal_test.cpp g01_share_format_parity_test.cpp auto_ratchet_sim_test.cpp)
55
target_link_libraries(btc_share_test PRIVATE
66
GTest::gtest_main GTest::gtest
77
core btc
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
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

Comments
 (0)