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
103 changes: 103 additions & 0 deletions src/impl/dgb/test/connection_coinbase_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <gtest/gtest.h>
#include <impl/dgb/coin/connection_coinbase.hpp>

#include <map>
#include <optional>
#include <string>
#include <utility>
Expand Down Expand Up @@ -101,4 +102,106 @@ TEST(ConnCoinbase, RefOpReturnLayout) {
EXPECT_EQ(tohex(op), std::string("6a28") + ref32 + "0807060504030201");
}


// ============================================================================
// (5)-(8) PPLNS SSOT wiring: build_connection_coinbase_from_pplns delegates the
// payout split to compute_pplns_payout_split (the #329 verification SSOT) and
// forwards every non-payout field unchanged into build_connection_coinbase_parts.
//
// These are DELEGATION-IDENTITY tests, not a second payout implementation: each
// asserts the PPLNS-sourced path produces bytes identical to the manual path
// fed by the SAME SSOT split. Reintroducing inline payout math, dropping the
// v36 floor, or mis-forwarding a field would diverge them. The split MATH
// itself is pinned independently in pplns_payout_split_test.cpp.

using Script = std::vector<unsigned char>;

// Build the manual reference: run the SSOT split, hand it to the parts builder.
dgb::coin::ConnCoinbaseParts manual_from_split(
const std::map<Script, uint288>& w, const uint288& total, uint64_t subsidy,
bool v36, const Script& finder,
const std::optional<Script>& seg, const uint256& ref, uint64_t nonce) {
auto split = dgb::coin::compute_pplns_payout_split(w, total, subsidy, v36, finder);
dgb::coin::ConnCoinbaseInputs in;
in.coinbase_script = CB;
in.segwit_commitment_script = seg;
in.payout_outputs = split.payout_outputs;
in.donation_amount = split.donation_amount;
in.donation_script = DON;
in.ref_hash = ref;
in.last_txout_nonce = nonce;
return dgb::coin::build_connection_coinbase_parts(in);
}

dgb::coin::ConnCoinbaseParts wired_from_pplns(
const std::map<Script, uint288>& w, const uint288& total, uint64_t subsidy,
bool v36, const Script& finder,
const std::optional<Script>& seg, const uint256& ref, uint64_t nonce) {
dgb::coin::ConnCoinbasePplnsInputs in;
in.coinbase_script = CB;
in.segwit_commitment_script = seg;
in.weights = w;
in.total_weight = total;
in.subsidy = subsidy;
in.use_v36_pplns = v36;
in.finder_script = finder;
in.donation_script = DON;
in.ref_hash = ref;
in.last_txout_nonce = nonce;
return dgb::coin::build_connection_coinbase_from_pplns(in);
}

void expect_parts_eq(const dgb::coin::ConnCoinbaseParts& a,
const dgb::coin::ConnCoinbaseParts& b) {
EXPECT_EQ(tohex(a.gentx.bytes), tohex(b.gentx.bytes));
EXPECT_EQ(a.coinb1, b.coinb1);
EXPECT_EQ(a.coinb2, b.coinb2);
EXPECT_EQ(a.gentx.txid, b.gentx.txid);
}

// (5) V36 split (no finder fee, >=1 sat donation floor) flows through identically.
TEST(ConnCoinbasePplns, V36WiredEqualsManual) {
std::map<Script, uint288> w{{P1, uint288(3)}, {P2, uint288(1)}};
uint256 ref(std::vector<unsigned char>(32, 0xab));
auto wired = wired_from_pplns(w, uint288(4), 10000, true, {}, std::nullopt, ref, NONCE);
auto manual = manual_from_split(w, uint288(4), 10000, true, {}, std::nullopt, ref, NONCE);
expect_parts_eq(wired, manual);
}

// (6) Pre-V36 path (use_v36_pplns=false + finder_script) forwards the flag and
// the 0.5% finder fee target — proves both are wired, not hard-coded.
TEST(ConnCoinbasePplns, PreV36FinderFeeWiredEqualsManual) {
std::map<Script, uint288> w{{P1, uint288(1)}, {P2, uint288(1)}};
uint256 ref(std::vector<unsigned char>(32, 0xab));
auto wired = wired_from_pplns(w, uint288(2), 20000, false, P1, std::nullopt, ref, NONCE);
auto manual = manual_from_split(w, uint288(2), 20000, false, P1, std::nullopt, ref, NONCE);
expect_parts_eq(wired, manual);
}

// (7) Non-payout fields (segwit commitment, distinct ref_hash, distinct nonce)
// forward unchanged into the assembler.
TEST(ConnCoinbasePplns, SegwitAndRefFieldsForwarded) {
std::map<Script, uint288> w{{P1, uint288(5)}, {P2, uint288(2)}};
Script seg = unhex("6a24aa21a9ed" + std::string(64, 'c'));
uint256 ref(std::vector<unsigned char>(32, 0x5c));
const uint64_t alt_nonce = 0x1112131415161718ull;
auto wired = wired_from_pplns(w, uint288(7), 7777777, true, {}, seg, ref, alt_nonce);
auto manual = manual_from_split(w, uint288(7), 7777777, true, {}, seg, ref, alt_nonce);
expect_parts_eq(wired, manual);
}

// (8) Value anchor (independent of the wiring): the SSOT split the wired path
// consumes carries the hand-computed V36 amounts in ascending order, and
// the assembled coinbase preserves that exact PPLNS vout sequence.
// subsidy 1000, P1:2 P2:1 (total 3) -> P1=666, P2=333, sum 999, donation 1;
// >=1 sat floor satisfied without deduction. asc(amount): P2(333), P1(666).
TEST(ConnCoinbasePplns, ValueAnchorAscendingPayoutOrder) {
std::map<Script, uint288> w{{P1, uint288(2)}, {P2, uint288(1)}};
auto split = dgb::coin::compute_pplns_payout_split(w, uint288(3), 1000, true, {});
std::vector<std::pair<Script, uint64_t>> expect{{P2, 333}, {P1, 666}};
EXPECT_EQ(split.payout_outputs, expect);
EXPECT_EQ(split.donation_amount, 1u);
// The wired coinbase is assembled from exactly this split (covered by (5)-(7)).
}

} // namespace
Loading