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
114 changes: 91 additions & 23 deletions src/impl/dash/version_negotiation.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@
//
// * get_desired_version_counts() — a PLAIN tally (one vote per share) over a
// chain window. Reference: p2pool-dash data.py get_desired_version_counts
// as consumed by Share.check()'s confirmed-state guard and by the
// AutoRatchet desired-version selection. It is NOT weighted in place.
// as consumed by the AutoRatchet desired-version selection and the VOTING
// tail guard (F10 keeps these count-based). It is NOT weighted in place.
//
// * get_desired_version_weights() — the SEPARATE WEIGHTED variant
// (weight = target_to_average_attempts(target), i.e. expected hashes) the
// v36 activation gate consumes. Reference: p2pool-merged-v36 work.py
// v36_active = weight[36] / Sum(weight) >= 0.95.
// (weight = target_to_average_attempts(target), i.e. expected hashes) that
// BOTH consensus gates consume: the 60% SUCCESSOR switch gate (D1) and the
// 95% v36 activation gate. Reference: p2pool data.py:1396-1414 (60% switch,
// PPLNS-weighted) + p2pool-merged-v36 work.py v36_active = weight[36] /
// Sum(weight) >= 0.95.
//
// The confirmed-state guard (Share.check): a SUCCESSOR-version share may follow
// its predecessor only if the new version already holds >= 60% of the PLAIN
// votes in the [9/10 .. 10/10] tail of the CHAIN_LENGTH window; a switch with
// its predecessor only if the new version already holds >= 60% of the WEIGHTED
// desired-version tally in the [9/10 .. 10/10] tail of the CHAIN_LENGTH window; a switch with
// fewer than CHAIN_LENGTH ancestors is rejected ("without enough history").

#include "share_chain.hpp" // dash::ShareChain, dash::DashShare
Expand Down Expand Up @@ -69,33 +71,40 @@ get_desired_version_weights(ShareChain& chain, const uint256& start_hash, uint64
static_cast<uint64_t>(chain.get_height(start_hash)));
for (auto&& [h, data] : chain.get_chain(start_hash, n)) {
(void)h;
// D5: consume the cached per-share work (ShareIndex::work, set at add()
// to target_to_average_attempts(bits_to_target(m_bits)) -- share_chain.hpp:227)
// instead of recomputing it live. The two are equal by construction; sourcing
// the cached accessor removes recompute-drift risk and standardizes on the
// shared F10 work metric (divergence-map row D5).
const uint288 w = data.index->work;
data.share.invoke([&](auto* obj) {
using S = std::remove_pointer_t<decltype(obj)>;
if constexpr (std::is_same_v<S, dash::DashShare>) {
uint288 w = chain::target_to_average_attempts(
chain::bits_to_target(obj->m_bits));
if constexpr (std::is_same_v<S, dash::DashShare>)
res[obj->m_desired_version] += w;
}
});
}
return res;
}

