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
48 changes: 48 additions & 0 deletions src/impl/dgb/coin/connection_coinbase.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@
// ============================================================================

#include "gentx_coinbase.hpp"
#include "pplns_payout_split.hpp" // compute_pplns_payout_split SSOT

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

#include <cstdint>
#include <map>
#include <optional>
#include <string>
#include <utility>
Expand Down Expand Up @@ -103,4 +105,50 @@ inline ConnCoinbaseParts build_connection_coinbase_parts(const ConnCoinbaseInput
return out;
}


// ============================================================================
// PPLNS-sourced per-connection coinbase (the #328/#329 SSOT bridge).
//
// Instead of pre-resolved payout_outputs/donation_amount, the caller passes the
// raw PPLNS weight map + subsidy and we delegate the amount split to
// compute_pplns_payout_split() -- the SAME helper share_check.hpp
// generate_share_transaction() (the verification path) calls (see #329). The
// per-connection coinbase a miner hashes and the coinbase the share check
// enforces are therefore byte-identical on every payout satoshi BY
// CONSTRUCTION: there is exactly one payout implementation, not two that must
// be kept in agreement. No payout arithmetic lives here -- pure delegation +
// field forwarding into build_connection_coinbase_parts().
// ============================================================================
struct ConnCoinbasePplnsInputs
{
std::vector<unsigned char> coinbase_script; // scriptSig (BIP34 height + tag)
std::optional<std::vector<unsigned char>> segwit_commitment_script;
// PPLNS weight map + total, exactly as produced by the ShareTracker
// (share_tracker.hpp get_v36_decayed_cumulative_weights / get_cumulative_weights).
std::map<std::vector<unsigned char>, uint288> weights;
uint288 total_weight;
uint64_t subsidy{0}; // block subsidy + fees to split
bool use_v36_pplns{true}; // V36 (no finder fee) vs pre-V36
std::vector<unsigned char> finder_script; // pre-V36 0.5% finder-fee target
std::vector<unsigned char> donation_script;
uint256 ref_hash; // p2pool ref_hash (32B)
uint64_t last_txout_nonce{0}; // OP_RETURN nonce (extranonce slot)
};

inline ConnCoinbaseParts build_connection_coinbase_from_pplns(const ConnCoinbasePplnsInputs& in)
{
PplnsPayoutSplit split = compute_pplns_payout_split(
in.weights, in.total_weight, in.subsidy, in.use_v36_pplns, in.finder_script);

ConnCoinbaseInputs ci;
ci.coinbase_script = in.coinbase_script;
ci.segwit_commitment_script = in.segwit_commitment_script;
ci.payout_outputs = std::move(split.payout_outputs);
ci.donation_amount = split.donation_amount;
ci.donation_script = in.donation_script;
ci.ref_hash = in.ref_hash;
ci.last_txout_nonce = in.last_txout_nonce;
return build_connection_coinbase_parts(ci);
}

} // namespace dgb::coin
112 changes: 112 additions & 0 deletions src/impl/dgb/coin/pplns_weight_walk.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#pragma once
// ─────────────────────────────────────────────────────────────────────────────
// SSOT: PPLNS weight walk — step 1 of generate_share_transaction(), lifted so the
// share-VERIFICATION path (share_check.hpp generate_share_transaction) and the
// per-connection Stratum coinbase EMISSION path (work_source.cpp
// build_connection_coinbase producer seam, bound in main_dgb.cpp) draw ONE
// tracker-walk implementation — no second copy to drift a payout satoshi.
//
// Counterpart of the steps 2-3 lift in pplns_payout_split.hpp (#328): together
// compute_pplns_weight_walk() + compute_pplns_payout_split() are the full
// former-inline body of generate_share_transaction()'s PPLNS computation.
// Verbatim lift — exact V36 (exponential depth-decay) vs pre-V36 (flat
// cumulative, grandparent start) branch, the data.py:762-764 insufficient-depth
// guard, the block-target / spread / 65535 max_weight cap, and the
// unlimited-weight V36 sentinel.
// Reference: frstrtr/p2pool-merged-v36 data.py:879/884-885, work.py:759.
//
// NOTE: the result type is DEDUCED from the tracker (dgb::CumulativeWeights) via
// decltype rather than named/included directly — share_tracker.hpp includes
// share_check.hpp, which includes this header, so naming the type here (or
// including share_tracker.hpp) would form a parse-time include cycle when
// share_tracker.hpp is the outer include. The dependent return type resolves
// only at instantiation, where TrackerT is complete.
// ─────────────────────────────────────────────────────────────────────────────

