Skip to content
321 changes: 308 additions & 13 deletions src/impl/bch/pool_entrypoint.hpp

Large diffs are not rendered by default.

29 changes: 28 additions & 1 deletion src/impl/bch/share_check.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,26 @@ uint256 share_init_verify(const ShareT& share, bool check_pow = true)
reinterpret_cast<const unsigned char*>(header_stream.data()), header_stream.size());
uint256 share_hash = Hash(hdr_span);

// [verify-preimage-diag] Non-fatal author-vs-verify divergence localiser.
// When a locally-stored identity is present and the recomputed hash differs,
// dump the preimage components. The header scalars are carried verbatim, so a
// mismatch here isolates to gentx_hash / merkle_root (coinbase+PPLNS author
// path). The chain keys off share.m_hash, so this is diagnostic only.
if (!share.m_hash.IsNull() && share_hash != share.m_hash) {
static int preimage_diag = 0;
if (preimage_diag++ < 10) {
LOG_WARNING << "[verify-preimage] recomputed=" << share_hash.GetHex().substr(0, 16)
<< " stored=" << share.m_hash.GetHex().substr(0, 16);
LOG_WARNING << "[verify-preimage] gentx_hash=" << gentx_hash.GetHex()
<< " merkle_root=" << merkle_root.GetHex();
LOG_WARNING << "[verify-preimage] hdr ver=" << share.m_min_header.m_version
<< " prev=" << share.m_min_header.m_previous_block.GetHex().substr(0, 16)
<< " time=" << share.m_min_header.m_timestamp
<< " bits=0x" << std::hex << share.m_min_header.m_bits << std::dec
<< " nonce=" << share.m_min_header.m_nonce;
}
}

// --- PoW check (SHA256d) ---
// For Bitcoin POW_FUNC is SHA256d, identical to the block identity hash.
// (LTC was scrypt(1024,1,1,256); BTC's pow_hash == share_hash via Hash(hdr_span).)
Expand Down Expand Up @@ -2744,7 +2764,14 @@ uint256 create_local_share(
// Chain is complete and shorter than 99 → None (zero)
share.m_far_share_hash = uint256();
} else {
share.m_far_share_hash = tracker.chain.get_nth_parent_key(prev_share, 99);
try {
share.m_far_share_hash = tracker.chain.get_nth_parent_key(prev_share, 99);
} catch (const std::exception&) {
// Bootstrap: chain shorter than the 99-deep lookback even though
// get_height_and_last reported a non-null last → far=None, mirroring
// the guarded _v35 author and p2pool's get_nth_parent_hash(prev,99).
share.m_far_share_hash = uint256();
}
}
} else {
// Genesis: p2pool always does (prev_absheight + 1), (prev_abswork + aps)
Expand Down
78 changes: 78 additions & 0 deletions src/impl/bch/stratum/coinbase_outputs.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,82 @@ assemble_v36_coinbase_outputs(
return outputs;
}

// apply_v35_finder_fee: pre-v36 (sub-36) finder fee. generate_share_transaction
// (share_check.hpp, use_v36_pplns==false branch) credits subsidy/200 to the
// share creator\x27s own script and shrinks the donation residual by the same
// amount. For a LOCALLY-authored share creator==finder==this connection\x27s
// payout_script, so mirror it exactly here: move subsidy/200 from the donation
// entry (get_v35_expected_payouts folds the ~0.5% into donation) onto
// payout_script. Total stays == subsidy. No-op when `payouts` is empty (cold
// start -> degraded single-output fallback owns that) or the fee rounds to 0.
// Pure/header-only so the production TU and the fenced dual-version KAT consume
// the IDENTICAL implementation. sv>=36 authors never call this (pure PPLNS).
// canonical_finder_script: reconstruct the finder's coinbase script EXACTLY as
// the verifier's generate_share_transaction sees it, i.e. get_share_script() =
// pubkey_hash_to_script(m_pubkey_hash, m_pubkey_type), where create_local_share
// derives (m_pubkey_hash, m_pubkey_type) from payout_script. Standard P2PKH /
// P2SH / P2WPKH scripts are returned verbatim (identity -- no behaviour change).
// A payout_script the share author cannot store as a 20-byte pubkey_hash maps as
// create_local_share does: size >= 20 -> P2PKH of the first 20 bytes; size < 20
// (incl. EMPTY) -> P2PKH(all-zeros) -- the SAME degenerate creator script the
// stored share carries. This keeps the sub-36 finder-fee target byte-identical
// author<->verifier so the gentx hash matches. Without it, an author whose miner
// authorised with a non-decodable address paid NO finder fee while the verifier
// credited subsidy/200 to P2PKH(zeros): a one-output GENTX-MISMATCH on every
// non-genesis share (the height>=2 verified-tip stall).
inline std::vector<unsigned char> canonical_finder_script(
const std::vector<unsigned char>& payout_script)
{
// P2PKH: 76 a9 14 <20> 88 ac (identity)
if (payout_script.size() == 25 && payout_script[0] == 0x76 &&
payout_script[1] == 0xa9 && payout_script[2] == 0x14 &&
payout_script[23] == 0x88 && payout_script[24] == 0xac)
return payout_script;
// P2SH: a9 14 <20> 87 (identity)
if (payout_script.size() == 23 && payout_script[0] == 0xa9 &&
payout_script[1] == 0x14 && payout_script[22] == 0x87)
return payout_script;
// P2WPKH: 00 14 <20> (identity -- pubkey_hash_to_script(type=1) is the same)
if (payout_script.size() == 22 && payout_script[0] == 0x00 &&
payout_script[1] == 0x14)
return payout_script;
// Non-standard: mirror create_local_share's m_pubkey_hash derivation, which
// only writes the hash when payout_script.size() >= 20 (else it stays zero).
unsigned char h[20] = {0};
if (payout_script.size() >= 20)
for (size_t i = 0; i < 20; ++i) h[i] = payout_script[i];
std::vector<unsigned char> script;
script.reserve(25);
script.push_back(0x76);
script.push_back(0xa9);
script.push_back(0x14);
script.insert(script.end(), h, h + 20);
script.push_back(0x88);
script.push_back(0xac);
return script;
}

inline void apply_v35_finder_fee(
std::map<std::vector<unsigned char>, double>& payouts,
const std::vector<unsigned char>& payout_script,
const std::vector<unsigned char>& donation_script,
uint64_t subsidy)
{
if (payouts.empty()) return;
const double finder_fee = static_cast<double>(subsidy / 200);
if (finder_fee <= 0.0) return;
// Credit the finder fee to the CANONICAL creator script the verifier
// reconstructs from the stored share, NOT the raw connection payout_script.
// These differ only when payout_script is non-standard/empty -- exactly the
// case that left author (no fee) and verifier (fee to P2PKH-zeros) one
// output apart. See canonical_finder_script().
const auto finder_script = canonical_finder_script(payout_script);
payouts[finder_script] += finder_fee;
if (!donation_script.empty()) {
auto dit = payouts.find(donation_script);
if (dit != payouts.end() && dit->second >= finder_fee)
dit->second -= finder_fee;
}
}

} // namespace bch::stratum
19 changes: 19 additions & 0 deletions src/impl/bch/stratum/work_source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,12 @@ void BCHWorkSource::set_pplns_fn(PplnsFn fn)
pplns_fn_ = std::move(fn);
}