// 60% confirmed-state guard (p2pool-dash data.py Share.check). A SUCCESSOR
// share is accepted only when its PLAIN vote count reaches floor(total*60/100).
// The floor matches the oracle's integer `sum*60//100` exactly (e.g. 4/7 votes
// clears thr=4, 3/7 does not).
// 60% switch gate (canonical v36-native -- F10 685669e9 share_check.hpp step 2;
// p2pool data.py:1396-1414). A SUCCESSOR-version share is accepted only when the
// new version holds >= 60% of the PPLNS-WEIGHTED desired-version tally (weight =
// target_to_average_attempts per share), via the exact rational
// new_ver_weight*100 >= total_weight*60 -- integer uint288, no IEEE-double and no
// floor. D1 standardization: the weighted tally (get_desired_version_weights)
// feeds this consensus gate; the PLAIN count (get_desired_version_counts) is
// retained for AutoRatchet + the VOTING tail guard only (D4). F10 deleted the
// old flat-count validate_version_switch precisely because flat-count diverges.
inline bool
successor_switch_allowed(const std::map<uint64_t, uint64_t>& plain_counts,
successor_switch_allowed(const std::map<uint64_t, uint288>& weights,
uint64_t successor_version)
{
uint64_t total = 0;
for (const auto& [v, c] : plain_counts) total += c;
if (total == 0) return false;
const uint64_t threshold = (total * 60) / 100; // floor, as in the oracle
auto it = plain_counts.find(successor_version);
const uint64_t have = (it == plain_counts.end()) ? 0 : it->second;
return have >= threshold;
uint288 total(0);
for (const auto& [v, w] : weights) total += w;
if (total == uint288(0)) return false;
auto it = weights.find(successor_version);
const uint288 have = (it == weights.end()) ? uint288(0) : it->second;
return have * uint288(100) >= total * uint288(60);
}

// v36 activation gate (p2pool-merged-v36 work.py): v36 is active once its
Expand Down Expand Up @@ -127,4 +136,63 @@ negotiation_window(ShareChain& chain, const uint256& prev_hash, uint64_t chain_l
return Window{start, dist};
}

// ── D2: unified 5-case version-switch classifier (F10 share_check.hpp step 2) ─
// successor_switch_allowed() answers ONLY the 60% upgrade threshold. The
// canonical v36-native shape (F10 share_check step 2; p2pool-dash data.py
// Share.check) classifies a proposed predecessor->desired version transition
// into five cases -- the threshold gate is just the body of ONE of them:
//
// Same desired == prev -> always allowed (continuation)
// SuccessorGated desired == prev + 1 -> allowed IFF the 60% weighted
// gate (successor_switch_allowed)
// clears over the window
// PredecessorAllowed desired == prev - 1 -> always allowed (downgrade-by-one
// rollback; needs no support window)
// InvalidJump |desired - prev| > 1 -> rejected (no version skipping)
// NoHistory a +1 switch is proposed -> rejected ("switch without enough
// with < CHAIN_LENGTH history"): the support window
// ancestors cannot be evaluated
//
// no-history applies ONLY to the +1 successor case -- it is the one transition
// that needs the support window; Same/Predecessor need no window, an InvalidJump
// is rejected on structure regardless of history. DASH previously expressed only
// negotiation_window() + the threshold; this folds them into the canonical shape.
// Pure + scaffolding-only (no live share_check caller) -- zero consensus risk.
enum class SwitchClass : uint8_t {
Same = 0, // desired == prev -- allow
SuccessorGated = 1, // desired == prev + 1 -- allow iff 60% gate clears
PredecessorAllowed = 2, // desired == prev - 1 -- allow (rollback)
InvalidJump = 3, // |desired - prev| > 1 -- reject
NoHistory = 4, // +1 switch, < CHAIN_LENGTH anc -- reject (no support window)
};

// Classify the predecessor->desired transition. `has_history` is whether the
// support window exists (>= CHAIN_LENGTH ancestors), i.e. negotiation_window()
// returned a value; it only changes the verdict for the +1 successor case.
inline SwitchClass
classify_switch(uint64_t prev_version, uint64_t desired_version, bool has_history)
{
if (desired_version == prev_version) return SwitchClass::Same;
if (desired_version == prev_version - 1) return SwitchClass::PredecessorAllowed;
if (desired_version == prev_version + 1)
return has_history ? SwitchClass::SuccessorGated : SwitchClass::NoHistory;
return SwitchClass::InvalidJump; // |delta| > 1
}

// Final accept decision for a classified transition. SuccessorGated defers to the
// 60% weighted gate result (`gate_cleared` = successor_switch_allowed over the
// window); every other case is decided structurally. Mirrors F10 step 2 exactly.
inline bool
switch_accepted(SwitchClass cls, bool gate_cleared)
{
switch (cls) {
case SwitchClass::Same:
case SwitchClass::PredecessorAllowed: return true;
case SwitchClass::SuccessorGated: return gate_cleared;
case SwitchClass::InvalidJump:
case SwitchClass::NoHistory: return false;
}
return false;
}

} // namespace dash::version_negotiation
103 changes: 76 additions & 27 deletions test/test_dash_conformance.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -409,12 +409,18 @@ struct SyntheticChain {
std::vector<dash::DashShare*> pool; // chain (ShareVariants::destroy) owns the shares; raw avoids double-free

uint256 add(uint8_t tag, const uint256& prev, uint16_t donation,
const uint160& pkh) {
const uint160& pkh,
uint32_t bits = BITS_DIFF1, uint64_t version = 0) {
auto* s = new dash::DashShare();
s->m_hash = pplns_share_hash(tag);
s->m_prev_hash = prev;
s->m_bits = BITS_DIFF1;
s->m_max_bits = BITS_DIFF1; // ShareIndex ctor reads m_max_bits
// Final fields set BEFORE chain.add(): ShareIndex caches
// work=target_to_average_attempts(bits_to_target(m_bits)) at insertion,
// and a production share has immutable bits once it enters the chain.
// (m_max_bits stays at diff1 -- min_work basis unchanged.)
s->m_bits = bits;
s->m_max_bits = BITS_DIFF1;
s->m_desired_version = version;
s->m_donation = donation;
s->m_pubkey_hash = pkh;
const uint256 h = s->m_hash;
Expand Down Expand Up @@ -549,7 +555,7 @@ TEST(DashConformancePplns, ColdChainFallsBackToSingleRecipient) {
// weight[36]/Sum >= 0.95). All thresholds and weights below are KAT vectors
// computed OUT-OF-BAND in CPython from the exact integer formulas
// target_to_average_attempts = 2**256 // (target + 1)
// tail-guard : count >= (total*60)//100 (floor)
// switch gate : have_w*100 >= total_w*60 (weighted, exact rational)
// v36 gate : w36*100 >= total*95 (exact rational)
// so the pins are not circular with the C++ path.

Expand All @@ -565,11 +571,8 @@ namespace {
// obj->m_bits live, so post-add patching is what the production walk observes.
uint256 add_versioned(SyntheticChain& sc, uint8_t tag, const uint256& prev,
uint64_t version, uint32_t bits, const uint160& pkh) {
uint256 h = sc.add(tag, prev, /*donation*/ 0, pkh);
auto* s = sc.pool.back();
s->m_desired_version = version;
s->m_bits = bits;
return h;
// Bits/version set pre-insertion so the cached ShareIndex::work matches.
return sc.add(tag, prev, /*donation*/ 0, pkh, bits, version);
}
} // namespace

Expand All @@ -595,18 +598,19 @@ TEST(DashConformanceVersionNeg, PlainCountAndWeightVsOutOfBandKat) {
<< "v36 weight != CPython KAT (3x diff1 work)";
}

// 60% SUCCESSOR tail-guard, floor semantics. KAT booleans from CPython
// count >= (total*60)//100. The 4/7 case (57% but thr=floor(4.2)=4) and 3/7
// case lock the floor exactly as the oracle's `sum*60//100`.
TEST(DashConformanceVersionNeg, SuccessorGuard60PercentFloorKat) {
// 60% SUCCESSOR switch gate, PPLNS-WEIGHTED exact-rational semantics (D1 /
// F10 685669e9). KAT booleans from CPython have_w*100 >= total_w*60 (no floor).
// The 4/7 case now REJECTS (400 < 420) where the prior plain-floor gate cleared
// it (thr=floor(4.2)=4) -- the exact divergence D1 standardizes away.
TEST(DashConformanceVersionNeg, SuccessorGuard60PercentWeightedKat) {
using dash::version_negotiation::successor_switch_allowed;
EXPECT_TRUE (successor_switch_allowed({{36u, 6u}, {16u, 4u}}, 36u)); // 60%
EXPECT_FALSE(successor_switch_allowed({{36u, 5u}, {16u, 5u}}, 36u)); // 50%
EXPECT_TRUE (successor_switch_allowed({{36u, 60u}, {16u, 40u}}, 36u)); // 60%
EXPECT_FALSE(successor_switch_allowed({{36u, 59u}, {16u, 41u}}, 36u)); // 59%
EXPECT_TRUE (successor_switch_allowed({{36u, 4u}, {16u, 3u}}, 36u)); // 4/7, thr=4
EXPECT_FALSE(successor_switch_allowed({{36u, 3u}, {16u, 4u}}, 36u)); // 3/7, thr=4
EXPECT_FALSE(successor_switch_allowed({}, 36u)); // empty
EXPECT_TRUE (successor_switch_allowed({{36u, uint288(6u)}, {16u, uint288(4u)}}, 36u)); // 6/10 = 60%
EXPECT_FALSE(successor_switch_allowed({{36u, uint288(5u)}, {16u, uint288(5u)}}, 36u)); // 5/10 = 50%
EXPECT_TRUE (successor_switch_allowed({{36u, uint288(60u)}, {16u, uint288(40u)}}, 36u)); // 60/100 = 60%
EXPECT_FALSE(successor_switch_allowed({{36u, uint288(59u)}, {16u, uint288(41u)}}, 36u)); // 59/100 = 59%
EXPECT_FALSE(successor_switch_allowed({{36u, uint288(4u)}, {16u, uint288(3u)}}, 36u)); // 4/7: 400 < 420 -> reject
EXPECT_FALSE(successor_switch_allowed({{36u, uint288(3u)}, {16u, uint288(4u)}}, 36u)); // 3/7: 300 < 420 -> reject
EXPECT_FALSE(successor_switch_allowed({}, 36u)); // empty
}

// v36 activation gate, exact-rational 95% on the work-weighted tally. KAT
Expand All @@ -626,10 +630,11 @@ TEST(DashConformanceVersionNeg, V36GateWeighted95PercentKat) {
EXPECT_FALSE(v36_active({{36u, big * uint288(94u)}, {16u, big * uint288(6u)}}));
}

// The two tallies must diverge: on the 3x-v36(diff1)+1x-v16(heavy) chain the
// PLAIN guard clears (v36 holds 3/4 of votes >= 60%) but the WEIGHTED gate
// denies (v36 holds only ~60% of work < 95%). Proves the gate consumes the
// weighted variant, not the plain count (F10 separation).
// D1 + F10 separation: the 60% SUCCESSOR gate consumes the WEIGHTED tally, not
// the plain count. On the 3x-v36(diff1)+1x-v16(heavy) chain the two disagree at
// the 60% boundary: plain v36 = 3/4 = 75% (a plain-count gate WOULD clear), but
// weighted v36 = 3*W_DIFF1 / (3*W_DIFF1 + W_HEAVY) = 59.9996% < 60%, so the
// canonical weighted gate DENIES. The 95% activation gate likewise denies.
TEST(DashConformanceVersionNeg, GateUsesWeightNotPlainCount) {
SyntheticChain sc;
const uint160 A = miner_h160(0x02);
Expand All @@ -641,12 +646,56 @@ TEST(DashConformanceVersionNeg, GateUsesWeightNotPlainCount) {
auto plain = dash::version_negotiation::get_desired_version_counts(sc.chain, tip, 4);
auto weights = dash::version_negotiation::get_desired_version_weights(sc.chain, tip, 4);

// Plain: v36 = 3/4 of votes -> SUCCESSOR switch permitted.
EXPECT_TRUE(dash::version_negotiation::successor_switch_allowed(plain, 36u));
// Weighted: v36 work share ~60% (3*W_DIFF1 / (3*W_DIFF1 + W_HEAVY)) < 95%.
// Plain count: v36 holds 3/4 = 75% of votes -- a plain-count 60% gate WOULD clear.
const std::map<uint64_t, uint64_t> plain_expected = {{16u, 1u}, {36u, 3u}};
EXPECT_EQ(plain, plain_expected);
// Weighted 60% SUCCESSOR gate (D1): v36 work ~59.9996% < 60% -> DENY.
EXPECT_FALSE(dash::version_negotiation::successor_switch_allowed(weights, 36u));
// Weighted 95% activation gate: v36 work ~60% < 95% -> not active.
EXPECT_FALSE(dash::version_negotiation::v36_active(weights));
}

// D2: unified 5-case version-switch classifier (F10 share_check.hpp step 2).
// Pins classify_switch() over every transition shape and the switch_accepted()
// decision table, then cross-checks the SuccessorGated body against the live D1
// 60% weighted gate so the classifier can never diverge from successor_switch_allowed.
TEST(DashConformanceVersionNeg, SwitchClassifier5CaseKat) {
using namespace dash::version_negotiation;
using C = dash::version_negotiation::SwitchClass;

// classify_switch: structural transition classification (prev, desired, has_history).
EXPECT_EQ(classify_switch(36u, 36u, true), C::Same); // continuation
EXPECT_EQ(classify_switch(36u, 36u, false), C::Same); // history irrelevant
EXPECT_EQ(classify_switch(35u, 34u, true), C::PredecessorAllowed); // downgrade-by-one
EXPECT_EQ(classify_switch(35u, 34u, false), C::PredecessorAllowed); // needs no window
EXPECT_EQ(classify_switch(35u, 36u, true), C::SuccessorGated); // +1 with support window
EXPECT_EQ(classify_switch(35u, 36u, false), C::NoHistory); // +1, window absent
EXPECT_EQ(classify_switch(35u, 37u, true), C::InvalidJump); // +2 skip
EXPECT_EQ(classify_switch(35u, 33u, true), C::InvalidJump); // -2 skip
EXPECT_EQ(classify_switch(35u, 40u, false), C::InvalidJump); // +5, structural reject

// switch_accepted: final verdict. SuccessorGated defers to the 60% gate;
// every other case is decided structurally (gate_cleared is then ignored).
EXPECT_TRUE (switch_accepted(C::Same, false)); // continuation always ok
EXPECT_TRUE (switch_accepted(C::PredecessorAllowed, false)); // rollback always ok
EXPECT_TRUE (switch_accepted(C::SuccessorGated, true)); // +1, gate cleared
EXPECT_FALSE(switch_accepted(C::SuccessorGated, false)); // +1, gate denied
EXPECT_FALSE(switch_accepted(C::InvalidJump, true)); // skip never ok
EXPECT_FALSE(switch_accepted(C::NoHistory, true)); // no support window

// Cross-check the gated body against the live D1 weighted gate: a +1 switch
// to v36 with history is accepted exactly when successor_switch_allowed clears.
const std::map<uint64_t, uint288> clears = {{36u, uint288(6u)}, {16u, uint288(4u)}}; // 60%
const std::map<uint64_t, uint288> denies = {{36u, uint288(5u)}, {16u, uint288(5u)}}; // 50%
EXPECT_TRUE (switch_accepted(classify_switch(35u, 36u, true),
successor_switch_allowed(clears, 36u)));
EXPECT_FALSE(switch_accepted(classify_switch(35u, 36u, true),
successor_switch_allowed(denies, 36u)));
// Same chain, but window absent -> NoHistory short-circuits the gate entirely.
EXPECT_FALSE(switch_accepted(classify_switch(35u, 36u, false),
successor_switch_allowed(clears, 36u)));
}

// ── DIP4 special-tx coinbase payload (CCbTx) wire-encoding conformance ──────
// DASH coinbase transactions carry a DIP-0004 "special transaction" extra
// payload: the CCbTx. Its merkleRootMNList / merkleRootQuorums fields are what
Expand Down
Loading