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
38 changes: 27 additions & 11 deletions src/impl/btc/auto_ratchet.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -140,25 +140,41 @@ class AutoRatchet
// (jtoomim rule). Prevents activation before the entire PPLNS
// window has transitioned — p2pool check() rejects V36 shares
// when the oldest 10% has < 60% signaling.
// WORK-WEIGHTED tail guard (mint<->accept coupling). The
// consensus accept gate (share_check step 2 / p2pool check()
// data.py:1399) keys the 60% switch rule off
// get_desired_version_counts, which in canonical p2pool
// (data.py:2651) weights each share by
// target_to_average_attempts(target) -- i.e. WORK, not a flat
// head-count. AutoRatchet must evaluate the SAME work-weighted
// 60% rule over the SAME [9/10*CL, CL] window before it
// activates; otherwise a 95%-by-COUNT activation can outrun the
// 60%-by-WORK accept gate under heterogeneous hashrate, the node
// mints a V36 boundary share that every peer rejects, and the
// crossing wedges. The weighted tally makes activation strictly
// imply the accept gate, so a minted boundary share can never be
// rejected.
uint32_t tail_start = (chain_length * 9) / 10;
uint32_t tail_size = chain_length / 10;
auto tail_ancestor = tracker.chain.get_nth_parent_key(best_share_hash, tail_start);
auto tail_counts = tracker.get_desired_version_counts(tail_ancestor, tail_size);

