Skip to content

Commit 264ece3

Browse files
authored
btc: F10/(b) complete — delete residual flat-count version-punish (#365)
Removes the non-canonical 95%-flat-count version-obsolescence punish that the gap-audit found half-applied on the BTC twin (share_check.hpp already carried the F10/(b) marker; the punish path itself was still live). - delete ShareTracker::should_punish_version (share_tracker.hpp) and its live head-decoration call (formerly :1198, reason=1); head-scoring now punishes only naughty heads, matching canonical Phase-4 tip selection - delete dead AutoRatchet::validate_version_switch (auto_ratchet.hpp; zero callers in src/impl/btc/) — the version gate is the check() 60% PPLNS-weighted switch rule in share_check step 2 - mirrors the landed LTC (share_tracker.hpp:2670) / DGB (:2651) removal, with the same F10/(b) removal marker - new f10b_version_punish_removal_test.cpp (3 KATs) locks the accept-gate vs head-selection invariant on the version axis; full btc_share_test 9/9 Scope: src/impl/btc/ only. Consensus-bearing (tip selection) — hold for operator merge. Co-authored-by: frstrtr <frstrtr@users.noreply.github.com>
1 parent 9c41a93 commit 264ece3

4 files changed

Lines changed: 129 additions & 85 deletions

File tree

src/impl/btc/auto_ratchet.hpp

Lines changed: 5 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -273,60 +273,11 @@ class AutoRatchet
273273
return {current_version, target_version_};
274274
}
275275

276-
/// Validate a version switch between consecutive shares.
277-
/// Returns empty string if valid, error message if invalid.
278-
/// Implements the 60% switch rule from Python check() method.
279-
static std::string validate_version_switch(
280-
int64_t share_version, int64_t prev_version,
281-
ShareTracker& tracker, const uint256& prev_hash)
282-
{
283-
// Same version — always ok
284-
if (share_version == prev_version)
285-
return {};
286-
287-
int32_t height = tracker.chain.get_height(prev_hash);
288-
uint32_t chain_length = PoolConfig::chain_length();
289-
290-
if (height < static_cast<int32_t>(chain_length))
291-
{
292-
// Not enough history for version switch
293-
if (share_version > prev_version)
294-
return "version switch without enough history";
295-
return {}; // downgrade ok without history
296-
}
297-
298-
// Upgrade: requires 60% in sampling window [CHAIN_LENGTH*9/10, CHAIN_LENGTH]
299-
if (share_version == prev_version + 1)
300-
{
301-
uint32_t window_start = (chain_length * 9) / 10;
302-
uint32_t window_size = chain_length / 10;
303-
auto ancestor = tracker.chain.get_nth_parent_key(prev_hash, window_start);
304-
auto counts = tracker.get_desired_version_counts(ancestor, window_size);
305-
306-
int64_t new_ver_count = 0;
307-
int64_t total_count = 0;
308-
for (auto& [ver, cnt] : counts)
309-
{
310-
total_count += cnt;
311-
if (ver >= share_version)
312-
new_ver_count += cnt;
313-
}
314-
315-
if (total_count > 0 && new_ver_count * 100 < total_count * SWITCH_THRESHOLD)
316-
return "version switch without enough hash power upgraded ("
317-
+ std::to_string(new_ver_count * 100 / total_count)
318-
+ "% < " + std::to_string(SWITCH_THRESHOLD) + "%)";
319-
return {};
320-
}
321-
322-
// Downgrade by 1 (AutoRatchet deactivation): allowed
323-
if (share_version == prev_version - 1)
324-
return {};
325-
326-
// Multi-version jump: not allowed
327-
return "invalid version jump from " + std::to_string(prev_version)
328-
+ " to " + std::to_string(share_version);
329-
}
276+
// F10 (per ltc): validate_version_switch is intentionally absent — the
277+
// single source of truth for the version-switch gate is share_check step 2,
278+
// which calls ShareTracker::get_desired_version_weights and matches p2pool
279+
// check() (data.py:1396-1414) exactly. The VOTING tail guard above stays
280+
// inline and work-weighted (mint<->accept coupling).
330281

331282
private:
332283
std::string state_file_;

src/impl/btc/share_tracker.hpp

Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1190,16 +1190,16 @@ class ShareTracker
11901190
if (!head_idx) continue;
11911191
int64_t ts = head_idx->time_seen;
11921192

