Skip to content

Commit 02180c5

Browse files
authored
Merge pull request #333 from frstrtr/dgb/pplns-weight-walk-ssot
dgb(phase-b): lift PPLNS weight walk into shared SSOT (step 1)
2 parents fa75858 + 84f1466 commit 02180c5

8 files changed

Lines changed: 729 additions & 134 deletions

File tree

src/impl/dgb/coin/connection_coinbase.hpp

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@
2121
// ============================================================================
2222

2323
#include "gentx_coinbase.hpp"
24+
#include "pplns_payout_split.hpp" // compute_pplns_payout_split SSOT
2425

2526
#include <core/uint256.hpp>
2627
#include <util/strencodings.h> // HexStr
2728
#include <btclibs/span.h> // Span
2829

2930
#include <cstdint>
31+
#include <map>
3032
#include <optional>
3133
#include <string>
3234
#include <utility>
@@ -103,4 +105,50 @@ inline ConnCoinbaseParts build_connection_coinbase_parts(const ConnCoinbaseInput
103105
return out;
104106
}
105107

108+
109+
// ============================================================================
110+
// PPLNS-sourced per-connection coinbase (the #328/#329 SSOT bridge).
111+
//
112+
// Instead of pre-resolved payout_outputs/donation_amount, the caller passes the
113+
// raw PPLNS weight map + subsidy and we delegate the amount split to
114+
// compute_pplns_payout_split() -- the SAME helper share_check.hpp
115+
// generate_share_transaction() (the verification path) calls (see #329). The
116+
// per-connection coinbase a miner hashes and the coinbase the share check
117+
// enforces are therefore byte-identical on every payout satoshi BY
118+
// CONSTRUCTION: there is exactly one payout implementation, not two that must
119+
// be kept in agreement. No payout arithmetic lives here -- pure delegation +
120+
// field forwarding into build_connection_coinbase_parts().
121+
// ============================================================================
122+
struct ConnCoinbasePplnsInputs
123+
{
124+
std::vector<unsigned char> coinbase_script; // scriptSig (BIP34 height + tag)
125+
std::optional<std::vector<unsigned char>> segwit_commitment_script;
126+
// PPLNS weight map + total, exactly as produced by the ShareTracker
127+
// (share_tracker.hpp get_v36_decayed_cumulative_weights / get_cumulative_weights).
128+
std::map<std::vector<unsigned char>, uint288> weights;
129+
uint288 total_weight;
130+
uint64_t subsidy{0}; // block subsidy + fees to split
131+
bool use_v36_pplns{true}; // V36 (no finder fee) vs pre-V36
132+
std::vector<unsigned char> finder_script; // pre-V36 0.5% finder-fee target
133+
std::vector<unsigned char> donation_script;
134+
uint256 ref_hash; // p2pool ref_hash (32B)
135+
uint64_t last_txout_nonce{0}; // OP_RETURN nonce (extranonce slot)
136+
};
137+
138+
inline ConnCoinbaseParts build_connection_coinbase_from_pplns(const ConnCoinbasePplnsInputs& in)
139+
{
140+
PplnsPayoutSplit split = compute_pplns_payout_split(
141+
in.weights, in.total_weight, in.subsidy, in.use_v36_pplns, in.finder_script);
142+
143+
ConnCoinbaseInputs ci;
144+
ci.coinbase_script = in.coinbase_script;
145+
ci.segwit_commitment_script = in.segwit_commitment_script;
146+
ci.payout_outputs = std::move(split.payout_outputs);
147+
ci.donation_amount = split.donation_amount;
148+
ci.donation_script = in.donation_script;
149+
ci.ref_hash = in.ref_hash;
150+
ci.last_txout_nonce = in.last_txout_nonce;
151+
return build_connection_coinbase_parts(ci);
152+
}
153+
106154
} // namespace dgb::coin
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: 31 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
#include "share_types.hpp"
1010

1111
#include <impl/dgb/coin/gentx_coinbase.hpp>
12+
#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)
1214