#include <core/coin_params.hpp>
#include <core/target_utils.hpp>
#include <core/uint256.hpp>

#include <algorithm>
#include <cstdint>
#include <stdexcept>
#include <string>
#include <utility>

namespace dgb::coin {

// Walk the sharechain from `prev_hash` (the new share's parent / the current
// tip when emitting work) backward and accumulate the PPLNS weight map exactly
// as the verifier does. `block_bits` is the block-header nBits feeding the
// pre-V36 max_weight cap (share.m_min_header.m_bits on the verify path).
//
// * prev_hash null or absent from the chain -> empty weights (caller treats
// as a safe coinbase-only / empty job — the pre-wire behavior).
// * chain shorter than real_chain_length -> throws std::invalid_argument,
// the SAME boundary generate_share_transaction() rejects, so emission and
// verification refuse identical insufficient-depth states.
template <typename TrackerT>
inline auto compute_pplns_weight_walk(
TrackerT& tracker,
const uint256& prev_hash,
uint32_t block_bits,
const core::CoinParams& params,
bool use_v36_pplns)
-> decltype(tracker.get_v36_decayed_cumulative_weights(
prev_hash, std::int32_t{0}, std::declval<const uint288&>()))
{
using Result = decltype(tracker.get_v36_decayed_cumulative_weights(
prev_hash, std::int32_t{0}, std::declval<const uint288&>()));
Result out{};

if (prev_hash.IsNull() || !tracker.chain.contains(prev_hash))
return out; // no parent in chain -> empty (safe coinbase-only job).

// p2pool data.py:762-764 — refuse to compute PPLNS with insufficient depth.
// Without this guard, attempt_verify() (which allows CHAIN_LENGTH+1) can
// trigger a PPLNS walk that terminates early, producing wrong coinbase
// amounts and causing persistent GENTX-MISMATCH during bootstrap.
auto chain_len = static_cast<int32_t>(params.real_chain_length);
{
auto pplns_height = tracker.chain.get_height(prev_hash);
auto pplns_last = tracker.chain.get_last(prev_hash);
if (!(pplns_height >= chain_len || pplns_last.IsNull()))
throw std::invalid_argument(
"share chain not long enough for PPLNS verification (height="
+ std::to_string(pplns_height) + " need="
+ std::to_string(chain_len) + ")");
}

// block_target from block header bits (matches Python: self.header['bits'].target)
auto block_target = chain::bits_to_target(block_bits);
auto max_weight = chain::target_to_average_attempts(block_target)
* params.spread * 65535;

// PPLNS formula selected by runtime v36_active (AutoRatchet state), not
// compile-time share version. Ref: p2pool data.py:879, work.py:759.
if (use_v36_pplns) {
// V36 PPLNS: exponential depth-decay, walk from parent.
uint288 unlimited_weight;
unlimited_weight.SetHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
out = tracker.get_v36_decayed_cumulative_weights(prev_hash, chain_len, unlimited_weight);
} else {
// Pre-V36 PPLNS: flat cumulative weights (no decay). CRITICAL: walk from
// GRANDPARENT for HEIGHT-1 shares. p2pool data.py:884-885:
// _pplns_start = previous_share.share_data['previous_share_hash']
// _pplns_max_shares = max(0, min(height, REAL_CHAIN_LENGTH) - 1)
uint256 pplns_start;
tracker.chain.get(prev_hash).share.invoke([&](auto* s) {
pplns_start = s->m_prev_hash; // grandparent
});
auto available = tracker.chain.get_height(prev_hash);
auto walk_count = static_cast<int32_t>(
std::max(0, std::min(chain_len, available) - 1));

if (!pplns_start.IsNull() && tracker.chain.contains(pplns_start) && walk_count > 0) {
out = tracker.get_cumulative_weights(pplns_start, walk_count, max_weight);
}
}
return out;
}

} // namespace dgb::coin
156 changes: 31 additions & 125 deletions src/impl/dgb/share_check.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
#include "share_types.hpp"

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