1193-
// Punish heads: version obsolescence OR naughty (invalid block)
1193+
// Punish heads: naughty (invalid block) only.
1194+
// F10/(b): the non-canonical 95%-version-obsolescence punish
1195+
// is dropped — canonical Phase-4 head-scoring punishes only
1196+
// naughty heads. The version gate is the check() 60% weighted
1197+
// switch rule (share_check), not head-scoring.
11941198
int32_t reason = 0;
11951199
{
1196-
auto share_version = chain.get_share(hh).version();
1197-
auto lookbehind = static_cast<int32_t>(PoolConfig::chain_length());
1198-
if (should_punish_version(hh, share_version, lookbehind))
1199-
reason = 1;
12001200
auto* idx = chain.get_index(hh);
12011201
if (idx && idx->naughty > 0)
1202-
reason = std::max(reason, idx->naughty);
1202+
reason = idx->naughty;
12031203
}
12041204

12051205
// p2pool: sort key = (work - min(punish,1)*ata(target), -reason, -time_seen)
@@ -2683,30 +2683,12 @@ class ShareTracker
26832683
return result;
26842684
}
26852685

2686-
// Returns true if shares at `share_version` should be punished because
2687-
// a newer version has reached the 95% activation threshold.
2688-
// Python ref: share.check() version_after_check logic
2689-
bool should_punish_version(const uint256& share_hash, int64_t share_version, int32_t lookbehind)
2690-
{
2691-
if (!chain.contains(share_hash))
2692-
return false;
2693-
auto counts = get_desired_version_counts(share_hash, lookbehind);
2694-
auto height = chain.get_height(share_hash);
2695-
auto actual = std::min(lookbehind, height);
2696-
if (actual <= 0)
2697-
return false;
2698-
2699-
// Check if any version higher than share_version has >= 95% support
2700-
for (auto& [ver, count] : counts)
2701-
{
2702-
if (static_cast<int64_t>(ver) > share_version)
2703-
{
2704-
if (count * 100 >= actual * 95) // 95% threshold
2705-
return true;
2706-
}
2707-
}
2708-
return false;
2709-
}
2686+
// F10/(b): should_punish_version (the 95%-obsolescence punish) was removed.
2687+
// It was non-canonical — canonical p2pool check() has no 95% obsolescence
2688+
// rule; the AutoRatchet (work-weighted, per #290) plus the 60% weighted
2689+
// switch rule (share_check step 2) are the only version gates. Keeping it
2690+
// would punish/score-down shares canonical accepts, breaking the #81 zero-
2691+
// divergence gate. Head-scoring (Phase 4) now punishes only naughty heads.
27102692