1315
#include <core/coin_params.hpp>
1416
#include <core/hash.hpp>
@@ -966,59 +968,17 @@ uint256 generate_share_transaction(const ShareT& share, TrackerT& tracker, const
966968
uint288 total_weight;
967969
uint288 total_donation_weight;
968970

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

1024984
// --- 2. Convert weights to exact integer payout amounts ---
@@ -1029,7 +989,6 @@ uint256 generate_share_transaction(const ShareT& share, TrackerT& tracker, const
1029989
// donation = subsidy - sum(amounts)
1030990

1031991
auto gst_t1 = std::chrono::steady_clock::now(); // after PPLNS walk
1032-
std::map<std::vector<unsigned char>, uint64_t> amounts;
1033992

1034993
// Periodic dump of PPLNS weights for cross-impl comparison
1035994
{
@@ -1051,89 +1010,36 @@ uint256 generate_share_transaction(const ShareT& share, TrackerT& tracker, const
10511010
}
10521011
}
10531012

1054-
if (!total_weight.IsNull())
1055-
{
1056-
for (auto& [script, weight] : weights)
1057-
{
1058-
uint64_t amount;
1059-
if (use_v36_pplns)
1060-
{
1061-
// V36: amounts[script] = subsidy * weight / total_weight
1062-
uint288 num = uint288(subsidy) * weight;
1063-
amount = (num / total_weight).GetLow64();
1064-
}
1065-
else
1066-
{
1067-
// Pre-V36: amounts[script] = subsidy * (199 * weight) / (200 * total_weight)
1068-
uint288 num = uint288(subsidy) * (weight * 199);
1069-
uint288 den = total_weight * 200;
1070-
amount = (num / den).GetLow64();
1071-
}
1072-
if (amount > 0)
1073-
amounts[script] = amount;
1074-
}
1075-
}
1076-
1077-
// Pre-V36: add 0.5% finder fee to share creator
1078-
if (!use_v36_pplns)
1079-
{
1080-
auto finder_script = get_share_script(&share);
1081-
amounts[finder_script] += subsidy / 200;
1082-
}
1013+
// --- 2-3. PPLNS amount math + consensus output ordering (SSOT) ----------
1014+
// Steps 2-3 are now the single tracker-free helper compute_pplns_payout_split().
1015+
// The per-connection Stratum coinbase assembler draws the SAME split, so
1016+
// emission and this verification path cannot diverge on a payout satoshi.
1017+
// The helper is a verbatim lift of the former inline math (exact V36 /
1018+
// pre-V36 formulae, 0.5% finder fee, >=1-sat V36 donation floor with the
1019+
// (amount, script) tiebreak, and the ascending sort truncated to [-4000:]).
1020+
// Reference: frstrtr/p2pool-merged-v36 data.py generate_transaction().
1021+
auto finder_script = get_share_script(&share);
1022+
auto pplns_split = dgb::coin::compute_pplns_payout_split(
1023+
weights, total_weight, subsidy, use_v36_pplns, finder_script);
1024+
auto& payout_outputs = pplns_split.payout_outputs;
1025+
uint64_t donation_amount = pplns_split.donation_amount;
10831026

1084-
// Donation output = subsidy minus sum of all payout amounts
1085-
uint64_t sum_amounts = 0;
1086-
for (auto& [s, a] : amounts)
1087-
sum_amounts += a;
1088-
uint64_t donation_amount = (subsidy > sum_amounts) ? (subsidy - sum_amounts) : 0;
1027+
auto gst_t2 = std::chrono::steady_clock::now(); // after amounts+ordering
10891028

1090-
// Dump amounts for cross-impl debugging
1029+
// Dump payouts for cross-impl debugging
10911030
if (dump_diag) {
1092-
LOG_DEBUG_DIAG << "[GST-AMOUNTS] subsidy=" << subsidy << " addrs=" << amounts.size()
1031+
uint64_t sum_amounts = 0;
1032+
for (auto& [s, a] : payout_outputs) sum_amounts += a;
1033+
LOG_DEBUG_DIAG << "[GST-AMOUNTS] subsidy=" << subsidy << " addrs=" << payout_outputs.size()
10931034
<< " sum=" << sum_amounts << " donation=" << donation_amount
10941035
<< " prev=" << prev_hash.GetHex().substr(0,16);
1095-
for (auto& [s, a] : amounts) {
1036+
for (auto& [s, a] : payout_outputs) {
10961037
static const char* HX = "0123456789abcdef";
10971038
std::string sh; for (size_t i = 0; i < std::min(s.size(), size_t(10)); ++i) { sh += HX[s[i]>>4]; sh += HX[s[i]&0xf]; }
10981039
LOG_DEBUG_DIAG << "[GST-AMOUNTS] " << sh << "... = " << a;
10991040
}
11001041
}
11011042

1102-
// V36 consensus: donation output must carry >= 1 satoshi (a60f7f7f)
1103-
if (use_v36_pplns) {
1104-
if (donation_amount < 1 && subsidy > 0 && !amounts.empty()) {
1105-
// Deduct 1 sat from the largest miner payout
1106-
// Deterministic tiebreak: (amount, script) — largest script wins when equal
1107-
auto largest = std::max_element(amounts.begin(), amounts.end(),
1108-
[](const auto& a, const auto& b) {
1109-
if (a.second != b.second) return a.second < b.second;
1110-
return a.first < b.first;
1111-
});
1112-
if (largest != amounts.end() && largest->second > 0) {
1113-
largest->second -= 1;
1114-
sum_amounts -= 1;
1115-
donation_amount = subsidy - sum_amounts;
1116-
}
1117-
}
1118-
}
1119-
1120-
// --- 3. Build sorted output list ---
1121-
auto gst_t2 = std::chrono::steady_clock::now(); // after amounts
1122-
// Python: sorted(dests, key=lambda a: (amounts[a], a))[-4000:]
1123-
// = ascending by (amount, script), keep last 4000 (highest amounts)
1124-
std::vector<std::pair<std::vector<unsigned char>, uint64_t>> payout_outputs(
1125-
amounts.begin(), amounts.end());
1126-
std::sort(payout_outputs.begin(), payout_outputs.end(),
1127-
[](const auto& a, const auto& b) {
1128-
if (a.second != b.second) return a.second < b.second; // asc by amount
1129-
return a.first < b.first; // asc by script for tie-breaking
1130-
});
1131-
1132-
// Keep last MAX_OUTPUTS (highest amounts), matching Python's [-4000:]
1133-
constexpr size_t MAX_OUTPUTS = 4000;
1134-
if (payout_outputs.size() > MAX_OUTPUTS)
1135-
payout_outputs.erase(payout_outputs.begin(), payout_outputs.end() - MAX_OUTPUTS);
1136-
11371043
// --- 4. Serialise the coinbase transaction ---
11381044
// Wire layout (version|vin|vouts|locktime) and txid are produced by the
11391045
// SSOT assembler dgb::coin::assemble_gentx_coinbase() so that emission and

0 commit comments

Comments
 (0)