Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/core/test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
if (BUILD_TESTING AND (GTest_FOUND OR GTEST_FOUND))
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)
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)
target_link_libraries(core_test PRIVATE GTest::gtest_main core c2pool_merged_mining GTest::gtest)
target_link_libraries(core_test PRIVATE c2pool_payout c2pool_hashrate ltc_coin) # OBJECT-lib SCC direct-naming (#22/#39)

Expand Down
109 changes: 109 additions & 0 deletions src/core/test/version_gate_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#include <gtest/gtest.h>

#include <map>
#include <string>
#include <cstdint>
#include <stdexcept>

#include <core/version_gate.hpp>
#include <core/uint256.hpp>

using core::version_gate::verify_version_transition;

// KAT for the cross-coin v36-native share-version-transition rule SSOT.
// Weight type is uint288, matching get_desired_version_weights' tally type.

namespace {

// Build a weighted desired-version tally: { version -> weight }.
std::map<uint64_t, uint288> tally(std::initializer_list<std::pair<uint64_t, uint64_t>> entries)
{
std::map<uint64_t, uint288> m;
for (const auto& [ver, w] : entries)
m[ver] = uint288(w);
return m;
}

} // namespace

TEST(VersionGateTransition, SameVersionAdmitted)
{
// parent=36, share=36: never throws (correct when minted), with or w/o history.
auto w = tally({{36, 100}});
EXPECT_NO_THROW(verify_version_transition<uint288>(36, 36, w, /*have_history=*/true));
EXPECT_NO_THROW(verify_version_transition<uint288>(36, 36, w, /*have_history=*/false));
}

TEST(VersionGateTransition, UpgradeWithMajoritySupportAdmitted)
{
// +1 upgrade, new version holds 70% of weighted support, history present -> ok.
auto w = tally({{36, 70}, {35, 30}});
EXPECT_NO_THROW(verify_version_transition<uint288>(35, 36, w, /*have_history=*/true));
}

TEST(VersionGateTransition, UpgradeWithoutMajoritySupportRejected)
{
// +1 upgrade, new version only 50% of weighted support -> reject.
auto w = tally({{36, 50}, {35, 50}});
try
{
verify_version_transition<uint288>(35, 36, w, /*have_history=*/true);
FAIL() << "expected throw";
}
catch (const std::invalid_argument& e)
{
EXPECT_STREQ(e.what(), "switch without enough hash power upgraded");
}
}

TEST(VersionGateTransition, UpgradeWithoutHistoryRejected)
{
// +1 upgrade, no CHAIN_LENGTH history -> reject with the history message.
auto w = tally({{36, 100}});
try
{
verify_version_transition<uint288>(35, 36, w, /*have_history=*/false);
FAIL() << "expected throw";
}
catch (const std::invalid_argument& e)
{
EXPECT_STREQ(e.what(), "switch without enough history");
}
}

TEST(VersionGateTransition, DowngradeByOneAdmitted)
{
// -1 downgrade (parent=36, share=35: AutoRatchet deactivation) with history -> ok.
auto w = tally({{36, 100}});
EXPECT_NO_THROW(verify_version_transition<uint288>(36, 35, w, /*have_history=*/true));
}

TEST(VersionGateTransition, MultiVersionJumpWithHistoryRejected)
{
// parent=34, share=36 (jump of 2) with history -> invalid version jump.
auto w = tally({{36, 100}});
try
{
verify_version_transition<uint288>(34, 36, w, /*have_history=*/true);
FAIL() << "expected throw";
}
catch (const std::invalid_argument& e)
{
EXPECT_STREQ(e.what(), "invalid version jump from 34 to 36");
}
}

TEST(VersionGateTransition, MultiVersionJumpWithoutHistoryAdmitted)
{
// parent=34, share=36 with no history: only +1 upgrades are gated -> admitted.
auto w = tally({{36, 100}});
EXPECT_NO_THROW(verify_version_transition<uint288>(34, 36, w, /*have_history=*/false));
}

TEST(VersionGateTransition, ExactSixtyPercentBoundaryAdmitted)
{
// new version == exactly 60% of total. Rule is `new*100 < total*60`:
// 60*100 == 100*60, NOT strictly less -> 60% PASSES (no throw).
auto w = tally({{36, 60}, {35, 40}}); // total 100, new=60 -> exactly 60%
EXPECT_NO_THROW(verify_version_transition<uint288>(35, 36, w, /*have_history=*/true));
}
72 changes: 72 additions & 0 deletions src/core/version_gate.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
//

#include <cstdint>
#include <map>
#include <stdexcept>
#include <string>

namespace core::version_gate
{
Expand All @@ -47,4 +50,73 @@ constexpr bool is_v36_active(uint64_t version)
return version >= V36_ACTIVATION_VERSION;
}


// Canonical v36-native share-version-transition rule (cross-coin SSOT).
// Applies the boundary admit/reject decision to an ALREADY-EXTRACTED
// (parent_version, share_version) pair using a PRECOMPUTED PPLNS-WEIGHTED
// desired-version tally for the [CHAIN_LENGTH*9/10, CHAIN_LENGTH] window behind
// the parent, plus a flag for whether CHAIN_LENGTH history exists behind the
// parent. Throws std::invalid_argument on a disallowed switch; returns on an
// admissible one.
//
// Rule (p2pool data.py check()): same-version always valid; +1 upgrade needs
// >= 60% PPLNS-WEIGHTED desired-version support for the new version in the
// window AND CHAIN_LENGTH history; -1 (AutoRatchet deactivation) valid; any
// other jump throws when history exists. With insufficient history only a +1
// upgrade is rejected ("switch without enough history") and other shapes pass,
// matching the BTC inline gate exactly.
//
// `floor` is the per-coin v36 activation version (e.g. BTC 35->36, DASH 16->36).
// It is bucket-3 transition-compat carried as a PARAMETER (never hardcoded) so a
// coin whose seam also runs an obsolescence branch (DASH) can thread its own
// floor when it migrates onto this SSOT; the canonical boundary rule itself does
// not reference it. Defaulted to V36_ACTIVATION_VERSION.
template <typename WeightT>
inline void verify_version_transition(
int64_t parent_version,
int64_t share_version,
const std::map<uint64_t, WeightT>& window_weights,
bool have_history,
uint64_t floor = V36_ACTIVATION_VERSION)
{
(void)floor;
// same version — always valid (it was correct when minted), regardless of history.
if (share_version == parent_version)
return;

if (have_history)
{
if (share_version == parent_version + 1)
{
// Upgrade by one: needs >= 60% weighted support for the new version.
WeightT new_ver_weight{};
WeightT total_weight{};
for (const auto& [ver, w] : window_weights)
{
total_weight = total_weight + w;
if (static_cast<int64_t>(ver) == share_version)
new_ver_weight = new_ver_weight + w;
}
// canonical: counts.get(VERSION,0) < sum(counts)*60//100
if (new_ver_weight * uint32_t(100) < total_weight * uint32_t(60))
throw std::invalid_argument("switch without enough hash power upgraded");
}
else if (parent_version == share_version + 1)
{
// downgrade by one (AutoRatchet deactivation: V35 may follow V36)
}
else
{
throw std::invalid_argument("invalid version jump from "
+ std::to_string(parent_version) + " to " + std::to_string(share_version));
}
}
else if (share_version == parent_version + 1)
{
// Not enough history for an upgrade boundary.
throw std::invalid_argument("switch without enough history");
}
// else (downgrade / multi-jump with insufficient history): admitted, matching BTC.
}

} // namespace core::version_gate
Loading