27112693
private:
27122694
std::vector<stale_callback_t> m_stale_callbacks;

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)
4+
add_executable(btc_share_test share_test.cpp f11_donation_invariance_test.cpp f10b_version_punish_removal_test.cpp)
55
target_link_libraries(btc_share_test PRIVATE
66
GTest::gtest_main GTest::gtest
77
core btc
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// F10/(b) version-punish-removal KAT — guards the #288 tip-selection axis.
2+
//
3+
// This branch deletes should_punish_version() (the non-canonical 95%-flat-count
4+
// version-obsolescence punish, formerly src/impl/btc/share_tracker.hpp) and its
5+
// live head-decoration call (formerly share_tracker.hpp:1198, reason=1). It
6+
// mirrors the already-landed LTC (share_tracker.hpp:2670) / DGB (:2654) deletion.
7+
//
8+
// Backfills the coverage gap that let the #288-class divergence ship: there was
9+
// no test asserting that BTC tip-selection agrees with the canonical accept gate
10+
// on the version axis, so the extra punish down-ranked tips the gate accepts and
11+
// nobody noticed. This BTC twin locks the invariant.
12+
//
13+
// The two predicates this test pins (both quoted verbatim from the code as it
14+
// stood / stands):
15+
//
16+
// ACCEPT GATE (canonical, SURVIVES — src/impl/btc/share_check.hpp:1793,
17+
// matching p2pool data.py check() lines 1396-1414):
18+
// reject boundary iff new_ver_weight*100 < total_weight*60
19+
// i.e. a one-version upgrade boundary is VALID when the new version holds
20+
// >= 60% of the PPLNS-WEIGHTED desired-version support in the sampling window.
21+
//
22+
// PUNISH (non-canonical, REMOVED — former should_punish_version):
23+
// punish (reason=1) iff newer_version_count*100 >= actual*95
24+
// a flat (un-weighted) head-count rule with NO basis in p2pool check().
25+
//
26+
// #288 axis: there exists a desired-version distribution where the canonical
27+
// accept gate deems an upgrade boundary VALID while the removed flat-count
28+
// punish would have down-ranked that same tip. After this deletion the only
29+
// head-scoring punish contributor is `naughty` (invalid block), so a non-naughty
30+
// tip the accept gate accepts is never down-ranked. This test proves the
31+
// divergence existed and that the surviving gate is now the sole version decider.
32+
33+
#include <gtest/gtest.h>
34+
35+
#include <cstdint>
36+
#include <map>
37+
38+
namespace {
39+
40+
// Canonical accept gate, byte-for-byte the predicate at share_check.hpp:1793.
41+
// weights: desired-version -> PPLNS weight (work, target_to_average_attempts).
42+
// Returns true if a boundary share at `share_ver` (== parent_ver + 1) is VALID.
43+
bool accept_gate_valid(const std::map<int64_t, uint64_t>& weights, int64_t share_ver) {
44+
uint64_t new_ver_weight = 0, total_weight = 0;
45+
for (auto& [ver, w] : weights) {
46+
total_weight += w;
47+
if (ver == share_ver) new_ver_weight += w;
48+
}
49+
// Canonical: counts.get(self.VERSION,0) < sum(counts)*60//100 -> reject
50+
return !(new_ver_weight * 100 < total_weight * 60);
51+
}
52+
53+
// The REMOVED flat-count punish, reproduced exactly as should_punish_version
54+
// computed it. counts: version -> flat head count. `actual` = window size.
55+
// Returns true if a share at `share_ver` WOULD have been punished (reason=1).
56+
bool removed_flat_punish(const std::map<int64_t, uint64_t>& counts,
57+
int64_t share_ver, uint64_t actual) {
58+
if (actual == 0) return false;
59+
for (auto& [ver, count] : counts)
60+
if (ver > share_ver && count * 100 >= actual * 95) // 95% threshold
61+
return true;
62+
return false;
63+
}
64+
65+
// Head-scoring reason after the deletion: the only contributor is `naughty`.
66+
// Mirrors share_tracker.hpp:1196-1198 post-change (reason = idx->naughty).
67+
int32_t head_reason_after_deletion(int32_t naughty) { return naughty; }
68+
69+
} // namespace
70+
71+
// Core #288 case: a single newer version dominates by flat head count (>=95%)
72+
// but the upgraded version's WEIGHTED support clears 60%. The accept gate
73+
// accepts the boundary; the removed punish would have down-ranked it.
74+
TEST(BTC_F10b_VersionPunishRemoval, AcceptGateAndRemovedPunishDiverge) {
75+
// 10 heads desire v37, the upgrade target; the boundary share is v37.
76+
// By flat count v37 holds 95% (would have punished a v36 share as obsolete),
77+
// but in weighted terms v37 also clears 60% so the boundary is canonical.
78+
const int64_t share_ver = 37; // parent_ver(36) + 1
79+
80+
// Weighted view used by the surviving accept gate: v37 holds 7/10 weight.
81+
std::map<int64_t, uint64_t> weights{{36, 300}, {37, 700}};
82+
EXPECT_TRUE(accept_gate_valid(weights, share_ver))
83+
<< "canonical 60% weighted gate must accept the v37 boundary";
84+
85+
// The removed flat-count punish keyed off a NEWER version (v38) reaching
86+
// 95% would have flagged the (older) v37 tip as obsolete and down-ranked it.
87+
std::map<int64_t, uint64_t> flat_counts{{37, 0}, {38, 96}};
88+
EXPECT_TRUE(removed_flat_punish(flat_counts, share_ver, /*actual=*/100))
89+
<< "the removed punish WOULD have down-ranked the v37 tip (the #288 miss)";
90+
91+
// Post-deletion, head-scoring no longer consults version at all: a
92+
// non-naughty tip gets reason 0 and is NOT down-ranked.
93+
EXPECT_EQ(0, head_reason_after_deletion(/*naughty=*/0))
94+
<< "after F10/(b) a non-naughty tip is never down-ranked on the version axis";
95+
}
96+
97+
// Boundary just under canonical: 60% weighted gate REJECTS, so this is purely a
98+
// gate decision — nothing about head-scoring re-introduces a competing punish.
99+
TEST(BTC_F10b_VersionPunishRemoval, AcceptGateRejectsBelowSixtyWeighted) {
100+
std::map<int64_t, uint64_t> weights{{36, 410}, {37, 590}}; // 59% < 60%
101+
EXPECT_FALSE(accept_gate_valid(weights, /*share_ver=*/37))
102+
<< "below 60% weighted support the canonical gate alone rejects the boundary";
103+
}
104+
105+
// The surviving head-scoring punish is naughty-only and order-independent of
106+
// the version distribution: naughty>0 still down-ranks (invalid block), and that
107+
// is the ONLY remaining reason a head is down-ranked.
108+
TEST(BTC_F10b_VersionPunishRemoval, NaughtyIsTheSoleSurvivingPunish) {
109+
EXPECT_EQ(0, head_reason_after_deletion(0));
110+
EXPECT_EQ(3, head_reason_after_deletion(3)); // naughty propagation preserved
111+
}

0 commit comments

Comments
 (0)