Skip to content

Commit 2671434

Browse files
committed
core(version-gate): add cross-coin verify_version_transition rule SSOT (additive)
Add the canonical v36-native share-version-transition RULE to core::version_gate as a templated function verify_version_transition. This standardizes the boundary admit/reject decision SHAPE that is currently inlined per-coin (BTC share_check.hpp ~1756-1815; DASH share_check.hpp ~656). The function applies the decision to an ALREADY-EXTRACTED (parent_version, share_version) pair plus a PRECOMPUTED PPLNS-WEIGHTED desired-version tally for the [CHAIN_LENGTH*9/10, CHAIN_LENGTH] window and a have_history flag. Per-coin version extraction and window-weight plumbing stay in each coin caller (legitimate per-coin divergence). Rule (p2pool data.py check()): same-version always valid; +1 upgrade needs >= 60% weighted support AND CHAIN_LENGTH history; -1 (AutoRatchet deactivation) valid; any other jump throws when history exists; with insufficient history only a +1 upgrade is rejected. Byte-for-byte equivalent to the BTC inline gate. Additive and behavior-preserving: NO callers in this PR. Includes a core_test KAT (version_gate_test.cpp, uint288 weights) covering same-version, +1 with/without 60% support, +1 without history, -1 downgrade, multi-jump with/without history, and the exact-60% boundary.
1 parent ab32fa4 commit 2671434

3 files changed

Lines changed: 182 additions & 1 deletion

File tree

src/core/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 OR GTEST_FOUND))
2-
add_executable(core_test pack_test.cpp events_test.cpp chain_test.cpp packet_test.cpp block_broadcast_test.cpp broadcast_convergence_matrix_test.cpp core_merkle_branches_test.cpp)
2+
add_executable(core_test pack_test.cpp events_test.cpp chain_test.cpp packet_test.cpp block_broadcast_test.cpp broadcast_convergence_matrix_test.cpp core_merkle_branches_test.cpp version_gate_test.cpp)
33
target_link_libraries(core_test PRIVATE GTest::gtest_main core c2pool_merged_mining GTest::gtest)
44
target_link_libraries(core_test PRIVATE c2pool_payout c2pool_hashrate ltc_coin) # OBJECT-lib SCC direct-naming (#22/#39)
55

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#include <gtest/gtest.h>
2+
3+
#include <map>
4+
#include <string>
5+
#include <cstdint>
6+
#include <stdexcept>
7+
8+
#include <core/version_gate.hpp>
9+
#include <core/uint256.hpp>
10+
11+
using core::version_gate::verify_version_transition;
12+
13+
// KAT for the cross-coin v36-native share-version-transition rule SSOT.
14+
// Weight type is uint288, matching get_desired_version_weights' tally type.
15+
16+
namespace {
17+
18+
// Build a weighted desired-version tally: { version -> weight }.
19+
std::map<uint64_t, uint288> tally(std::initializer_list<std::pair<uint64_t, uint64_t>> entries)
20+
{
21+
std::map<uint64_t, uint288> m;
22+
for (const auto& [ver, w] : entries)
23+
m[ver] = uint288(w);
24+
return m;
25+
}
26+
27+
} // namespace
28+
29+
TEST(VersionGateTransition, SameVersionAdmitted)
30+
{
31+
// parent=36, share=36: never throws (correct when minted), with or w/o history.
32+
auto w = tally({{36, 100}});
33+
EXPECT_NO_THROW(verify_version_transition<uint288>(36, 36, w, /*have_history=*/true));
34+
EXPECT_NO_THROW(verify_version_transition<uint288>(36, 36, w, /*have_history=*/false));
35+
}
36+
37+
TEST(VersionGateTransition, UpgradeWithMajoritySupportAdmitted)
38+
{
39+
// +1 upgrade, new version holds 70% of weighted support, history present -> ok.
40+
auto w = tally({{36, 70}, {35, 30}});
41+
EXPECT_NO_THROW(verify_version_transition<uint288>(35, 36, w, /*have_history=*/true));
42+
}
43+
44+
TEST(VersionGateTransition, UpgradeWithoutMajoritySupportRejected)
45+
{
46+
// +1 upgrade, new version only 50% of weighted support -> reject.
47+
auto w = tally({{36, 50}, {35, 50}});
48+
try
49+
{
50+
verify_version_transition<uint288>(35, 36, w, /*have_history=*/true);
51+
FAIL() << "expected throw";
52+
}
53+
catch (const std::invalid_argument& e)
54+
{
55+
EXPECT_STREQ(e.what(), "switch without enough hash power upgraded");
56+
}
57+
}
58+
59+
TEST(VersionGateTransition, UpgradeWithoutHistoryRejected)
60+
{
61+
// +1 upgrade, no CHAIN_LENGTH history -> reject with the history message.
62+
auto w = tally({{36, 100}});
63+
try
64+
{
65+
verify_version_transition<uint288>(35, 36, w, /*have_history=*/false);
66+
FAIL() << "expected throw";
67+
}
68+
catch (const std::invalid_argument& e)
69+
{
70+
EXPECT_STREQ(e.what(), "switch without enough history");
71+
}
72+
}
73+
74+
TEST(VersionGateTransition, DowngradeByOneAdmitted)
75+
{
76+
// -1 downgrade (parent=36, share=35: AutoRatchet deactivation) with history -> ok.
77+
auto w = tally({{36, 100}});
78+
EXPECT_NO_THROW(verify_version_transition<uint288>(36, 35, w, /*have_history=*/true));
79+
}
80+
81+
TEST(VersionGateTransition, MultiVersionJumpWithHistoryRejected)
82+
{
83+
// parent=34, share=36 (jump of 2) with history -> invalid version jump.
84+
auto w = tally({{36, 100}});
85+
try
86+
{
87+
verify_version_transition<uint288>(34, 36, w, /*have_history=*/true);
88+
FAIL() << "expected throw";
89+
}
90+
catch (const std::invalid_argument& e)
91+
{
92+
EXPECT_STREQ(e.what(), "invalid version jump from 34 to 36");
93+
}
94+
}
95+
96+
TEST(VersionGateTransition, MultiVersionJumpWithoutHistoryAdmitted)
97+
{
98+
// parent=34, share=36 with no history: only +1 upgrades are gated -> admitted.
99+
auto w = tally({{36, 100}});
100+
EXPECT_NO_THROW(verify_version_transition<uint288>(34, 36, w, /*have_history=*/false));
101+
}
102+
103+
TEST(VersionGateTransition, ExactSixtyPercentBoundaryAdmitted)
104+
{
105+
// new version == exactly 60% of total. Rule is `new*100 < total*60`:
106+
// 60*100 == 100*60, NOT strictly less -> 60% PASSES (no throw).
107+
auto w = tally({{36, 60}, {35, 40}}); // total 100, new=60 -> exactly 60%
108+
EXPECT_NO_THROW(verify_version_transition<uint288>(35, 36, w, /*have_history=*/true));
109+
}