void BCHWorkSource::set_author_version_fn(std::function<int64_t()> fn)
{
std::lock_guard<std::mutex> lk(callback_mutex_);
author_version_fn_ = std::move(fn);
}

void BCHWorkSource::set_ref_hash_fn(RefHashFn fn)
{
std::lock_guard<std::mutex> lk(callback_mutex_);
Expand Down Expand Up @@ -384,10 +390,16 @@ core::stratum::CoinbaseResult BCHWorkSource::build_connection_coinbase(

PplnsFn pplns_fn; RefHashFn ref_hash_fn;
std::vector<unsigned char> donation_script;
std::function<int64_t()> author_version_fn;
{
std::lock_guard<std::mutex> lk(callback_mutex_);
pplns_fn = pplns_fn_; ref_hash_fn = ref_hash_fn_; donation_script = donation_script_;
author_version_fn = author_version_fn_;
}
// Author version gates the pre-v36 finder-fee shape (applied below, needs
// this connection\x27s payout_script). Unset => 35 (safe: fresh chains author
// at v35). sv>=36 => pure PPLNS, no finder fee.
const int64_t author_version = author_version_fn ? author_version_fn() : 35;

// scriptSig (deterministic): BIP34 height + pool tag.
auto bip34 = bip34_height_push(height);
Expand Down Expand Up @@ -425,6 +437,13 @@ core::stratum::CoinbaseResult BCHWorkSource::build_connection_coinbase(
payouts[donation_script] += static_cast<double>(dropped_value);
}

// Pre-v36 finder fee: sub-36 authors owe generate_share_transaction\x27s v35
// credit of subsidy/200 to the (creator==finder==) miner\x27s own payout_script,
// deducted from the donation residual. Header-only apply_v35_finder_fee keeps
// this byte-identical to the KAT-pinned math; sv>=36 skips it entirely.
if (author_version < 36)
apply_v35_finder_fee(payouts, payout_script, donation_script, coinbasevalue);

// V36 removes the finder fee -- pure PPLNS accounting. The oracle
// (p2pool-merged-v36 data.py ~945) fires the subsidy/200 finder fee ONLY in
// the `not v36_active` branch; the v36 gentx pays no finder fee. Byte-parity
Expand Down
7 changes: 7 additions & 0 deletions src/impl/bch/stratum/work_source.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,12 @@ class BCHWorkSource : public core::stratum::IWorkSource
/// appears as an output.
void set_donation_script(std::vector<unsigned char> script);

/// Wire the author-version producer: returns the sharechain-tip version the
/// next locally-authored share is stamped at (mirrors pool_entrypoint\x27s
/// create_ver derivation off the tip). build_connection_coinbase uses it to
/// gate the pre-v36 finder-fee shape. Left unset => v36-pure (no finder fee).
void set_author_version_fn(std::function<int64_t()> fn);

private:
// External dependencies (non-owning references)
bch::coin::HeaderChain& chain_;
Expand Down Expand Up @@ -267,6 +273,7 @@ class BCHWorkSource : public core::stratum::IWorkSource
RefHashFn ref_hash_fn_;
CreateShareFn create_share_fn_;
std::vector<unsigned char> donation_script_;
std::function<int64_t()> author_version_fn_;

// Template cache (filled lazily; invalidated when work_generation_ bumps)
// Stage c populates these.
Expand Down
46 changes: 45 additions & 1 deletion src/impl/bch/test/coinbase_author_kat_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
// ORACLE packer (scripts/gen_g2_oracle.py, a verbatim transcription of the
// data.py v36 amounts/dests/payouts block), NOT authored against this builder.
//
// Three cases:
// Four cases (dual-version finder-fee bar in 3+4):
// CASE 1 -- ORDERING: four PPLNS dests with an amount TIE (B==C) exercises the
// (amount asc, THEN script asc) sort; donation marker forced LAST;
// the marker <1-sat rule decrements the largest (D) by one satoshi.
Expand Down Expand Up @@ -41,6 +41,7 @@
using Script = std::vector<unsigned char>;
using Outputs = std::vector<std::pair<Script, uint64_t>>;
using bch::stratum::assemble_v36_coinbase_outputs;
using bch::stratum::apply_v35_finder_fee;

static const uint64_t SUBSIDY = 1000000000ULL;

Expand Down Expand Up @@ -112,6 +113,26 @@ static std::map<Script,double> pplns(std::vector<std::pair<Script,uint64_t>> wei
return amounts;
}

// V35 pre-marker PPLNS amounts = subsidy*199*weight//(200*total_weight) (the
// 199/200 haircut get_v35_expected_payouts emits), donation entry = subsidy -
// sum(pplns) BEFORE the finder fee is carved out. apply_v35_finder_fee then
// moves subsidy/200 from DON onto the creator==finder script. Mirrors
// share_tracker.hpp get_v35_expected_payouts + share_check.hpp v35 verify.
static std::map<Script,double> pplns_v35(std::vector<std::pair<Script,uint64_t>> weights,
uint64_t donation_weight) {
uint64_t total_weight = donation_weight;
for (auto& [s,w] : weights) total_weight += w;
std::map<Script,double> amounts;
uint64_t sum = 0;
for (auto& [s,w] : weights) {
uint64_t amt = SUBSIDY * 199 * w / (200 * total_weight);
amounts[s] = static_cast<double>(amt);
sum += amt;
}
amounts[DON] = static_cast<double>(SUBSIDY - sum); // pre-finder donation
return amounts;
}

int main() {
// ----- CASE 1: ordering + amount-tie script tiebreak + marker decrement ---
{
Expand Down Expand Up @@ -176,6 +197,29 @@ int main() {
std::cout << "[KAT] case3 finder-fee-removal byte-diff -- PASS\n";
}

// ----- CASE 4: sub-36 AUTHOR path == fee-present oracle vector ------------
// v35 author path: get_v35 199/200 shape -> apply_v35_finder_fee -> assemble.
// Same {A:60,B:40} inputs as CASE 3 but authored at sv<36: subsidy/200 moves
// from the donation residual onto the creator==finder script (A). Assembled
// bytes MUST equal CASE 3's ORACLE_WITH_FINDERFEE -- the very vector a v36
// author is forbidden to emit. Dual-version bar: v36 => fee-LESS, v35 =>
// fee-PRESENT, from the SAME production helpers.
{
auto payouts = pplns_v35({{A,60},{B,40}}, 0);
apply_v35_finder_fee(payouts, /*creator==finder==*/A, DON, SUBSIDY);
auto outs = assemble_v36_coinbase_outputs(payouts, DON, SUBSIDY);
assert(outs.size() == 3);
assert(outs[0].first == B && outs[0].second == 398000000ULL);
assert(outs[1].first == A && outs[1].second == 601999999ULL); // 199/200 + fee, -1 marker
assert(outs[2].first == DON && outs[2].second == 1ULL);
const std::string ORACLE_WITH_FINDERFEE =
"80ffb817000000001976a914bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb88ac"
"7fcae123000000001976a914aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa88ac"
"010000000000000017a9148c6272621d89e8fa526dd86acff60c7136be8e8587";
assert(serialize_outsection(outs) == ORACLE_WITH_FINDERFEE);
std::cout << "[KAT] case4 sub-36 finder-fee author byte-parity -- PASS\n";
}

std::cout << "[KAT] bch G2 coinbase-author KAT -- ALL CASES PASS\n";
return 0;
}
Loading