Skip to content

Commit 3004e58

Browse files
authored
Merge pull request #292 from frstrtr/dgb/auto-ratchet-mint
dgb: AutoRatchet mint-side share-version ratchet (Phase B pool/share)
2 parents 1b008f8 + 8b80704 commit 3004e58

2 files changed

Lines changed: 434 additions & 0 deletions

File tree

src/impl/dgb/auto_ratchet.hpp

Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
#pragma once
2+
3+
// AutoRatchet: autonomous share version transition state machine (DGB port).
4+
//
5+
// Manages DGB's <baseline> -> V36 share format transition without manual
6+
// operator coordination. Persists state to JSON so restarts don't regress
7+
// once the network has confirmed a new version.
8+
//
9+
// State machine (identical shape to ltc::AutoRatchet — bucket-2 v36-native
10+
// shared structure, standardized cross-coin toward the v37 unified form):
11+
//
12+
// VOTING -------(95% desired_version >= target)------> ACTIVATED
13+
// ^ |
14+
// |---(<50% desired_version >= target)---< |
15+
// |
16+
// (sustained 2*CHAIN_LENGTH at 95% new-format shares) |
17+
// v
18+
// VOTING <--(follows old network, keeps voting)------- CONFIRMED
19+
// ^ |
20+
// |---(<50% votes, network genuinely old)---< |
21+
// (permanent on restart) |
22+
// CONFIRMED <--------------------------+
23+
//
24+
// Port of p2pool-v36 data.py AutoRatchet (lines 2109-2344), mirroring
25+
// src/impl/ltc/auto_ratchet.hpp.
26+
//
27+
// DGB DIVERGENCE FROM LTC (per operator 2026-06-17 re-scope): DGB's live
28+
// pre-v36 baseline is NOT necessarily V35 — it conforms to the DGB oracle
29+
// frstrtr/p2pool-dgb-scrypt, which runs an OLDER share version than LTC's
30+
// v35. The VOTING-state output version is therefore a CONSTRUCTOR PARAMETER
31+
// (base_version_), NOT the ltc hardcode `target_version_ - 1`. The exact live
32+
// value is a [decision-needed] for the wiring step; the default below keeps
33+
// the module compilable and KAT-exercisable but MUST be overridden at the
34+
// run-loop wire-in once confirmed against the oracle. Until then this module
35+
// is surface-for-tap (unwired).
36+
37+
#include "config_pool.hpp"
38+
#include "share_tracker.hpp"
39+
#include <core/log.hpp>
40+
41+
#include <nlohmann/json.hpp>
42+
#include <algorithm>
43+
#include <chrono>
44+
#include <cstdint>
45+
#include <fstream>
46+
#include <string>
47+
#include <utility>
48+
49+
namespace dgb
50+
{
51+
52+
enum class RatchetState : uint8_t
53+
{
54+
VOTING = 0, // producing old-format shares, voting for upgrade
55+
ACTIVATED = 1, // producing new-format shares, monitoring support
56+
CONFIRMED = 2 // permanent: new format confirmed, survives restart
57+
};
58+
59+
inline const char* ratchet_state_str(RatchetState s)
60+
{
61+
switch (s) {
62+
case RatchetState::VOTING: return "VOTING";
63+
case RatchetState::ACTIVATED: return "ACTIVATED";
64+
case RatchetState::CONFIRMED: return "CONFIRMED";
65+
}
66+
return "UNKNOWN";
67+
}
68+
69+
class AutoRatchet
70+
{
71+
public:
72+
// Thresholds (matching Python reference — bucket-2 standardized).
73+
static constexpr int ACTIVATION_THRESHOLD = 95; // % votes to activate
74+
static constexpr int DEACTIVATION_THRESHOLD = 50; // % below which to revert
75+
static constexpr int CONFIRMATION_MULTIPLIER = 2; // confirm after 2x CHAIN_LENGTH
76+
static constexpr int SWITCH_THRESHOLD = 60; // % required for format switch in validation
77+
78+
// base_version: the share version DGB mints while VOTING. DGB-specific —
79+
// see file header. Defaults to target_version-1 only as a compile default.
80+
explicit AutoRatchet(const std::string& state_file_path = "",
81+
int64_t target_version = 36,
82+
int64_t base_version = -1)
83+
: state_file_(state_file_path)
84+
, target_version_(target_version)
85+
, base_version_(base_version >= 0 ? base_version : target_version - 1)
86+
{
87+
load();
88+
}
89+
90+
RatchetState state() const { return state_; }
91+
int64_t target_version() const { return target_version_; }
92+
int64_t base_version() const { return base_version_; }
93+
94+
/// Determine which share version to produce based on network state.
95+
/// Returns (share_version, desired_version_to_vote).
96+
std::pair<int64_t, int64_t> get_share_version(
97+
ShareTracker& tracker,
98+
const uint256& best_share_hash)
99+
{
100+
const int64_t current_version = base_version_; // DGB: oracle baseline, NOT target-1
101+
const uint32_t chain_length = PoolConfig::chain_length();
102+
const uint32_t confirmation_window = chain_length * CONFIRMATION_MULTIPLIER;
103+
104+
// No chain — use persisted state for bootstrap
105+
if (best_share_hash.IsNull() ||
106+
!tracker.chain.contains(best_share_hash) ||
107+
tracker.chain.get_height(best_share_hash) < 1)
108+
{
109+
if (state_ == RatchetState::CONFIRMED)
110+
return {target_version_, target_version_};
111+
return {current_version, target_version_};
112+
}
113+
114+
// Count votes and actual new-format shares in window.
115+
// p2pool uses tracker.get_height() (chain depth) for both sampling
116+
// and confirmation counting. This matches data.py:2488,2576.
117+
int32_t height = tracker.chain.get_height(best_share_hash);
118+
int32_t sample = std::min(height, static_cast<int32_t>(chain_length));
119+
120+
int32_t target_votes = 0; // shares voting desired_version >= target
121+
int32_t target_shares = 0; // shares actually IN target format
122+
int32_t total = 0;
123+
124+
auto chain_view = tracker.chain.get_chain(best_share_hash, sample);
125+
for (auto [hash, data] : chain_view)
126+
{
127+
++total;
128+
data.share.invoke([&](auto* obj) {
129+
if (static_cast<int64_t>(obj->m_desired_version) >= target_version_)
130+
++target_votes;
131+
if (static_cast<int64_t>(std::remove_pointer_t<decltype(obj)>::version) >= target_version_)
132+
++target_shares;
133+
});
134+
}
135+
136+
if (total == 0)
137+
{
138+
if (state_ == RatchetState::CONFIRMED)
139+
return {target_version_, target_version_};
140+
return {current_version, target_version_};
141+
}
142+
143+
int vote_pct = (target_votes * 100) / total;
144+
int share_pct = (target_shares * 100) / total;
145+
bool full_window = (total >= static_cast<int32_t>(chain_length));
146+
147+
// --- State transitions ---
148+
auto old_state = state_;
149+
150+
if (state_ == RatchetState::VOTING)
151+
{
152+
if (full_window && vote_pct >= ACTIVATION_THRESHOLD)
153+
{
154+
// WORK-WEIGHTED tail guard (mint<->accept coupling). The
155+
// consensus accept gate (share_check step 2 / p2pool check()
156+
// data.py:1399) keys the 60% switch rule off
157+
// get_desired_version_counts, which in canonical p2pool
158+
// (data.py:2651) weights each share by
159+
// target_to_average_attempts(target) -- i.e. WORK, not a flat
160+
// head-count. AutoRatchet must evaluate the SAME work-weighted
161+
// 60% rule over the SAME [9/10*CL, CL] window before it
162+
// activates; otherwise a 95%-by-COUNT activation can outrun the
163+
// 60%-by-WORK accept gate, the node mints a V36 boundary share
164+
// every peer rejects, and the crossing wedges. (DGB accept gate
165+
// already work-weighted: #249/#289.)
166+
uint32_t tail_start = (chain_length * 9) / 10;
167+
uint32_t tail_size = chain_length / 10;
168+
auto tail_ancestor = tracker.chain.get_nth_parent_key(best_share_hash, tail_start);
169+
auto tail_weights = tracker.get_desired_version_weights(tail_ancestor, tail_size);
170+
171+
// mapped_type is the work-weight accumulator (uint288); default 0.
172+
decltype(tail_weights)::mapped_type tail_target{}, tail_total{};
173+
for (auto& [ver, w] : tail_weights) {
174+
tail_total = tail_total + w;
175+
if (static_cast<int64_t>(ver) >= target_version_)
176+
tail_target = tail_target + w;
177+
}
178+
// Canonical: counts.get(VERSION,0) < sum(counts)*60//100
179+
bool tail_ok = !(tail_target * uint32_t(100) < tail_total * uint32_t(SWITCH_THRESHOLD));
180+
181+
if (!tail_ok) {
182+
static int tail_log = 0;
183+
if (tail_log++ % 20 == 0)
184+
LOG_INFO << "[AutoRatchet] VOTING: full window " << vote_pct
185+
<< "% >= " << ACTIVATION_THRESHOLD << "% but oldest 10% work-weighted V"
186+
<< target_version_ << " desire < " << SWITCH_THRESHOLD << "%) — waiting";
187+
// Don't transition yet
188+
}
189+
else
190+
{
191+
state_ = RatchetState::ACTIVATED;
192+
activated_at_ = now_seconds();
193+
activated_height_ = height;
194+
// Credit retroactive shares for late-joining nodes
195+
// p2pool data.py:2535: retroactive = max(0, height - net.CHAIN_LENGTH)
196+
int32_t retroactive = std::max(0, height - static_cast<int32_t>(chain_length));
197+
confirm_count_ = retroactive;
198+
last_seen_height_ = height;
199+
200+
LOG_INFO << "[AutoRatchet] VOTING -> ACTIVATED ("
201+
<< vote_pct << "% of " << total << " shares vote V"
202+
<< target_version_ << ", window=" << chain_length
203+
<< ", retroactive=" << retroactive << ")";
204+
205+
// Skip to CONFIRMED if chain is already deep enough
206+
if (retroactive >= static_cast<int32_t>(confirmation_window) &&
207+
share_pct >= ACTIVATION_THRESHOLD)
208+
{
209+
state_ = RatchetState::CONFIRMED;
210+
confirmed_at_ = now_seconds();
211+
LOG_INFO << "[AutoRatchet] VOTING -> CONFIRMED (retroactive: "
212+
<< retroactive << " >= " << confirmation_window << ")";
213+
}
214+
save();
215+
} // else (tail guard passed)
216+
}
217+
}
218+
else if (state_ == RatchetState::ACTIVATED)
219+
{
220+
if (full_window && vote_pct < DEACTIVATION_THRESHOLD)
221+
{
222+
// Network genuinely reverted
223+
state_ = RatchetState::VOTING;
224+
activated_at_ = 0;
225+
activated_height_ = 0;
226+
confirm_count_ = 0;
227+
last_seen_height_ = 0;
228+
LOG_INFO << "[AutoRatchet] ACTIVATED -> VOTING ("
229+
<< vote_pct << "% < " << DEACTIVATION_THRESHOLD << "% threshold)";
230+
save();
231+
}
232+
else if (activated_height_ > 0)
233+
{
234+
// Track cumulative height increases using chain depth.
235+
// p2pool data.py:2576: uses tracker.get_height() (chain depth).
236+
if (last_seen_height_ > 0 && height > last_seen_height_)
237+
confirm_count_ += (height - last_seen_height_);
238+
last_seen_height_ = height;
239+
240+
{
241+
static int ac_log = 0;
242+
if (ac_log++ % 20 == 0)
243+
LOG_INFO << "[AutoRatchet] ACTIVATED: vote=" << vote_pct
244+
<< "% share=" << share_pct << "% full=" << (full_window ? "True" : "False")
245+
<< " height=" << height
246+
<< " confirm=" << confirm_count_ << "/" << confirmation_window;
247+
}
248+
249+
if (confirm_count_ >= static_cast<int32_t>(confirmation_window) &&
250+
share_pct >= ACTIVATION_THRESHOLD)
251+
{
252+
state_ = RatchetState::CONFIRMED;
253+
confirmed_at_ = now_seconds();
254+
LOG_INFO << "[AutoRatchet] ACTIVATED -> CONFIRMED ("
255+
<< confirm_count_ << " cumulative shares, "
256+
<< share_pct << "% V" << target_version_ << ")";
257+
save();
258+
}
259+
}
260+
}
261+
else if (state_ == RatchetState::CONFIRMED)
262+
{
263+
// CONFIRMED is permanent, but respect network consensus
264+
if (full_window && vote_pct < DEACTIVATION_THRESHOLD)
265+
{
266+
LOG_WARNING << "[AutoRatchet] CONFIRMED but network is "
267+
<< (100 - vote_pct) << "% old version — following consensus";
268+
return {current_version, target_version_};
269+
}
270+
}
271+
272+
if (old_state != state_)
273+
{
274+
LOG_INFO << "[AutoRatchet] State: " << ratchet_state_str(old_state)
275+
<< " -> " << ratchet_state_str(state_);
276+
}
277+
278+
// Output
279+
if (state_ == RatchetState::ACTIVATED || state_ == RatchetState::CONFIRMED)
280+
return {target_version_, target_version_};
281+
else
282+
return {current_version, target_version_};
283+
}
284+
285+
// F10 (per ltc): validate_version_switch is intentionally absent — the
286+
// single source of truth for the version-switch gate is share_check step 2,
287+
// which calls ShareTracker::get_desired_version_weights and matches p2pool
288+
// check() (data.py:1396-1414) exactly. The VOTING tail guard above stays
289+
// inline and work-weighted (mint<->accept coupling).
290+
291+
private:
292+
std::string state_file_;
293+
int64_t target_version_;
294+
int64_t base_version_;
295+
RatchetState state_ = RatchetState::VOTING;
296+
int64_t activated_at_ = 0;
297+
int32_t activated_height_ = 0;
298+
int64_t confirmed_at_ = 0;
299+
int32_t confirm_count_ = 0;
300+
int32_t last_seen_height_ = 0;
301+
302+
static int64_t now_seconds()
303+
{
304+
return std::chrono::duration_cast<std::chrono::seconds>(
305+
std::chrono::system_clock::now().time_since_epoch()).count();
306+
}
307+
308+
void load()
309+
{
310+
if (state_file_.empty()) return;
311+
try {
312+
std::ifstream f(state_file_);
313+
if (!f.is_open()) return;
314+
nlohmann::json j;
315+
f >> j;
316+
std::string s = j.value("state", "voting");
317+
if (s == "activated") state_ = RatchetState::ACTIVATED;
318+
else if (s == "confirmed") state_ = RatchetState::CONFIRMED;
319+
else state_ = RatchetState::VOTING;
320+
activated_at_ = j.value("activated_at", int64_t(0));
321+
activated_height_ = j.value("activated_height", int32_t(0));
322+
confirmed_at_ = j.value("confirmed_at", int64_t(0));
323+
confirm_count_ = j.value("confirm_count", int32_t(0));
324+
LOG_INFO << "[AutoRatchet] Loaded state: " << ratchet_state_str(state_)
325+
<< " (target=V" << target_version_
326+
<< " base=V" << base_version_
327+
<< " confirmed_at=" << confirmed_at_
328+
<< " confirm_count=" << confirm_count_ << ")";
329+
} catch (const std::exception& e) {
330+
LOG_WARNING << "[AutoRatchet] Failed to load state: " << e.what();
331+
}
332+
}
333+
334+
void save()
335+
{
336+
if (state_file_.empty()) return;
337+
try {
338+
nlohmann::json j;
339+
switch (state_) {
340+
case RatchetState::VOTING: j["state"] = "voting"; break;
341+
case RatchetState::ACTIVATED: j["state"] = "activated"; break;
342+
case RatchetState::CONFIRMED: j["state"] = "confirmed"; break;
343+
}
344+
j["activated_at"] = activated_at_;
345+
j["activated_height"] = activated_height_;
346+
j["confirmed_at"] = confirmed_at_;
347+
j["confirm_count"] = confirm_count_;
348+
j["target_version"] = target_version_;
349+
j["base_version"] = base_version_;
350+
351+
std::ofstream f(state_file_);
352+
f << j.dump(2);
353+
} catch (const std::exception& e) {
354+
LOG_WARNING << "[AutoRatchet] Failed to save state: " << e.what();
355+
}
356+
}
357+
};
358+
359+
} // namespace dgb

0 commit comments

Comments
 (0)