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