#include <core/coin_params.hpp>
#include <core/hash.hpp>
Expand Down Expand Up @@ -966,59 +968,17 @@ uint256 generate_share_transaction(const ShareT& share, TrackerT& tracker, const
uint288 total_weight;
uint288 total_donation_weight;

if (!prev_hash.IsNull() && tracker.chain.contains(prev_hash))
// Step 1 of the PPLNS computation now lives in the shared SSOT
// dgb::coin::compute_pplns_weight_walk() so the per-connection Stratum
// coinbase EMISSION path walks the tracker through the SAME code this
// VERIFICATION path does — byte-identical weights and identical insufficient-
// depth guard, no second walk to drift. Verbatim lift; see pplns_weight_walk.hpp.
{
// p2pool data.py:762-764 — refuse to compute PPLNS with insufficient depth.
// Without this guard, attempt_verify() (which allows CHAIN_LENGTH+1) can
// trigger a PPLNS walk that terminates early, producing wrong coinbase
// amounts and causing persistent GENTX-MISMATCH during bootstrap.
auto chain_len = static_cast<int32_t>(params.real_chain_length);
{
auto pplns_height = tracker.chain.get_height(prev_hash);
auto pplns_last = tracker.chain.get_last(prev_hash);
if (!(pplns_height >= chain_len || pplns_last.IsNull()))
throw std::invalid_argument(
"share chain not long enough for PPLNS verification (height="
+ std::to_string(pplns_height) + " need="
+ std::to_string(chain_len) + ")");
}

// block_target from block header bits (matches Python: self.header['bits'].target)
auto block_target = chain::bits_to_target(share.m_min_header.m_bits);
auto max_weight = chain::target_to_average_attempts(block_target)
* params.spread * 65535;

// PPLNS formula selected by runtime v36_active (AutoRatchet state),
// not compile-time share version. Ref: p2pool data.py:879, work.py:759.
if (use_v36_pplns) {
// V36 PPLNS: exponential depth-decay, walk from parent
uint288 unlimited_weight;
unlimited_weight.SetHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
auto result = tracker.get_v36_decayed_cumulative_weights(prev_hash, chain_len, unlimited_weight);
weights = std::move(result.weights);
total_weight = result.total_weight;
total_donation_weight = result.total_donation_weight;
} else {
// Pre-V36 PPLNS: flat cumulative weights (no decay)
// CRITICAL: Walk from GRANDPARENT for HEIGHT-1 shares.
// p2pool data.py:884-885:
// _pplns_start = previous_share.share_data['previous_share_hash']
// _pplns_max_shares = max(0, min(height, REAL_CHAIN_LENGTH) - 1)
uint256 pplns_start;
tracker.chain.get(prev_hash).share.invoke([&](auto* s) {
pplns_start = s->m_prev_hash; // grandparent
});
auto available = tracker.chain.get_height(prev_hash);
auto walk_count = static_cast<int32_t>(
std::max(0, std::min(chain_len, available) - 1));

if (!pplns_start.IsNull() && tracker.chain.contains(pplns_start) && walk_count > 0) {
auto result = tracker.get_cumulative_weights(pplns_start, walk_count, max_weight);
weights = std::move(result.weights);
total_weight = result.total_weight;
total_donation_weight = result.total_donation_weight;
}
}
auto cw = dgb::coin::compute_pplns_weight_walk(
tracker, prev_hash, share.m_min_header.m_bits, params, use_v36_pplns);
weights = std::move(cw.weights);
total_weight = cw.total_weight;
total_donation_weight = cw.total_donation_weight;
}

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

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

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

if (!total_weight.IsNull())
{
for (auto& [script, weight] : weights)
{
uint64_t amount;
if (use_v36_pplns)
{
// V36: amounts[script] = subsidy * weight / total_weight
uint288 num = uint288(subsidy) * weight;
amount = (num / total_weight).GetLow64();
}
else
{
// Pre-V36: amounts[script] = subsidy * (199 * weight) / (200 * total_weight)
uint288 num = uint288(subsidy) * (weight * 199);
uint288 den = total_weight * 200;
amount = (num / den).GetLow64();
}
if (amount > 0)
amounts[script] = amount;
}
}

// Pre-V36: add 0.5% finder fee to share creator
if (!use_v36_pplns)
{
auto finder_script = get_share_script(&share);
amounts[finder_script] += subsidy / 200;
}
// --- 2-3. PPLNS amount math + consensus output ordering (SSOT) ----------
// Steps 2-3 are now the single tracker-free helper compute_pplns_payout_split().
// The per-connection Stratum coinbase assembler draws the SAME split, so
// emission and this verification path cannot diverge on a payout satoshi.
// The helper is a verbatim lift of the former inline math (exact V36 /
// pre-V36 formulae, 0.5% finder fee, >=1-sat V36 donation floor with the
// (amount, script) tiebreak, and the ascending sort truncated to [-4000:]).
// Reference: frstrtr/p2pool-merged-v36 data.py generate_transaction().
auto finder_script = get_share_script(&share);
auto pplns_split = dgb::coin::compute_pplns_payout_split(
weights, total_weight, subsidy, use_v36_pplns, finder_script);
auto& payout_outputs = pplns_split.payout_outputs;
uint64_t donation_amount = pplns_split.donation_amount;

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

// Dump amounts for cross-impl debugging
// Dump payouts for cross-impl debugging
if (dump_diag) {
LOG_DEBUG_DIAG << "[GST-AMOUNTS] subsidy=" << subsidy << " addrs=" << amounts.size()
uint64_t sum_amounts = 0;
for (auto& [s, a] : payout_outputs) sum_amounts += a;
LOG_DEBUG_DIAG << "[GST-AMOUNTS] subsidy=" << subsidy << " addrs=" << payout_outputs.size()
<< " sum=" << sum_amounts << " donation=" << donation_amount
<< " prev=" << prev_hash.GetHex().substr(0,16);
for (auto& [s, a] : amounts) {
for (auto& [s, a] : payout_outputs) {
static const char* HX = "0123456789abcdef";
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]; }
LOG_DEBUG_DIAG << "[GST-AMOUNTS] " << sh << "... = " << a;
}
}

