Skip to content

Commit 7702eca

Browse files
committed
dgb(phase-b): lift PPLNS weight walk into shared SSOT (step 1)
Extract generate_share_transaction() step-1 PPLNS tracker walk into dgb::coin::compute_pplns_weight_walk(), the producer-side counterpart of the steps 2-3 lift in pplns_payout_split.hpp (#328). The share-verification path and the per-connection Stratum coinbase emission path now draw one tracker-walk implementation, so the weight map a miner hashes cannot drift from the one the verifier enforces. Verbatim lift: exact V36 depth-decay vs pre-V36 grandparent-start branch, the data.py insufficient-depth guard, and the block-target/spread max_weight cap are unchanged. Result type is deduced from the tracker (decltype) to avoid the share_tracker.hpp <-> share_check.hpp include cycle. KAT WeightWalkSSOTContract locks the helper contract (verbatim V36 forward + safe-empty on null/absent parent); the unchanged generate_share_transaction fixtures prove value-level byte-identity. dgb_share_test 27/27, dgb_connection_coinbase_test 8/8.
1 parent 2b50be4 commit 7702eca

3 files changed

Lines changed: 187 additions & 52 deletions

File tree

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#pragma once
2+
// ─────────────────────────────────────────────────────────────────────────────
3+
// SSOT: PPLNS weight walk — step 1 of generate_share_transaction(), lifted so the
4+
// share-VERIFICATION path (share_check.hpp generate_share_transaction) and the
5+
// per-connection Stratum coinbase EMISSION path (work_source.cpp
6+
// build_connection_coinbase producer seam, bound in main_dgb.cpp) draw ONE
7+
// tracker-walk implementation — no second copy to drift a payout satoshi.
8+
//
9+
// Counterpart of the steps 2-3 lift in pplns_payout_split.hpp (#328): together
10+
// compute_pplns_weight_walk() + compute_pplns_payout_split() are the full
11+
// former-inline body of generate_share_transaction()'s PPLNS computation.
12+
// Verbatim lift — exact V36 (exponential depth-decay) vs pre-V36 (flat
13+
// cumulative, grandparent start) branch, the data.py:762-764 insufficient-depth
14+
// guard, the block-target / spread / 65535 max_weight cap, and the
15+
// unlimited-weight V36 sentinel.
16+
// Reference: frstrtr/p2pool-merged-v36 data.py:879/884-885, work.py:759.
17+
//
18+
// NOTE: the result type is DEDUCED from the tracker (dgb::CumulativeWeights) via
19+
// decltype rather than named/included directly — share_tracker.hpp includes
20+
// share_check.hpp, which includes this header, so naming the type here (or
21+
// including share_tracker.hpp) would form a parse-time include cycle when
22+
// share_tracker.hpp is the outer include. The dependent return type resolves
23+
// only at instantiation, where TrackerT is complete.
24+
// ─────────────────────────────────────────────────────────────────────────────
25+
26+
#include <core/coin_params.hpp>
27+
#include <core/target_utils.hpp>
28+
#include <core/uint256.hpp>
29+
30+
#include <algorithm>
31+
#include <cstdint>
32+
#include <stdexcept>
33+
#include <string>
34+
#include <utility>
35+
36+
namespace dgb::coin {
37+
38+
// Walk the sharechain from `prev_hash` (the new share's parent / the current
39+
// tip when emitting work) backward and accumulate the PPLNS weight map exactly
40+
// as the verifier does. `block_bits` is the block-header nBits feeding the
41+
// pre-V36 max_weight cap (share.m_min_header.m_bits on the verify path).
42+
//
43+
// * prev_hash null or absent from the chain -> empty weights (caller treats
44+
// as a safe coinbase-only / empty job — the pre-wire behavior).
45+
// * chain shorter than real_chain_length -> throws std::invalid_argument,
46+
// the SAME boundary generate_share_transaction() rejects, so emission and
47+
// verification refuse identical insufficient-depth states.
48+
template <typename TrackerT>
49+
inline auto compute_pplns_weight_walk(
50+
TrackerT& tracker,
51+
const uint256& prev_hash,
52+
uint32_t block_bits,
53+
const core::CoinParams& params,
54+
bool use_v36_pplns)
55+
-> decltype(tracker.get_v36_decayed_cumulative_weights(
56+
prev_hash, std::int32_t{0}, std::declval<const uint288&>()))
57+
{
58+
using Result = decltype(tracker.get_v36_decayed_cumulative_weights(
59+
prev_hash, std::int32_t{0}, std::declval<const uint288&>()));
60+
Result out{};
61+
62+
if (prev_hash.IsNull() || !tracker.chain.contains(prev_hash))
63+
return out; // no parent in chain -> empty (safe coinbase-only job).
64+
65+
// p2pool data.py:762-764 — refuse to compute PPLNS with insufficient depth.
66+
// Without this guard, attempt_verify() (which allows CHAIN_LENGTH+1) can
67+
// trigger a PPLNS walk that terminates early, producing wrong coinbase
68+
// amounts and causing persistent GENTX-MISMATCH during bootstrap.
69+
auto chain_len = static_cast<int32_t>(params.real_chain_length);
70+
{
71+
auto pplns_height = tracker.chain.get_height(prev_hash);
72+
auto pplns_last = tracker.chain.get_last(prev_hash);
73+
if (!(pplns_height >= chain_len || pplns_last.IsNull()))
74+
throw std::invalid_argument(
75+
"share chain not long enough for PPLNS verification (height="
76+
+ std::to_string(pplns_height) + " need="
77+
+ std::to_string(chain_len) + ")");
78+
}
79+
80+
// block_target from block header bits (matches Python: self.header['bits'].target)
81+
auto block_target = chain::bits_to_target(block_bits);
82+
auto max_weight = chain::target_to_average_attempts(block_target)
83+
* params.spread * 65535;
84+
85+
// PPLNS formula selected by runtime v36_active (AutoRatchet state), not
86+
// compile-time share version. Ref: p2pool data.py:879, work.py:759.
87+
if (use_v36_pplns) {
88+
// V36 PPLNS: exponential depth-decay, walk from parent.
89+
uint288 unlimited_weight;
90+
unlimited_weight.SetHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
91+
out = tracker.get_v36_decayed_cumulative_weights(prev_hash, chain_len, unlimited_weight);
92+
} else {
93+
// Pre-V36 PPLNS: flat cumulative weights (no decay). CRITICAL: walk from
94+
// GRANDPARENT for HEIGHT-1 shares. p2pool data.py:884-885:
95+
// _pplns_start = previous_share.share_data['previous_share_hash']
96+
// _pplns_max_shares = max(0, min(height, REAL_CHAIN_LENGTH) - 1)
97+
uint256 pplns_start;
98+
tracker.chain.get(prev_hash).share.invoke([&](auto* s) {
99+
pplns_start = s->m_prev_hash; // grandparent
100+
});
101+
auto available = tracker.chain.get_height(prev_hash);
102+
auto walk_count = static_cast<int32_t>(
103+
std::max(0, std::min(chain_len, available) - 1));
104+
105+
if (!pplns_start.IsNull() && tracker.chain.contains(pplns_start) && walk_count > 0) {
106+
out = tracker.get_cumulative_weights(pplns_start, walk_count, max_weight);
107+
}
108+
}
109+
return out;
110+
}
111+
112+
} // namespace dgb::coin

src/impl/dgb/share_check.hpp

Lines changed: 11 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
#include <impl/dgb/coin/gentx_coinbase.hpp>
1212
#include <impl/dgb/coin/pplns_payout_split.hpp>
13+
#include <impl/dgb/coin/pplns_weight_walk.hpp> // SSOT: PPLNS step-1 tracker walk (shared w/ emission)
1314

1415
#include <core/coin_params.hpp>
1516
#include <core/hash.hpp>
@@ -967,59 +968,17 @@ uint256 generate_share_transaction(const ShareT& share, TrackerT& tracker, const
967968
uint288 total_weight;
968969
uint288 total_donation_weight;
969970

970-
if (!prev_hash.IsNull() && tracker.chain.contains(prev_hash))
971+
// Step 1 of the PPLNS computation now lives in the shared SSOT
972+
// dgb::coin::compute_pplns_weight_walk() so the per-connection Stratum
973+
// coinbase EMISSION path walks the tracker through the SAME code this
974+
// VERIFICATION path does — byte-identical weights and identical insufficient-
975+
// depth guard, no second walk to drift. Verbatim lift; see pplns_weight_walk.hpp.
971976
{
972-
// p2pool data.py:762-764 — refuse to compute PPLNS with insufficient depth.
973-
// Without this guard, attempt_verify() (which allows CHAIN_LENGTH+1) can
974-
// trigger a PPLNS walk that terminates early, producing wrong coinbase
975-
// amounts and causing persistent GENTX-MISMATCH during bootstrap.
976-
auto chain_len = static_cast<int32_t>(params.real_chain_length);
977-
{
978-
auto pplns_height = tracker.chain.get_height(prev_hash);
979-
auto pplns_last = tracker.chain.get_last(prev_hash);
980-
if (!(pplns_height >= chain_len || pplns_last.IsNull()))
981-
throw std::invalid_argument(
982-
"share chain not long enough for PPLNS verification (height="
983-
+ std::to_string(pplns_height) + " need="
984-
+ std::to_string(chain_len) + ")");
985-
}
986-
987-
// block_target from block header bits (matches Python: self.header['bits'].target)
988-
auto block_target = chain::bits_to_target(share.m_min_header.m_bits);
989-
auto max_weight = chain::target_to_average_attempts(block_target)
990-
* params.spread * 65535;
991-
992-
// PPLNS formula selected by runtime v36_active (AutoRatchet state),
993-
// not compile-time share version. Ref: p2pool data.py:879, work.py:759.
994-
if (use_v36_pplns) {
995-
// V36 PPLNS: exponential depth-decay, walk from parent
996-
uint288 unlimited_weight;
997-
unlimited_weight.SetHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
998-
auto result = tracker.get_v36_decayed_cumulative_weights(prev_hash, chain_len, unlimited_weight);
999-
weights = std::move(result.weights);
1000-
total_weight = result.total_weight;
1001-
total_donation_weight = result.total_donation_weight;
1002-
} else {
1003-
// Pre-V36 PPLNS: flat cumulative weights (no decay)
1004-
// CRITICAL: Walk from GRANDPARENT for HEIGHT-1 shares.
1005-
// p2pool data.py:884-885:
1006-
// _pplns_start = previous_share.share_data['previous_share_hash']
1007-
// _pplns_max_shares = max(0, min(height, REAL_CHAIN_LENGTH) - 1)
1008-
uint256 pplns_start;
1009-
tracker.chain.get(prev_hash).share.invoke([&](auto* s) {
1010-
pplns_start = s->m_prev_hash; // grandparent
1011-
});
1012-
auto available = tracker.chain.get_height(prev_hash);
1013-
auto walk_count = static_cast<int32_t>(
1014-
std::max(0, std::min(chain_len, available) - 1));
1015-
1016-
if (!pplns_start.IsNull() && tracker.chain.contains(pplns_start) && walk_count > 0) {
1017-
auto result = tracker.get_cumulative_weights(pplns_start, walk_count, max_weight);
1018-
weights = std::move(result.weights);
1019-
total_weight = result.total_weight;
1020-
total_donation_weight = result.total_donation_weight;
1021-
}
1022-
}
977+
auto cw = dgb::coin::compute_pplns_weight_walk(
978+
tracker, prev_hash, share.m_min_header.m_bits, params, use_v36_pplns);
979+
weights = std::move(cw.weights);
980+
total_weight = cw.total_weight;
981+
total_donation_weight = cw.total_donation_weight;
1023982
}
1024983

1025984
// --- 2. Convert weights to exact integer payout amounts ---

src/impl/dgb/test/share_test.cpp

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <impl/dgb/config_coin.hpp>
1919
#include <impl/dgb/share.hpp>
2020
#include <impl/dgb/share_tracker.hpp> // DensePPLNSWindow — V36 decayed-PPLNS SSOT
21+
#include <impl/dgb/coin/pplns_weight_walk.hpp> // SSOT: PPLNS step-1 tracker walk (shared emission/verify)
2122
#include <impl/dgb/params.hpp> // make_coin_params — assembled CoinParams SSOT
2223
#include <impl/dgb/coin/rpc_conf.hpp> // #82 external-daemon RPC creds (digibyte.conf)
2324
#include <impl/dgb/auto_ratchet.hpp> // Phase B: mint-side share-version ratchet
@@ -248,6 +249,69 @@ TEST(DGB_share_test, DesiredVersionWeightsByAttempts)
248249
EXPECT_GT(w.at(35u), w.at(36u));
249250
}
250251

252+
// ── compute_pplns_weight_walk() SSOT contract KAT ───────────────────────────
253+
// The PPLNS step-1 tracker walk is now ONE helper shared by the share-
254+
// VERIFICATION path (generate_share_transaction) and the per-connection Stratum
255+
// coinbase EMISSION path (build_connection_coinbase producer seam). Value-level
256+
// faithfulness of the lift is proven by the unchanged generate_share_transaction
257+
// fixtures (they route through the helper now); this KAT independently locks the
258+
// helper's CONTRACT so a later edit that breaks the emission path is caught even
259+
// where no verifier test covers it:
260+
// (1) it forwards verbatim to get_v36_decayed_cumulative_weights on the V36
261+
// path (byte-identical weights/totals), and
262+
// (2) it returns an empty, safe result (no throw) when the parent is null or
263+
// absent from the chain — the "safe coinbase-only / empty job" the seam
264+
// relies on while the tip is unknown.
265+
TEST(DGB_share_test, WeightWalkSSOTContract)
266+
{
267+
const core::CoinParams params = dgb::make_coin_params(/*testnet=*/false);
268+
269+
dgb::ShareTracker tracker;
270+
auto mk = [&](const char* hh, const char* ph, uint32_t bits) {
271+
auto* s = new dgb::MergedMiningShare();
272+
s->m_hash.SetHex(hh);
273+
if (ph) s->m_prev_hash.SetHex(ph); else s->m_prev_hash.SetNull();
274+
s->m_bits = bits;
275+
s->m_max_bits = bits;
276+
dgb::ShareType st; st = s;
277+
tracker.add(st);
278+
};
279+
const char* h0 = "00000000000000000000000000000000000000000000000000000000000000b0";
280+
const char* h1 = "00000000000000000000000000000000000000000000000000000000000000b1";
281+
const char* h2 = "00000000000000000000000000000000000000000000000000000000000000b2";
282+
mk(h0, nullptr, 0x1e0fffff);
283+
mk(h1, h0, 0x1e0fffff);
284+
mk(h2, h1, 0x1e0fffff);
285+
286+
uint256 tip; tip.SetHex(h2);
287+
const auto chain_len = static_cast<int32_t>(params.real_chain_length);
288+
289+
// (1) V36 path forwards verbatim to the tracker's decayed-weights SSOT.
290+
uint288 unlimited_weight;
291+
unlimited_weight.SetHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
292+
const auto direct = tracker.get_v36_decayed_cumulative_weights(tip, chain_len, unlimited_weight);
293+
const auto via_helper = dgb::coin::compute_pplns_weight_walk(
294+
tracker, tip, /*block_bits=*/0x1e0fffff, params, /*use_v36_pplns=*/true);
295+
EXPECT_EQ(via_helper.weights, direct.weights);
296+
EXPECT_EQ(via_helper.total_weight, direct.total_weight);
297+
EXPECT_EQ(via_helper.total_donation_weight, direct.total_donation_weight);
298+
299+
// (2a) null parent -> empty, safe (no throw).
300+
const auto null_walk = dgb::coin::compute_pplns_weight_walk(
301+
tracker, uint256::ZERO, 0x1e0fffff, params, /*use_v36_pplns=*/true);
302+
EXPECT_TRUE(null_walk.weights.empty());
303+
EXPECT_TRUE(null_walk.total_weight.IsNull());
304+
EXPECT_TRUE(null_walk.total_donation_weight.IsNull());
305+
306+
// (2b) parent absent from the chain -> empty, safe (no throw).
307+
uint256 absent; absent.SetHex(
308+
"00000000000000000000000000000000000000000000000000000000deadbeef");
309+
const auto absent_walk = dgb::coin::compute_pplns_weight_walk(
310+
tracker, absent, 0x1e0fffff, params, /*use_v36_pplns=*/true);
311+
EXPECT_TRUE(absent_walk.weights.empty());
312+
EXPECT_TRUE(absent_walk.total_weight.IsNull());
313+
}
314+
251315

252316
// ── Address-encoding + relay-policy SSOT KAT (assembled CoinParams) ─────────
253317
// Pins the values make_coin_params() actually hands the pool, on BOTH nets,

0 commit comments

Comments
 (0)