|
| 1 | +#pragma once |
| 2 | +// ============================================================================ |
| 3 | +// pplns_payout_split.hpp — SSOT: PPLNS weights -> consensus-sorted payout outputs. |
| 4 | +// |
| 5 | +// Converts the decayed/cumulative PPLNS weight map (produced by the ShareTracker |
| 6 | +// — see share_tracker.hpp get_v36_decayed_cumulative_weights / |
| 7 | +// get_cumulative_weights) into the EXACT integer per-script payout amounts and |
| 8 | +// the consensus output ordering that the coinbase carries. |
| 9 | +// |
| 10 | +// This is steps 2-3 of share_check.hpp generate_share_transaction() lifted into |
| 11 | +// a pure, tracker-free, chain-free function so the SAME math can feed BOTH: |
| 12 | +// - generate_share_transaction() (the share verification SSOT), and |
| 13 | +// - stratum/work_source build_connection_coinbase() (per-connection coinbase), |
| 14 | +// guaranteeing emission and verification can never diverge on a payout satoshi. |
| 15 | +// |
| 16 | +// Port of frstrtr/p2pool-merged-v36 data.py generate_transaction() amount math: |
| 17 | +// V36: amounts[script] = subsidy * weight / total_weight |
| 18 | +// Pre-V36: amounts[script] = subsidy * (199 * weight) / (200 * total_weight) |
| 19 | +// amounts[finder] += subsidy // 200 (0.5% finder fee) |
| 20 | +// donation = subsidy - sum(amounts) |
| 21 | +// V36 floor: donation must carry >= 1 sat (a60f7f7f) — deduct 1 from the |
| 22 | +// largest payout, deterministic tiebreak (amount, script). |
| 23 | +// order: sorted(dests, key=(amount, script))[-4000:] (asc, keep highest) |
| 24 | +// |
| 25 | +// Pure: takes already-built weights + subsidy + finder script, so it is directly |
| 26 | +// KAT-able against a canonical oracle vector (test/pplns_payout_split_test.cpp). |
| 27 | +// ============================================================================ |
| 28 | + |
| 29 | +#include <core/uint256.hpp> // uint288 |
| 30 | + |
| 31 | +#include <algorithm> |
| 32 | +#include <cstdint> |
| 33 | +#include <map> |
| 34 | +#include <utility> |
| 35 | +#include <vector> |
| 36 | + |
| 37 | +namespace dgb::coin |
| 38 | +{ |
| 39 | + |
| 40 | +// Maximum coinbase payout outputs, matching p2pool's [-4000:] slice. |
| 41 | +inline constexpr std::size_t PPLNS_MAX_OUTPUTS = 4000; |
| 42 | + |
| 43 | +struct PplnsPayoutSplit |
| 44 | +{ |
| 45 | + // (scriptPubKey, value) pairs in final consensus order: ascending by |
| 46 | + // (amount, script), truncated to the highest PPLNS_MAX_OUTPUTS. |
| 47 | + std::vector<std::pair<std::vector<unsigned char>, uint64_t>> payout_outputs; |
| 48 | + // donation = subsidy - sum(payout amounts) (after the v36 >=1 sat floor). |
| 49 | + uint64_t donation_amount{0}; |
| 50 | +}; |
| 51 | + |
| 52 | +// weights / total_weight: PPLNS weight map keyed by full scriptPubKey. |
| 53 | +// subsidy: total coinbase value to distribute (block subsidy + fees). |
| 54 | +// use_v36_pplns: select the V36 formula (no finder fee) vs pre-V36 (0.5% fee). |
| 55 | +// finder_script: share creator's payout script — receives the pre-V36 finder |
| 56 | +// fee; ignored when use_v36_pplns is true. |
| 57 | +inline PplnsPayoutSplit compute_pplns_payout_split( |
| 58 | + const std::map<std::vector<unsigned char>, uint288>& weights, |
| 59 | + const uint288& total_weight, |
| 60 | + uint64_t subsidy, |
| 61 | + bool use_v36_pplns, |
| 62 | + const std::vector<unsigned char>& finder_script) |
| 63 | +{ |
| 64 | + std::map<std::vector<unsigned char>, uint64_t> amounts; |
| 65 | + |
| 66 | + // --- exact integer payout amounts (generate_share_transaction step 2) --- |
| 67 | + if (!total_weight.IsNull()) |
| 68 | + { |
| 69 | + for (auto& [script, weight] : weights) |
| 70 | + { |
| 71 | + uint64_t amount; |
| 72 | + if (use_v36_pplns) |
| 73 | + { |
| 74 | + // V36: amounts[script] = subsidy * weight / total_weight |
| 75 | + uint288 num = uint288(subsidy) * weight; |
| 76 | + amount = (num / total_weight).GetLow64(); |
| 77 | + } |
| 78 | + else |
| 79 | + { |
| 80 | + // Pre-V36: amounts[script] = subsidy * (199 * weight) / (200 * total_weight) |
| 81 | + uint288 num = uint288(subsidy) * (weight * 199); |
| 82 | + uint288 den = total_weight * 200; |
| 83 | + amount = (num / den).GetLow64(); |
| 84 | + } |
| 85 | + if (amount > 0) |
| 86 | + amounts[script] = amount; |
| 87 | + } |
| 88 | + } |
| 89 | + |
| 90 | + // Pre-V36: add 0.5% finder fee to share creator. |
| 91 | + if (!use_v36_pplns) |
| 92 | + amounts[finder_script] += subsidy / 200; |
| 93 | + |
| 94 | + // Donation = subsidy minus the sum of all payout amounts. |
| 95 | + uint64_t sum_amounts = 0; |
| 96 | + for (auto& [s, a] : amounts) |
| 97 | + sum_amounts += a; |
| 98 | + uint64_t donation_amount = (subsidy > sum_amounts) ? (subsidy - sum_amounts) : 0; |
| 99 | + |
| 100 | + // V36 consensus: donation output must carry >= 1 satoshi (a60f7f7f). |
| 101 | + // Deduct 1 sat from the largest miner payout; deterministic tiebreak |
| 102 | + // (amount, script) — largest script wins when amounts are equal. |
| 103 | + if (use_v36_pplns) |
| 104 | + { |
| 105 | + if (donation_amount < 1 && subsidy > 0 && !amounts.empty()) |
| 106 | + { |
| 107 | + auto largest = std::max_element(amounts.begin(), amounts.end(), |
| 108 | + [](const auto& a, const auto& b) { |
| 109 | + if (a.second != b.second) return a.second < b.second; |
| 110 | + return a.first < b.first; |
| 111 | + }); |
| 112 | + if (largest != amounts.end() && largest->second > 0) |
| 113 | + { |
| 114 | + largest->second -= 1; |
| 115 | + sum_amounts -= 1; |
| 116 | + donation_amount = subsidy - sum_amounts; |
| 117 | + } |
| 118 | + } |
| 119 | + } |
| 120 | + |
| 121 | + // --- consensus output order (generate_share_transaction step 3) --------- |
| 122 | + // Python: sorted(dests, key=lambda a: (amounts[a], a))[-4000:] |
| 123 | + // = ascending by (amount, script), keep last PPLNS_MAX_OUTPUTS (highest). |
| 124 | + PplnsPayoutSplit out; |
| 125 | + out.donation_amount = donation_amount; |
| 126 | + out.payout_outputs.assign(amounts.begin(), amounts.end()); |
| 127 | + std::sort(out.payout_outputs.begin(), out.payout_outputs.end(), |
| 128 | + [](const auto& a, const auto& b) { |
| 129 | + if (a.second != b.second) return a.second < b.second; // asc by amount |
| 130 | + return a.first < b.first; // asc by script tiebreak |
| 131 | + }); |
| 132 | + if (out.payout_outputs.size() > PPLNS_MAX_OUTPUTS) |
| 133 | + out.payout_outputs.erase(out.payout_outputs.begin(), |
| 134 | + out.payout_outputs.end() - PPLNS_MAX_OUTPUTS); |
| 135 | + return out; |
| 136 | +} |
| 137 | + |
| 138 | +} // namespace dgb::coin |
0 commit comments