Skip to content

Commit fb543dc

Browse files
committed
dgb(phase-b): extract PPLNS payout-split SSOT (weights -> consensus-sorted payout outputs)
Lift steps 2-3 of share_check.hpp generate_share_transaction (PPLNS amount math + consensus output ordering) into a pure, tracker-free helper coin/pplns_payout_split.hpp: compute_pplns_payout_split(). This is the bridge that lets the per-connection Stratum coinbase assembler draw the SAME payout amount split the share verification SSOT enforces, so emission and verification cannot diverge on a satoshi. Additive/fenced: nothing calls it yet (no behavior change). Follow-ons: (1) delegate generate_share_transaction to it (byte-identity), (2) wire it into DGBWorkSource::build_connection_coinbase feeding ConnCoinbaseInputs.payout_outputs. KAT dgb_pplns_payout_split_test: V36 exact amounts + >=1 sat donation floor, pre-V36 amounts + 0.5%% finder fee, empty-weights bootstrap, zero-amount drop; all against hand-computed p2pool v36 data.py oracle vectors. Registered in the test CMakeLists and both build.yml dgb --target allowlists (#143 NOT_BUILT trap).
1 parent 09d2d7f commit fb543dc

4 files changed

Lines changed: 414 additions & 2 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ jobs:
8181
test_mweb_builder \
8282
test_address_resolution test_compute_share_target \
8383
test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_redistribute_test dgb_block_assembly_test dgb_header_sample_build_test dgb_header_ingest_test dgb_mempool_ingest_test \
84-
dgb_gentx_coinbase_test dgb_connection_coinbase_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \
84+
dgb_gentx_coinbase_test dgb_connection_coinbase_test dgb_pplns_payout_split_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \
8585
dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_reconstruct_closure_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test dgb_embedded_tx_select_test dgb_template_other_txs_test dgb_coinbase_value_parity_test dgb_submit_classify_test dgb_aux_parent_coinbase_parity_test dgb_template_capture_test dgb_aux_doge_db_commitment_bind_test \
8686
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
8787
dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test dgb_forced_won_share_dualpath_test dgb_scrypt_pow_test dgb_nonce_grinder_test dgb_regrind_block_test dgb_won_block_finalize_test v37_test \
@@ -213,7 +213,7 @@ jobs:
213213
test_mweb_builder \
214214
test_address_resolution test_compute_share_target \
215215
test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_redistribute_test dgb_block_assembly_test dgb_header_sample_build_test dgb_header_ingest_test dgb_mempool_ingest_test \
216-
dgb_gentx_coinbase_test dgb_connection_coinbase_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \
216+
dgb_gentx_coinbase_test dgb_connection_coinbase_test dgb_pplns_payout_split_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \
217217
dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_reconstruct_closure_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test dgb_embedded_tx_select_test dgb_template_other_txs_test dgb_coinbase_value_parity_test dgb_submit_classify_test dgb_aux_parent_coinbase_parity_test dgb_template_capture_test dgb_aux_doge_db_commitment_bind_test \
218218
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
219219
dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test dgb_forced_won_share_dualpath_test dgb_scrypt_pow_test dgb_nonce_grinder_test dgb_regrind_block_test dgb_won_block_finalize_test test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
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

src/impl/dgb/test/CMakeLists.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,21 @@ if (BUILD_TESTING AND GTest_FOUND)
161161
dgb_coin pool sharechain)
162162
gtest_add_tests(dgb_connection_coinbase_test "" AUTO)
163163

164+
# --- Phase B: PPLNS weights -> consensus-sorted payout outputs KAT ------
165+
# Pins coin/pplns_payout_split.hpp: compute_pplns_payout_split reproduces
166+
# steps 2-3 of share_check.hpp generate_share_transaction (the verification
167+
# SSOT) satoshi-for-satoshi -- the bridge that lets build_connection_coinbase
168+
# draw the SAME PPLNS amount split the share check enforces. Header-only over
169+
# uint288, registered with the proven dgb link set for parity with sibling
170+
# coinbase KATs. MUST also be in the build.yml --target allowlist (#143 trap).
171+
add_executable(dgb_pplns_payout_split_test pplns_payout_split_test.cpp)
172+
target_link_libraries(dgb_pplns_payout_split_test PRIVATE
173+
GTest::gtest_main GTest::gtest
174+
core dgb
175+
c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage
176+
dgb_coin pool sharechain)
177+
gtest_add_tests(dgb_pplns_payout_split_test "" AUTO)
178+
164179
# --- #172 share-path gentx KAT (collapse consumption proof) ------------
165180
# Proves generate_share_transaction (the verification SSOT call site #172
166181
# rewired) EMITS assemble_gentx_coinbase framing for a real v36 share, via

0 commit comments

Comments
 (0)