int64_t tail_target = 0, tail_total = 0;
for (auto& [ver, cnt] : tail_counts) {
tail_total += cnt;
if (ver >= target_version_)
tail_target += cnt;
auto tail_weights = tracker.get_desired_version_weights(tail_ancestor, tail_size);

// mapped_type is the work-weight accumulator (uint288); default 0.
decltype(tail_weights)::mapped_type tail_target{}, tail_total{};
for (auto& [ver, w] : tail_weights) {
tail_total = tail_total + w;
if (static_cast<int64_t>(ver) >= target_version_)
tail_target = tail_target + w;
}
int tail_pct = (tail_total > 0) ? static_cast<int>(tail_target * 100 / tail_total) : 0;
// Canonical: counts.get(VERSION,0) < sum(counts)*60//100
bool tail_ok = !(tail_target * uint32_t(100) < tail_total * uint32_t(SWITCH_THRESHOLD));

if (tail_pct < SWITCH_THRESHOLD) {
if (!tail_ok) {
static int tail_log = 0;
if (tail_log++ % 20 == 0)
LOG_INFO << "[AutoRatchet] VOTING: full window " << vote_pct
<< "% >= " << ACTIVATION_THRESHOLD << "% but oldest 10% only "
<< tail_pct << "% (need " << SWITCH_THRESHOLD << "%) — waiting";
<< "% >= " << ACTIVATION_THRESHOLD << "% but oldest 10% work-weighted V"
<< target_version_ << " desire < " << SWITCH_THRESHOLD << "%) — waiting";
// Don't transition yet
}
else
Expand Down
73 changes: 57 additions & 16 deletions src/impl/btc/share_check.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1701,31 +1701,72 @@ bool share_check(const ShareT& share,
if (share.m_timestamp > now + 600)
throw std::invalid_argument("share timestamp is too far in the future");

// 2. Version counting — AutoRatchet upgrade enforcement
// p2pool data.py:1400-1414: version switch validation only at BOUNDARIES
// (when share.VERSION != parent.VERSION). Shares that match their parent's
// version are always valid — they were correct when created.
// Previous code rejected ALL v35 shares retroactively after 95% activation.
{
auto lookbehind = static_cast<int32_t>(PoolConfig::chain_length());
if (tracker.chain.contains(share.m_hash) &&
!share.m_prev_hash.IsNull() && tracker.chain.contains(share.m_prev_hash))
// 2. Version switch enforcement — canonical 60% weighted switch rule.
// p2pool data.py check() (lines 1396-1414): a version BOUNDARY
// (share.version != parent.version) is only valid when, in the sampling
// window [CHAIN_LENGTH*9/10, CHAIN_LENGTH] behind the PARENT, the new
// version holds >= 60% of the PPLNS-WEIGHTED desired-version counts.
// The weight is target_to_average_attempts per share — get_desired_version_weights,
// matching canonical get_desired_version_counts (data.py:2651) — NOT a flat count.
// F10/(b): the prior non-canonical 95%-flat-count should_punish_version
// punish is removed; the canonical 60% switch rule is now the ONLY version
// gate. AutoRatchet stays work-weighted (#290) and does not gate peer
// shares here. This couples the accept gate to the mint guard so an
// activated node can never mint a boundary share its peers reject.
{
auto chain_length = static_cast<int32_t>(PoolConfig::chain_length());
if (!share.m_prev_hash.IsNull() && tracker.chain.contains(share.m_prev_hash))
{
// Get parent's share version
// Parent's share version (the type the incoming share must legally follow)
int64_t parent_version = 0;
tracker.chain.get_share(share.m_prev_hash).invoke([&](auto* obj) {
parent_version = std::remove_pointer_t<decltype(obj)>::version;
});
int64_t share_ver = share.version;

// Only enforce version obsolescence at version BOUNDARIES
if (share.version != parent_version)
auto prev_height = tracker.chain.get_height(share.m_prev_hash);
if (prev_height >= chain_length)
{
auto height = tracker.chain.get_height(share.m_hash);
if (height >= lookbehind)
if (share_ver == parent_version)
{
if (tracker.should_punish_version(share.m_hash, share.version, lookbehind))
throw std::invalid_argument("share version too old — newer version has 95%+ activation");
// same version — always valid (correct when created)
}
else if (share_ver == parent_version + 1)
{
// Upgrade by one version: requires >= 60% weighted support in
// window [CHAIN_LENGTH*9/10, CHAIN_LENGTH] behind the parent.
uint32_t window_start = (static_cast<uint32_t>(chain_length) * 9) / 10;
uint32_t window_size = static_cast<uint32_t>(chain_length) / 10;
auto ancestor = tracker.chain.get_nth_parent_key(share.m_prev_hash, window_start);
auto weights = tracker.get_desired_version_weights(
ancestor, static_cast<int32_t>(window_size));

uint288 new_ver_weight; // weight of shares desiring exactly share_ver
uint288 total_weight;
for (auto& [ver, w] : weights)
{
total_weight = total_weight + w;
if (static_cast<int64_t>(ver) == share_ver)
new_ver_weight = new_ver_weight + w;
}
// Canonical: counts.get(self.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_ver + 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_ver));
}
}
else if (share_ver == parent_version + 1)
{
// Not enough history for an upgrade boundary
throw std::invalid_argument("switch without enough history");
}
}
}
Expand Down
30 changes: 30 additions & 0 deletions src/impl/btc/share_tracker.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2169,6 +2169,36 @@ class ShareTracker
return counts;
}

// Work-weighted version tally over a lookbehind window. Mirrors
// get_desired_version_counts but weights each share by its work
// (target_to_average_attempts), matching canonical p2pool
// get_desired_version_counts (data.py:2651) — the COUNTS name is a
// misnomer; the canonical tally is WORK-weighted. The consensus accept
// gate keys the 60% switch rule off this work-weighted tally, so the
// AutoRatchet mint gate must evaluate the same weighting to stay coupled.
// Returns map of version -> accumulated work weight.
std::map<uint64_t, uint288> get_desired_version_weights(const uint256& share_hash, int32_t lookbehind)
{
std::map<uint64_t, uint288> weights;
if (!chain.contains(share_hash))
return weights;
auto height = chain.get_height(share_hash);
auto actual = std::min(lookbehind, height);
if (actual <= 0)
return weights;

auto view = chain.get_chain(share_hash, actual);
for (auto [hash, data] : view)
{
uint64_t dv = 0;
data.share.invoke([&](auto* obj) { dv = obj->m_desired_version; });
auto* idx = chain.get_index(hash);
if (idx)
weights[dv] = weights[dv] + idx->work;
}
return weights;
}

// -- Merged mining: per-chain PPLNS weights --
// For a specific aux chain_id, walk the share chain and accumulate PPLNS
// weights for V36-signaling shares. Uses O(log n) skip list.
Expand Down
Loading