src/core/version_gate.hpp

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
//
3333

3434
#include <cstdint>
35+
#include <map>
36+
#include <stdexcept>
37+
#include <string>
3538

3639
namespace core::version_gate
3740
{
@@ -47,4 +50,73 @@ constexpr bool is_v36_active(uint64_t version)
4750
return version >= V36_ACTIVATION_VERSION;
4851
}
4952

53+
54+
// Canonical v36-native share-version-transition rule (cross-coin SSOT).
55+
// Applies the boundary admit/reject decision to an ALREADY-EXTRACTED
56+
// (parent_version, share_version) pair using a PRECOMPUTED PPLNS-WEIGHTED
57+
// desired-version tally for the [CHAIN_LENGTH*9/10, CHAIN_LENGTH] window behind
58+
// the parent, plus a flag for whether CHAIN_LENGTH history exists behind the
59+
// parent. Throws std::invalid_argument on a disallowed switch; returns on an
60+
// admissible one.
61+
//
62+
// Rule (p2pool data.py check()): same-version always valid; +1 upgrade needs
63+
// >= 60% PPLNS-WEIGHTED desired-version support for the new version in the
64+
// window AND CHAIN_LENGTH history; -1 (AutoRatchet deactivation) valid; any
65+
// other jump throws when history exists. With insufficient history only a +1
66+
// upgrade is rejected ("switch without enough history") and other shapes pass,
67+
// matching the BTC inline gate exactly.
68+
//
69+
// `floor` is the per-coin v36 activation version (e.g. BTC 35->36, DASH 16->36).
70+
// It is bucket-3 transition-compat carried as a PARAMETER (never hardcoded) so a
71+
// coin whose seam also runs an obsolescence branch (DASH) can thread its own
72+
// floor when it migrates onto this SSOT; the canonical boundary rule itself does
73+
// not reference it. Defaulted to V36_ACTIVATION_VERSION.
74+
template <typename WeightT>
75+
inline void verify_version_transition(
76+
int64_t parent_version,
77+
int64_t share_version,
78+
const std::map<uint64_t, WeightT>& window_weights,
79+
bool have_history,
80+
uint64_t floor = V36_ACTIVATION_VERSION)
81+
{
82+
(void)floor;
83+
// same version — always valid (it was correct when minted), regardless of history.
84+
if (share_version == parent_version)
85+
return;
86+
87+
if (have_history)
88+
{
89+
if (share_version == parent_version + 1)
90+
{
91+
// Upgrade by one: needs >= 60% weighted support for the new version.
92+
WeightT new_ver_weight{};
93+
WeightT total_weight{};
94+
for (const auto& [ver, w] : window_weights)
95+
{
96+
total_weight = total_weight + w;
97+
if (static_cast<int64_t>(ver) == share_version)
98+
new_ver_weight = new_ver_weight + w;
99+
}
100+
// canonical: counts.get(VERSION,0) < sum(counts)*60//100
101+
if (new_ver_weight * uint32_t(100) < total_weight * uint32_t(60))
102+
throw std::invalid_argument("switch without enough hash power upgraded");
103+
}
104+
else if (parent_version == share_version + 1)
105+
{
106+
// downgrade by one (AutoRatchet deactivation: V35 may follow V36)
107+
}
108+
else
109+
{
110+
throw std::invalid_argument("invalid version jump from "
111+
+ std::to_string(parent_version) + " to " + std::to_string(share_version));
112+
}
113+
}
114+
else if (share_version == parent_version + 1)
115+
{
116+
// Not enough history for an upgrade boundary.
117+
throw std::invalid_argument("switch without enough history");
118+
}
119+
// else (downgrade / multi-jump with insufficient history): admitted, matching BTC.
120+
}
121+
50122
} // namespace core::version_gate

0 commit comments

Comments
 (0)