// V36 consensus: donation output must carry >= 1 satoshi (a60f7f7f)
if (use_v36_pplns) {
if (donation_amount < 1 && subsidy > 0 && !amounts.empty()) {
// Deduct 1 sat from the largest miner payout
// Deterministic tiebreak: (amount, script) — largest script wins when equal
auto largest = std::max_element(amounts.begin(), amounts.end(),
[](const auto& a, const auto& b) {
if (a.second != b.second) return a.second < b.second;
return a.first < b.first;
});
if (largest != amounts.end() && largest->second > 0) {
largest->second -= 1;
sum_amounts -= 1;
donation_amount = subsidy - sum_amounts;
}
}
}

// --- 3. Build sorted output list ---
auto gst_t2 = std::chrono::steady_clock::now(); // after amounts
// Python: sorted(dests, key=lambda a: (amounts[a], a))[-4000:]
// = ascending by (amount, script), keep last 4000 (highest amounts)
std::vector<std::pair<std::vector<unsigned char>, uint64_t>> payout_outputs(
amounts.begin(), amounts.end());
std::sort(payout_outputs.begin(), payout_outputs.end(),
[](const auto& a, const auto& b) {
if (a.second != b.second) return a.second < b.second; // asc by amount
return a.first < b.first; // asc by script for tie-breaking
});

// Keep last MAX_OUTPUTS (highest amounts), matching Python's [-4000:]
constexpr size_t MAX_OUTPUTS = 4000;
if (payout_outputs.size() > MAX_OUTPUTS)
payout_outputs.erase(payout_outputs.begin(), payout_outputs.end() - MAX_OUTPUTS);

// --- 4. Serialise the coinbase transaction ---
// Wire layout (version|vin|vouts|locktime) and txid are produced by the
// SSOT assembler dgb::coin::assemble_gentx_coinbase() so that emission and
Expand Down
Loading
Loading