diff --git a/src/impl/dgb/share_check.hpp b/src/impl/dgb/share_check.hpp new file mode 100644 index 000000000..249518b68 --- /dev/null +++ b/src/impl/dgb/share_check.hpp @@ -0,0 +1,3307 @@ +#pragma once + +// P2: Share verification — check_hash_link, check_merkle_link, share init/check +// Ported from legacy sharechains/data.cpp + sharechains/share.cpp + +#include "config_pool.hpp" +#include "share.hpp" +#include "share_messages.hpp" +#include "share_types.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace dgb +{ + +// P2Pool witness nonce: '[P2Pool]' repeated 4 times = 32 bytes +// Used for witness commitment: SHA256d(wtxid_merkle_root || P2POOL_WITNESS_NONCE) +static const unsigned char P2POOL_WITNESS_NONCE[32] = { + 0x5b, 0x50, 0x32, 0x50, 0x6f, 0x6f, 0x6c, 0x5d, + 0x5b, 0x50, 0x32, 0x50, 0x6f, 0x6f, 0x6c, 0x5d, + 0x5b, 0x50, 0x32, 0x50, 0x6f, 0x6f, 0x6c, 0x5d, + 0x5b, 0x50, 0x32, 0x50, 0x6f, 0x6f, 0x6c, 0x5d, +}; + +// Compute P2Pool witness commitment hash from raw wtxid merkle root. +// Returns SHA256d(root || '[P2Pool]'*4) +inline uint256 compute_p2pool_witness_commitment(const uint256& wtxid_merkle_root) { + uint256 nonce; + std::memcpy(nonce.data(), P2POOL_WITNESS_NONCE, 32); + return Hash(wtxid_merkle_root, nonce); +} + +// ============================================================================ +// check_hash_link() +// +// Restores SHA256 mid-state from hash_link, continues hashing with +// (data + extra), then double-SHA256 finalises to the gentx_hash. +// +// Legacy: sharechains/data.cpp check_hash_link() +// ============================================================================ +template +inline uint256 check_hash_link(const HashLinkT& hash_link, + const std::vector& data, + const std::vector& const_ending = {}) +{ + const uint64_t extra_length = hash_link.m_length % 64; // 512/8 = 64 + + // hash_link.extra_data handling: + // Python reference (check_hash_link in p2pool/data.py): + // extra = (extra_data + const_ending)[len(extra_data) + len(const_ending) - extra_length:] + // This takes the LAST extra_length bytes of (extra_data + const_ending), + // i.e. extra_data first, then const_ending tail — matching the original + // byte order in the coinbase prefix (after the full SHA256 blocks). + std::vector extra; + if constexpr (requires { hash_link.m_extra_data.m_data; }) + { + // V36HashLinkType: extra_data is BaseScript (VarStr) + extra.assign(hash_link.m_extra_data.m_data.begin(), + hash_link.m_extra_data.m_data.end()); + if (extra.size() < extra_length) + { + // Append const_ending tail AFTER extra_data (matching Python's order) + auto needed = extra_length - extra.size(); + if (const_ending.size() >= needed) + extra.insert(extra.end(), const_ending.end() - needed, const_ending.end()); + } + } + else + { + // Pre-V36: extra_data always empty, use const_ending tail + extra.assign(const_ending.begin(), const_ending.end()); + if (extra.size() > extra_length) + extra.erase(extra.begin(), extra.begin() + (extra.size() - extra_length)); + } + if (extra.size() != extra_length) + throw std::runtime_error("check_hash_link: extra size mismatch"); + + // Restore SHA256 mid-state from hash_link.m_state (32 bytes, big-endian) + const auto& state_bytes = hash_link.m_state.m_data; + uint32_t init_state[8] = { + ReadBE32(state_bytes.data() + 0), + ReadBE32(state_bytes.data() + 4), + ReadBE32(state_bytes.data() + 8), + ReadBE32(state_bytes.data() + 12), + ReadBE32(state_bytes.data() + 16), + ReadBE32(state_bytes.data() + 20), + ReadBE32(state_bytes.data() + 24), + ReadBE32(state_bytes.data() + 28), + }; + + // Continue hashing from mid-state: Write(data) fills the partial + // block (extra is already in buf via the constructor), then processes + // remaining full blocks. Single SHA256 pass. + unsigned char out1[CSHA256::OUTPUT_SIZE]; + CSHA256(init_state, extra, hash_link.m_length) + .Write(data.data(), data.size()) + .Finalize(out1); + + // Second SHA256 pass → double-SHA256 + unsigned char out2[CSHA256::OUTPUT_SIZE]; + CSHA256().Write(out1, CSHA256::OUTPUT_SIZE).Finalize(out2); + + // Write raw double-SHA256 output directly into uint256 + // (matches Bitcoin Core's Hash() which does the same) + uint256 result; + std::memcpy(result.data(), out2, 32); + return result; +} + +// ============================================================================ +// prefix_to_hash_link() +// +// Forward computation of hash_link: given a coinbase prefix (everything up to +// and including the const_ending), capture the SHA256 mid-state so that +// check_hash_link() can resume and produce the coinbase txid. +// +// Python reference (p2pool): +// def prefix_to_hash_link(prefix, const_ending=''): +// x = sha256(prefix) +// return dict(state=x.state, extra_data=x.buf[:max(0,len(x.buf)-len(const_ending))], +// length=x.length//8) +// ============================================================================ +inline V36HashLinkType prefix_to_hash_link( + const std::vector& prefix, + const std::vector& const_ending) +{ + // Feed the entire prefix into a CSHA256 + CSHA256 hasher; + hasher.Write(prefix.data(), prefix.size()); + + V36HashLinkType result; + + // Extract mid-state as big-endian bytes (matches check_hash_link's ReadBE32) + result.m_state.m_data.resize(32); + for (int i = 0; i < 8; ++i) { + WriteBE32(result.m_state.m_data.data() + i * 4, hasher.s[i]); + } + + // extra_data = buf[0 .. bufsize - const_ending_len] + size_t bufsize = hasher.bytes % 64; + size_t extra_len = (bufsize > const_ending.size()) ? (bufsize - const_ending.size()) : 0; + result.m_extra_data.m_data.assign(hasher.buf, hasher.buf + extra_len); + + // length = total bytes processed so far + result.m_length = hasher.bytes; + + return result; +} + +// prefix_to_hash_link for V35: returns HashLinkType (no extra_data field) +inline HashLinkType prefix_to_hash_link_v35( + const std::vector& prefix, + const std::vector& const_ending) +{ + auto v36_link = prefix_to_hash_link(prefix, const_ending); + HashLinkType result; + result.m_state = v36_link.m_state; + result.m_length = v36_link.m_length; + // V35: extra_data is always empty (FixedStrType(0)) — the donation script + // is long enough to consume the entire SHA256 buffer tail. + return result; +} + +// ============================================================================ +// check_merkle_link() +// +// Walk a Merkle branch to compute the root from a given tip_hash. +// +// Legacy: libcoind/data.cpp check_merkle_link() +// ============================================================================ +inline uint256 check_merkle_link(const uint256& tip_hash, const MerkleLink& link) +{ + if (link.m_branch.size() > 0 && + link.m_index >= (1u << link.m_branch.size())) + throw std::invalid_argument("check_merkle_link: index too large"); + + uint256 cur = tip_hash; + for (size_t i = 0; i < link.m_branch.size(); ++i) + { + // Combine: if bit i of index is set, branch[i] is on the left + PackStream ps; + if ((link.m_index >> i) & 1) + { + ps << link.m_branch[i]; + ps << cur; + } + else + { + ps << cur; + ps << link.m_branch[i]; + } + + // double-SHA256 of the 64-byte concatenation + auto sp = std::span( + reinterpret_cast(ps.data()), ps.size()); + cur = Hash(sp); + } + return cur; +} + +// ============================================================================ +// compute_gentx_before_refhash() +// +// Computes the constant ending bytes that appear after the coinbase outputs +// and before the ref_hash in the serialised coinbase transaction. +// +// Legacy: networks/network.cpp "init gentx_before_refhash" +// Formula: VarStr(DONATION_SCRIPT) + int64(0) + VarStr(0x6a28 + int256(0) + int64(0))[:3] +// ============================================================================ +inline std::vector compute_gentx_before_refhash(int64_t share_version, + const core::CoinParams& params) +{ + std::vector result; + + // 1. VarStr(DONATION_SCRIPT) + auto donation_script = params.donation_script_func(share_version); + { + PackStream s; + BaseScript bs; + bs.m_data = donation_script; + s << bs; + auto* p = reinterpret_cast(s.data()); + result.insert(result.end(), p, p + s.size()); + } + + // 2. int64(0) + { + uint64_t zero64 = 0; + auto* p = reinterpret_cast(&zero64); + result.insert(result.end(), p, p + 8); + } + + // 3. VarStr(0x6a 0x28 + int256(0) + int64(0)) — but only the first 3 bytes + { + PackStream inner; + // raw bytes: OP_RETURN (0x6a) + PUSH_40 (0x28) + unsigned char prefix[2] = {0x6a, 0x28}; + inner.write(std::span(reinterpret_cast(prefix), 2)); + // 32 zero bytes (uint256(0)) + uint256 zero256; + inner << zero256; + // 8 zero bytes (uint64(0)) + uint64_t zero64 = 0; + inner.write(std::span(reinterpret_cast(&zero64), 8)); + + // Pack as VarStr + PackStream outer; + BaseScript bs; + bs.m_data.resize(inner.size()); + std::memcpy(bs.m_data.data(), inner.data(), inner.size()); + outer << bs; + + // Take only the first 3 bytes + auto* p = reinterpret_cast(outer.data()); + result.insert(result.end(), p, p + std::min(3, outer.size())); + } + + return result; +} + +// ============================================================================ +// pubkey_hash_to_address() +// +// Convert (pubkey_hash, pubkey_type) to a Litecoin address string. +// Used for V35 share_data.address field (VarStr). +// pubkey_type: 0=P2PKH, 1=P2WPKH, 2=P2SH (same as V36 encoding) +// ============================================================================ +inline std::string pubkey_hash_to_address(const uint160& pubkey_hash, uint8_t pubkey_type, + const core::CoinParams& params) +{ + if (pubkey_type == 0) { + // P2PKH: Base58Check with version byte + std::vector payload(21); + payload[0] = params.address_version; + std::memcpy(payload.data() + 1, pubkey_hash.data(), 20); + return EncodeBase58Check({payload.data(), payload.size()}); + } else if (pubkey_type == 1) { + // P2WPKH: Bech32 segwit v0 + std::vector prog(20); + std::memcpy(prog.data(), pubkey_hash.data(), 20); + return bech32::encode_segwit(params.bech32_hrp, 0, prog); + } else if (pubkey_type == 2) { + // P2SH: Base58Check with version byte + std::vector payload(21); + payload[0] = params.address_p2sh_version; + std::memcpy(payload.data() + 1, pubkey_hash.data(), 20); + return EncodeBase58Check({payload.data(), payload.size()}); + } + // Fallback: P2PKH + std::vector payload(21); + payload[0] = params.address_version; + std::memcpy(payload.data() + 1, pubkey_hash.data(), 20); + return EncodeBase58Check({payload.data(), payload.size()}); +} + +// ============================================================================ +// compute_ref_hash_for_work() +// +// Computes the p2pool ref_hash for a set of share fields. Used at Stratum +// work generation time to build the OP_RETURN commitment per connection. +// +// Parameters mirror the share fields that feed into the ref_stream. +// Returns (ref_hash, last_txout_nonce). +// ============================================================================ +struct RefHashParams { + int64_t share_version{36}; // 35 or 36 — determines serialization format + uint256 prev_share; + std::vector coinbase_scriptSig; + uint32_t share_nonce{0}; + // V36: pubkey_hash + pubkey_type; V35: address (string bytes) + uint160 pubkey_hash; + uint8_t pubkey_type{0}; + std::string address; // V35: base58/bech32 address string + uint64_t subsidy{0}; + uint16_t donation{50}; + uint8_t stale_info{0}; + uint64_t desired_version{36}; + bool has_segwit{false}; + SegwitData segwit_data; + std::vector merged_addresses; // V36 only + uint256 far_share_hash; + uint32_t max_bits{0}; + uint32_t bits{0}; + uint32_t timestamp{0}; + uint32_t absheight{0}; + uint128 abswork; + std::vector merged_coinbase_info; // V36: per-chain DOGE header + merkle proof + uint256 merged_payout_hash; // V36 only + BaseScript message_data; // V36 PossiblyNoneType(b'', VarStrType()) +}; + +inline std::pair compute_ref_hash_for_work(const RefHashParams& p, + const core::CoinParams& params) +{ + PackStream ref_stream; + + // IDENTIFIER + { + auto hex = params.active_identifier_hex(); + for (size_t i = 0; i + 1 < hex.size(); i += 2) { + unsigned char byte = static_cast( + std::stoul(hex.substr(i, 2), nullptr, 16)); + ref_stream.write(std::span( + reinterpret_cast(&byte), 1)); + } + } + + ref_stream << p.prev_share; + + // coinbase as VarStr + { + BaseScript bs; + bs.m_data = p.coinbase_scriptSig; + ref_stream << bs; + } + + ref_stream << p.share_nonce; + + if (p.share_version >= 36) { + // V36: pubkey_hash (uint160) + pubkey_type (uint8) + ref_stream << p.pubkey_hash; + ref_stream << p.pubkey_type; + ::Serialize(ref_stream, VarInt(p.subsidy)); + } else if (p.share_version >= 34) { + // V34-V35: address as VarStr + BaseScript addr_bs; + addr_bs.m_data.assign(p.address.begin(), p.address.end()); + ref_stream << addr_bs; + ref_stream << p.subsidy; // fixed uint64 + } else { + // Pre-V34: pubkey_hash only + ref_stream << p.pubkey_hash; + ref_stream << p.subsidy; // fixed uint64 + } + + ref_stream << p.donation; + ref_stream << p.stale_info; + ::Serialize(ref_stream, VarInt(p.desired_version)); + + if (p.has_segwit) + ref_stream << p.segwit_data; + + // V36: merged_addresses (after segwit_data, before far_share_hash) + if (p.share_version >= 36) + ref_stream << p.merged_addresses; + + ref_stream << p.far_share_hash; + ref_stream << p.max_bits; + ref_stream << p.bits; + ref_stream << p.timestamp; + ref_stream << p.absheight; + + if (p.share_version >= 36) { + ::Serialize(ref_stream, Using(p.abswork)); + ref_stream << p.merged_coinbase_info; + ref_stream << p.merged_payout_hash; + // V36 ref_type includes message_data as PossiblyNoneType(b'', VarStrType()) + ref_stream << p.message_data; + } else { + // Pre-V36: abswork as fixed uint128 + ref_stream << p.abswork; + } + + auto ref_span = std::span( + reinterpret_cast(ref_stream.data()), ref_stream.size()); + uint256 ref_hash = Hash(ref_span); + + { + static int rfn_log = 0; + static int rfn_v36_log = 0; + bool should_log = (rfn_log < 3) || (p.share_version >= 36 && rfn_v36_log < 5); + if (should_log) { + rfn_log++; + if (p.share_version >= 36) rfn_v36_log++; + static const char* HX = "0123456789abcdef"; + std::string hex; + auto* rd = reinterpret_cast(ref_stream.data()); + for (size_t i = 0; i < ref_stream.size(); ++i) { hex += HX[rd[i]>>4]; hex += HX[rd[i]&0xf]; } + LOG_INFO << "[REF-FN] v=" << p.share_version + << " ref_packed_len=" << ref_stream.size() + << " ref_hash=" << ref_hash.GetHex() + << " absheight=" << p.absheight << " prev=" << p.prev_share.GetHex().substr(0,16); + LOG_INFO << "[REF-FN-FULL] " << hex; + } + } + + // Generate a random-ish last_txout_nonce + uint64_t nonce = static_cast(std::time(nullptr)) ^ + (static_cast(p.timestamp) << 32) ^ + (static_cast(p.absheight) << 16); + + return {ref_hash, nonce}; +} + +// Thread-local state from share_init_verify — read by attempt_verify() without re-computing scrypt. +inline thread_local bool g_last_init_is_block = false; +inline thread_local uint256 g_last_pow_hash; // scrypt hash of the share header + +// ============================================================================ +// share_init_verify() +// +// Performs the init()-phase verification of a share: +// 1. Basic field validation (coinbase size, merkle branch lengths) +// 2. Compute hash_link_data from ref serialisation +// 3. check_hash_link → gentx_hash +// 4. check_merkle_link → merkle_root +// 5. Build full block header +// 6. Compute hash (double-SHA256 of header) +// 7. Verify pow_hash <= target +// +// Returns the share hash (double-SHA256 of the reconstructed header). +// Throws on any validation failure. +// +// NOTE: The full GenerateShareTransaction reconstruction from check() is +// deferred to a later PR (P2.5). This function covers the PoW + hash-link +// verification path from legacy Share::init(). +// ============================================================================ +template +uint256 share_init_verify(const ShareT& share, const core::CoinParams& params, bool check_pow = true) +{ + // --- Basic validation --- + if (share.m_coinbase.size() < 2 || share.m_coinbase.size() > 100) + throw std::invalid_argument("bad coinbase size"); + + if (share.m_merkle_link.m_branch.size() > 16) + throw std::invalid_argument("merkle branch too long"); + + constexpr int64_t ver = ShareT::version; + + if constexpr (ver >= dgb::SEGWIT_ACTIVATION_VERSION) + { + if constexpr (requires { share.m_segwit_data; }) + { + if (share.m_segwit_data.has_value()) + { + if (share.m_segwit_data->m_txid_merkle_link.m_branch.size() > 16) + throw std::invalid_argument("segwit txid merkle branch too long"); + } + } + } + + // --- Compute ref_hash --- + // RefType serialisation: IDENTIFIER + share_info fields + segwit_data + // Then hash256 it, then check_merkle_link with ref_merkle_link + // + // For now we serialise the minimal fields the same way the legacy code + // does (share_data + share_info + optional segwit_data) via a PackStream. + PackStream ref_stream; + + // IDENTIFIER bytes (8 bytes from IDENTIFIER_HEX) + { + auto hex = params.active_identifier_hex(); + for (size_t i = 0; i + 1 < hex.size(); i += 2) + { + unsigned char byte = static_cast( + std::stoul(hex.substr(i, 2), nullptr, 16)); + ref_stream.write(std::span( + reinterpret_cast(&byte), 1)); + } + } + + // share_info serialisation — we re-serialise the share's share_info fields + // through the same Formatter path that was used to decode them. + // For ref_hash we need: share_data + share_info fields + segwit_data + // We serialise the relevant fields into the ref_stream. + { + // prev_hash + ref_stream << share.m_prev_hash; + // coinbase + ref_stream << share.m_coinbase; + // nonce (uint32_t LE) + ref_stream << share.m_nonce; + + // address or pubkey_hash — V34/V35 use m_address (VarStr), + // V36+ uses m_pubkey_hash (uint160) + m_pubkey_type (uint8_t), + // pre-V34 uses m_pubkey_hash only. + if constexpr (requires { share.m_address; }) + ref_stream << share.m_address; + else if constexpr (requires { share.m_pubkey_type; }) + { + ref_stream << share.m_pubkey_hash; + ref_stream << share.m_pubkey_type; + } + else + ref_stream << share.m_pubkey_hash; + + // subsidy: VarInt for V36+, raw uint64_t LE for older + if constexpr (ver >= 36) + ::Serialize(ref_stream, VarInt(share.m_subsidy)); + else + ref_stream << share.m_subsidy; + + ref_stream << share.m_donation; + // stale_info as EnumType> — single byte + { + uint8_t si = static_cast(share.m_stale_info); + ref_stream << si; + } + // desired_version as VarInt + { + uint64_t dv = share.m_desired_version; + ::Serialize(ref_stream, VarInt(dv)); + } + + // segwit_data (optional) + if constexpr (requires { share.m_segwit_data; }) + { + if constexpr (ver >= dgb::SEGWIT_ACTIVATION_VERSION) + { + // PossiblyNoneType: ALWAYS serialize (p2pool writes default when None) + if (share.m_segwit_data.has_value()) { + ref_stream << share.m_segwit_data.value(); + } else { + std::vector empty_branch; + ref_stream << empty_branch; + uint256 zero_root; + ref_stream << zero_root; + } + } + } + + // merged_addresses (V36+) + if constexpr (ver >= 36) + { + if constexpr (requires { share.m_merged_addresses; }) + ref_stream << share.m_merged_addresses; + } + + // tx info (pre-v34) + if constexpr (ver < 34) + { + if constexpr (requires { share.m_tx_info; }) + ref_stream << share.m_tx_info; + } + + // far_share_hash, max_bits, bits, timestamp, absheight + ref_stream << share.m_far_share_hash; + ref_stream << share.m_max_bits; + ref_stream << share.m_bits; + ref_stream << share.m_timestamp; + ref_stream << share.m_absheight; + + // abswork: AbsworkV36Format for V36+, raw uint128 LE for older + if constexpr (ver >= 36) + { + if constexpr (requires { share.m_abswork; }) + ::Serialize(ref_stream, Using(share.m_abswork)); + } + else + { + ref_stream << share.m_abswork; + } + + // V36+ merged mining commitment fields + if constexpr (ver >= 36) + { + if constexpr (requires { share.m_merged_coinbase_info; }) + ref_stream << share.m_merged_coinbase_info; + if constexpr (requires { share.m_merged_payout_hash; }) + ref_stream << share.m_merged_payout_hash; + } + } + + // V36 ref_type includes message_data as PossiblyNoneType(b'', VarStrType()) + // When m_message_data is empty, BaseScript serialises as varint(0) = 0x00. + if constexpr (ver >= 36) + { + if constexpr (requires { share.m_message_data; }) + ref_stream << share.m_message_data; + } + + // hash256 of the ref_type serialisation + auto ref_span = std::span( + reinterpret_cast(ref_stream.data()), ref_stream.size()); + uint256 hash_ref = Hash(ref_span); + + // check_merkle_link with ref_merkle_link + uint256 ref_hash = check_merkle_link(hash_ref, share.m_ref_merkle_link); + + // --- Build hash_link_data --- + // hash_link_data = ref_hash bytes + pack(last_txout_nonce, LE64) + pack(0, LE32) + std::vector hash_link_data; + hash_link_data.insert(hash_link_data.end(), ref_hash.data(), ref_hash.data() + 32); + { + // last_txout_nonce as little-endian uint64 + uint64_t nonce = share.m_last_txout_nonce; + auto* p = reinterpret_cast(&nonce); + hash_link_data.insert(hash_link_data.end(), p, p + 8); + } + { + // trailing zero uint32 + uint32_t zero = 0; + auto* p = reinterpret_cast(&zero); + hash_link_data.insert(hash_link_data.end(), p, p + 4); + } + + auto gentx_before_refhash = compute_gentx_before_refhash(ver, params); + + // --- check_hash_link → gentx_hash --- + uint256 gentx_hash = check_hash_link(share.m_hash_link, hash_link_data, gentx_before_refhash); + + // Diagnostic: compare hash_link-derived gentx_hash with expected + // Only log for self-validation (check_pow=true means local share) + if (check_pow) { + static int verify_diag = 0; + if (verify_diag < 10) { + LOG_INFO << "[verify-diag] gentx_hash(hash_link)=" << gentx_hash.GetHex() + << " hash_link_data_len=" << hash_link_data.size(); + ++verify_diag; + } + } + + // --- Merkle root --- + // For segwit-activated shares, use segwit_data.txid_merkle_link; otherwise merkle_link + uint256 merkle_root; + if constexpr (ver >= dgb::SEGWIT_ACTIVATION_VERSION) + { + if constexpr (requires { share.m_segwit_data; }) + { + if (share.m_segwit_data.has_value()) + merkle_root = check_merkle_link(gentx_hash, share.m_segwit_data->m_txid_merkle_link); + else + merkle_root = check_merkle_link(gentx_hash, share.m_merkle_link); + } + else + { + merkle_root = check_merkle_link(gentx_hash, share.m_merkle_link); + } + } + else + { + merkle_root = check_merkle_link(gentx_hash, share.m_merkle_link); + } + + // --- Reconstruct full block header and compute hash --- + // BlockHeaderType: version(int32) + previous_block + merkle_root + timestamp + bits + nonce + // Note: the full block header uses fixed 4-byte version (not VarInt like SmallBlockHeaderType) + PackStream header_stream; + { + uint32_t hdr_version = static_cast(share.m_min_header.m_version); + header_stream << hdr_version; + } + header_stream << share.m_min_header.m_previous_block; + header_stream << merkle_root; + header_stream << share.m_min_header.m_timestamp; + header_stream << share.m_min_header.m_bits; + header_stream << share.m_min_header.m_nonce; + + // hash = double-SHA256 of the header (the share's identity) + auto hdr_span = std::span( + reinterpret_cast(header_stream.data()), header_stream.size()); + uint256 share_hash = Hash(hdr_span); + + // --- PoW check (scrypt) --- + // For Litecoin the POW_FUNC is scrypt(1024,1,1,256). + // Blocks are identified by SHA256d, but PoW validity uses scrypt hash. + if (check_pow) + { + uint256 target = chain::bits_to_target(share.m_bits); + if (target.IsNull()) + throw std::invalid_argument("share target is zero"); + if (target > params.max_target) + throw std::invalid_argument("share target exceeds MAX_TARGET"); + auto max_target = chain::bits_to_target(share.m_max_bits); + if (!max_target.IsNull() && target > max_target) + throw std::invalid_argument("share target exceeds max_target — too easy"); + + // Compute PoW hash using coin-specific function (scrypt for LTC, x11 for Dash, etc.) + auto hdr_pow_span = std::span( + reinterpret_cast(header_stream.data()), header_stream.size()); + uint256 pow_hash = params.pow_func(hdr_pow_span); + g_last_pow_hash = pow_hash; // cache for attempt_verify merged check + + if (pow_hash > target) + { + // Expected for stratum pseudoshares: VARDIFF target is much lower + // than share target, so most submissions won't meet PoW. + // Only a real share (1 in ~20000 pseudoshares) passes this check. + LOG_TRACE << "PoW below share target: bits=" << share.m_bits + << " target=" << target.GetHex().substr(0,32) + << " pow_hash=" << pow_hash.GetHex().substr(0,32); + throw std::invalid_argument("share PoW hash does not meet target"); + } + + // Block detection: check if share's scrypt hash also meets the BLOCK target. + // min_header.m_bits = block difficulty from GBT (much harder than share target). + // When pow_hash <= block_target, this share IS a solved block! + uint256 block_target = chain::bits_to_target(share.m_min_header.m_bits); + g_last_init_is_block = false; + if (!block_target.IsNull() && pow_hash <= block_target) { + g_last_init_is_block = true; + static uint256 s_last_block_share; + if (share_hash != s_last_block_share) { + s_last_block_share = share_hash; + LOG_INFO << "[BLOCK] Peer share meets block target" + << " hash=" << share_hash.GetHex().substr(0,16); + } + } + } + + return share_hash; +} + +// ============================================================================ +// Helper: convert pubkey_hash + type to full scriptPubKey +// ============================================================================ +inline std::vector pubkey_hash_to_script(const uint160& hash, uint8_t type = 0) +{ + std::vector script; + auto h = hash.GetChars(); // little-endian (internal storage order) + switch (type) + { + case 1: // P2WPKH: OP_0 <20> + // Use GetChars() output directly — same as P2PKH and P2SH. + // The bytes in m_pubkey_hash are the raw witness program as + // deserialized from the wire. No reversal needed. + script.reserve(22); + script.push_back(0x00); + script.push_back(0x14); + script.insert(script.end(), h.begin(), h.end()); + break; + case 2: // P2SH: OP_HASH160 <20> OP_EQUAL + script.reserve(23); + script.push_back(0xa9); + script.push_back(0x14); + script.insert(script.end(), h.begin(), h.end()); + script.push_back(0x87); + break; + default: // P2PKH: OP_DUP OP_HASH160 <20> OP_EQUALVERIFY OP_CHECKSIG + script.reserve(25); + script.push_back(0x76); + script.push_back(0xa9); + script.push_back(0x14); + script.insert(script.end(), h.begin(), h.end()); + script.push_back(0x88); + script.push_back(0xac); + break; + } + return script; +} + +// ============================================================================ +// Normalize a parent chain script to merged chain P2PKH script. +// +// P2WPKH (00 14 ) → P2PKH (76 a9 14 88 ac) [same pubkey_hash] +// P2PKH (76 a9 14 88 ac) → passed through +// P2SH (a9 14 87) → P2SH (passed through) +// P2WSH (00 20 ) → empty (unconvertible) +// P2TR (51 20 ) → empty (unconvertible) +// +// Returns empty vector for unconvertible scripts (Tier 3: redistributed). +// Matches Python data.py:build_canonical_merged_coinbase() conversion logic. +// ============================================================================ +inline std::vector normalize_script_for_merged( + const std::vector& script) +{ + // P2PKH (25 bytes: 76 a9 14 <20> 88 ac) — already correct + if (script.size() == 25 && script[0] == 0x76 && script[1] == 0xa9 && + script[2] == 0x14 && script[23] == 0x88 && script[24] == 0xac) + return script; + + // P2WPKH (22 bytes: 00 14 <20>) — convert to P2PKH using same hash + if (script.size() == 22 && script[0] == 0x00 && script[1] == 0x14) + { + std::vector p2pkh; + p2pkh.reserve(25); + p2pkh.push_back(0x76); // OP_DUP + p2pkh.push_back(0xa9); // OP_HASH160 + p2pkh.push_back(0x14); // PUSH 20 + p2pkh.insert(p2pkh.end(), script.begin() + 2, script.end()); // + p2pkh.push_back(0x88); // OP_EQUALVERIFY + p2pkh.push_back(0xac); // OP_CHECKSIG + return p2pkh; + } + + // P2SH (23 bytes: a9 14 <20> 87) — pass through (DOGE supports P2SH) + if (script.size() == 23 && script[0] == 0xa9 && script[1] == 0x14 && + script[22] == 0x87) + return script; + + // P2WSH (34 bytes: 00 20 <32>) or P2TR (34 bytes: 51 20 <32>) — unconvertible + return {}; +} + +// MERGED: prefix for weight map keys — matches p2pool's 'MERGED:' + hex string. +// Keeps Tier 1/1.5 (explicit DOGE script) keys separate from raw LTC script keys +// in the same weight map, preventing V35+V36 weight collapse for the same miner. +// 7 bytes: 0x4d 0x45 0x52 0x47 0x45 0x44 0x3a = "MERGED:" +inline constexpr std::array MERGED_KEY_PREFIX = { + 0x4d, 0x45, 0x52, 0x47, 0x45, 0x44, 0x3a +}; + +// Prepend MERGED: prefix to a script for use as a weight map key. +inline std::vector make_merged_key( + const std::vector& script) +{ + std::vector key; + key.reserve(MERGED_KEY_PREFIX.size() + script.size()); + key.insert(key.end(), MERGED_KEY_PREFIX.begin(), MERGED_KEY_PREFIX.end()); + key.insert(key.end(), script.begin(), script.end()); + return key; +} + +// Check if a weight key has the MERGED: prefix. +inline bool is_merged_key(const std::vector& key) +{ + return key.size() > MERGED_KEY_PREFIX.size() && + std::equal(MERGED_KEY_PREFIX.begin(), MERGED_KEY_PREFIX.end(), + key.begin()); +} + +// Strip MERGED: prefix, returning the raw script bytes. +// Caller must check is_merged_key() first. +inline std::vector strip_merged_key( + const std::vector& key) +{ + return std::vector( + key.begin() + MERGED_KEY_PREFIX.size(), key.end()); +} + +// Resolve a weight map key to a DOGE-compatible scriptPubKey. +// MERGED:-prefixed keys: strip prefix, use directly (already a DOGE script). +// Raw keys: autoconvert (P2WPKH→P2PKH, P2PKH/P2SH pass through). +// Returns empty if unconvertible (P2WSH, P2TR, etc.). +inline std::vector resolve_merged_payout_script( + const std::vector& key) +{ + if (is_merged_key(key)) + return strip_merged_key(key); + return normalize_script_for_merged(key); +} + +// ============================================================================ +// Helper: extract full scriptPubKey from a share variant +// ============================================================================ +inline std::vector get_share_script(const auto* obj) +{ + if constexpr (requires { obj->m_pubkey_type; }) + return pubkey_hash_to_script(obj->m_pubkey_hash, obj->m_pubkey_type); + else if constexpr (requires { obj->m_address; }) + { + // V34/V35: m_address contains a human-readable address string. + // Convert to scriptPubKey for PPLNS weight computation. + std::string addr_str(obj->m_address.m_data.begin(), obj->m_address.m_data.end()); + auto script = core::address_to_script(addr_str); + if (!script.empty()) + return script; + // Fallback: return raw bytes (shouldn't happen with valid addresses) + return obj->m_address.m_data; + } + else + return pubkey_hash_to_script(obj->m_pubkey_hash, 0); +} + +// ============================================================================ +// generate_share_transaction() +// +// Reconstructs the expected coinbase transaction from a share's fields and +// the PPLNS weights computed from the share chain. Returns the expected +// gentx txid (double-SHA256 of the non-witness serialised transaction). +// +// This is the C++ port of p2pool v36's generate_transaction() / check(). +// +// The coinbase structure is: +// tx_ins: [ { prev_output: 0...0:ffffffff, script: coinbase } ] +// tx_outs: [ segwit_commitment?, +// ...pplns_payout_outputs..., +// donation_output, +// op_return_commitment ] +// lock_time: 0 +// +// Reference: frstrtr/p2pool-merged-v36 p2pool/data.py generate_transaction() +// ============================================================================ +template +uint256 generate_share_transaction(const ShareT& share, TrackerT& tracker, const core::CoinParams& params, bool dump_diag = false, bool v36_active = false) +{ + auto gst_t0 = std::chrono::steady_clock::now(); + constexpr int64_t ver = ShareT::version; + // p2pool selects PPLNS formula by runtime AutoRatchet state, not compile-time + // share version. When v36_active is true (AutoRatchet ACTIVATED/CONFIRMED), + // use v36 PPLNS even for v35 shares. Ref: p2pool data.py:879, work.py:759. + const bool use_v36_pplns = v36_active || (ver >= 36); + const uint64_t subsidy = share.m_subsidy; + const uint16_t donation = share.m_donation; + + // Debug: log the PPLNS formula selection for cross-impl comparison + { + static int gst_pplns_log = 0; + if (gst_pplns_log++ % 50 == 0) { + LOG_DEBUG_DIAG << "[GST-PPLNS] v36_active=" << v36_active + << " use_v36_pplns=" << use_v36_pplns + << " ver=" << ver + << " start=" << share.m_prev_hash.GetHex().substr(0, 16) + << " subsidy=" << subsidy + << " donation=" << donation; + } + } + + // --- 1. Compute PPLNS weights with full scriptPubKey keys --- + // Walk from share's prev_hash (parent) backward through the chain. + // This matches the Python: weights are computed relative to the share's parent. + + auto prev_hash = share.m_prev_hash; + std::map, uint288> weights; + uint288 total_weight; + uint288 total_donation_weight; + + if (!prev_hash.IsNull() && tracker.chain.contains(prev_hash)) + { + // p2pool data.py:762-764 — refuse to compute PPLNS with insufficient depth. + // Without this guard, attempt_verify() (which allows CHAIN_LENGTH+1) can + // trigger a PPLNS walk that terminates early, producing wrong coinbase + // amounts and causing persistent GENTX-MISMATCH during bootstrap. + auto chain_len = static_cast(params.real_chain_length); + { + auto pplns_height = tracker.chain.get_height(prev_hash); + auto pplns_last = tracker.chain.get_last(prev_hash); + if (!(pplns_height >= chain_len || pplns_last.IsNull())) + throw std::invalid_argument( + "share chain not long enough for PPLNS verification (height=" + + std::to_string(pplns_height) + " need=" + + std::to_string(chain_len) + ")"); + } + + // block_target from block header bits (matches Python: self.header['bits'].target) + auto block_target = chain::bits_to_target(share.m_min_header.m_bits); + auto max_weight = chain::target_to_average_attempts(block_target) + * params.spread * 65535; + + // PPLNS formula selected by runtime v36_active (AutoRatchet state), + // not compile-time share version. Ref: p2pool data.py:879, work.py:759. + if (use_v36_pplns) { + // V36 PPLNS: exponential depth-decay, walk from parent + uint288 unlimited_weight; + unlimited_weight.SetHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + auto result = tracker.get_v36_decayed_cumulative_weights(prev_hash, chain_len, unlimited_weight); + weights = std::move(result.weights); + total_weight = result.total_weight; + total_donation_weight = result.total_donation_weight; + } else { + // Pre-V36 PPLNS: flat cumulative weights (no decay) + // CRITICAL: Walk from GRANDPARENT for HEIGHT-1 shares. + // p2pool data.py:884-885: + // _pplns_start = previous_share.share_data['previous_share_hash'] + // _pplns_max_shares = max(0, min(height, REAL_CHAIN_LENGTH) - 1) + uint256 pplns_start; + tracker.chain.get(prev_hash).share.invoke([&](auto* s) { + pplns_start = s->m_prev_hash; // grandparent + }); + auto available = tracker.chain.get_height(prev_hash); + auto walk_count = static_cast( + std::max(0, std::min(chain_len, available) - 1)); + + if (!pplns_start.IsNull() && tracker.chain.contains(pplns_start) && walk_count > 0) { + auto result = tracker.get_cumulative_weights(pplns_start, walk_count, max_weight); + weights = std::move(result.weights); + total_weight = result.total_weight; + total_donation_weight = result.total_donation_weight; + } + } + } + + // --- 2. Convert weights to exact integer payout amounts --- + // Python formula: + // Pre-V36: amounts[script] = subsidy * (199 * weight) / (200 * total_weight) + // amounts[finder] += subsidy // 200 + // V36: amounts[script] = subsidy * weight / total_weight + // donation = subsidy - sum(amounts) + + auto gst_t1 = std::chrono::steady_clock::now(); // after PPLNS walk + std::map, uint64_t> amounts; + + // Periodic dump of PPLNS weights for cross-impl comparison + { + static auto last_amt_dump = std::chrono::steady_clock::now() - std::chrono::seconds(60); + auto now_d = std::chrono::steady_clock::now(); + if (now_d - last_amt_dump > std::chrono::seconds(30) && weights.size() >= 2) { + last_amt_dump = now_d; + LOG_DEBUG_DIAG << "[PPLNS-AMT] subsidy=" << subsidy + << " total_weight=" << total_weight.GetLow64() + << " don_weight=" << total_donation_weight.GetLow64() + << " addrs=" << weights.size() + << " prev=" << prev_hash.GetHex().substr(0, 16); + for (auto& [s, w] : weights) { + uint64_t a = (total_weight.IsNull()) ? 0 : + (uint288(subsidy) * w / total_weight).GetLow64(); + LOG_DEBUG_DIAG << "[PPLNS-AMT] weight=" << w.GetLow64() + << " amount=" << a; + } + } + } + + if (!total_weight.IsNull()) + { + for (auto& [script, weight] : weights) + { + uint64_t amount; + if (use_v36_pplns) + { + // V36: amounts[script] = subsidy * weight / total_weight + uint288 num = uint288(subsidy) * weight; + amount = (num / total_weight).GetLow64(); + } + else + { + // Pre-V36: amounts[script] = subsidy * (199 * weight) / (200 * total_weight) + uint288 num = uint288(subsidy) * (weight * 199); + uint288 den = total_weight * 200; + amount = (num / den).GetLow64(); + } + if (amount > 0) + amounts[script] = amount; + } + } + + // Pre-V36: add 0.5% finder fee to share creator + if (!use_v36_pplns) + { + auto finder_script = get_share_script(&share); + amounts[finder_script] += subsidy / 200; + } + + // Donation output = subsidy minus sum of all payout amounts + uint64_t sum_amounts = 0; + for (auto& [s, a] : amounts) + sum_amounts += a; + uint64_t donation_amount = (subsidy > sum_amounts) ? (subsidy - sum_amounts) : 0; + + // Dump amounts for cross-impl debugging + if (dump_diag) { + LOG_DEBUG_DIAG << "[GST-AMOUNTS] subsidy=" << subsidy << " addrs=" << amounts.size() + << " sum=" << sum_amounts << " donation=" << donation_amount + << " prev=" << prev_hash.GetHex().substr(0,16); + for (auto& [s, a] : amounts) { + static const char* HX = "0123456789abcdef"; + 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]; } + LOG_DEBUG_DIAG << "[GST-AMOUNTS] " << sh << "... = " << a; + } + } + + // V36 consensus: donation output must carry >= 1 satoshi (a60f7f7f) + if (use_v36_pplns) { + if (donation_amount < 1 && subsidy > 0 && !amounts.empty()) { + // Deduct 1 sat from the largest miner payout + // Deterministic tiebreak: (amount, script) — largest script wins when equal + auto largest = std::max_element(amounts.begin(), amounts.end(), + [](const auto& a, const auto& b) { + if (a.second != b.second) return a.second < b.second; + return a.first < b.first; + }); + if (largest != amounts.end() && largest->second > 0) { + largest->second -= 1; + sum_amounts -= 1; + donation_amount = subsidy - sum_amounts; + } + } + } + + // --- 3. Build sorted output list --- + auto gst_t2 = std::chrono::steady_clock::now(); // after amounts + // Python: sorted(dests, key=lambda a: (amounts[a], a))[-4000:] + // = ascending by (amount, script), keep last 4000 (highest amounts) + std::vector, uint64_t>> payout_outputs( + amounts.begin(), amounts.end()); + std::sort(payout_outputs.begin(), payout_outputs.end(), + [](const auto& a, const auto& b) { + if (a.second != b.second) return a.second < b.second; // asc by amount + return a.first < b.first; // asc by script for tie-breaking + }); + + // Keep last MAX_OUTPUTS (highest amounts), matching Python's [-4000:] + constexpr size_t MAX_OUTPUTS = 4000; + if (payout_outputs.size() > MAX_OUTPUTS) + payout_outputs.erase(payout_outputs.begin(), payout_outputs.end() - MAX_OUTPUTS); + + // --- 4. Serialise the coinbase transaction --- + // Non-witness serialization (for txid computation): + // version(4) + vin_count(varint) + vin + vout_count(varint) + vouts + locktime(4) + PackStream tx; + + // tx version = 1 + uint32_t tx_version = 1; + tx.write(std::span(reinterpret_cast(&tx_version), 4)); + + // vin count = 1 + { + unsigned char one = 1; + tx.write(std::span(reinterpret_cast(&one), 1)); + } + + // vin[0]: prev_output = 0...0:ffffffff, script = coinbase, sequence = 0 + { + // prev_hash (32 zero bytes) + uint256 zero_hash; + tx << zero_hash; + // prev_index (0xffffffff) + uint32_t prev_idx = 0xffffffff; + tx.write(std::span(reinterpret_cast(&prev_idx), 4)); + // script (VarStr) + tx << share.m_coinbase; + // sequence (0xffffffff — standard coinbase sequence, matches Python) + uint32_t seq = 0xffffffff; + tx.write(std::span(reinterpret_cast(&seq), 4)); + } + + // Count total outputs + size_t n_outs = payout_outputs.size() + 1 /* donation */ + 1 /* OP_RETURN commitment */; + // Segwit commitment output (if applicable) + bool has_segwit = false; + if constexpr (ver >= dgb::SEGWIT_ACTIVATION_VERSION) + { + if constexpr (requires { share.m_segwit_data; }) + { + if (share.m_segwit_data.has_value()) + { + has_segwit = true; + n_outs += 1; + } + } + } + + // vout count (varint — for < 253 outputs, it's a single byte) + if (n_outs < 253) + { + uint8_t cnt = static_cast(n_outs); + tx.write(std::span(reinterpret_cast(&cnt), 1)); + } + else + { + uint8_t marker = 0xfd; + tx.write(std::span(reinterpret_cast(&marker), 1)); + uint16_t cnt = static_cast(n_outs); + tx.write(std::span(reinterpret_cast(&cnt), 2)); + } + + // Helper to write a single tx_out: value(8LE) + script(VarStr) + auto write_txout = [&](uint64_t value, const std::vector& script) { + tx.write(std::span(reinterpret_cast(&value), 8)); + BaseScript bs; + bs.m_data = script; + tx << bs; + }; + + // Segwit commitment output (value=0, script=OP_RETURN + witness_commitment) + if (has_segwit) + { + if constexpr (requires { share.m_segwit_data; }) + { + if (share.m_segwit_data.has_value()) + { + // witness commitment: 0x6a24aa21a9ed + SHA256d(wtxid_merkle_root || '[P2Pool]'*4) + std::vector wscript; + wscript.push_back(0x6a); // OP_RETURN + wscript.push_back(0x24); // PUSH 36 + wscript.push_back(0xaa); + wscript.push_back(0x21); + wscript.push_back(0xa9); + wscript.push_back(0xed); + auto& sd = share.m_segwit_data.value(); + uint256 commitment = compute_p2pool_witness_commitment(sd.m_wtxid_merkle_root); + auto commitment_bytes = commitment.GetChars(); + wscript.insert(wscript.end(), commitment_bytes.begin(), commitment_bytes.end()); + write_txout(0, wscript); + if (dump_diag) { + LOG_INFO << "[WC-GST] wtxid_root=" << sd.m_wtxid_merkle_root.GetHex() + << " commitment=" << commitment.GetHex(); + } + } + } + } + + // PPLNS payout outputs + for (auto& [script, amount] : payout_outputs) + write_txout(amount, script); + + // Donation output — V35 shares always use P2PK DONATION_SCRIPT, + // V36 shares always use P2SH COMBINED_DONATION_SCRIPT. + // Each share was created with the donation script matching its version. + auto donation_script = params.donation_script_func(ver); + write_txout(donation_amount, donation_script); + + // OP_RETURN commitment: value=0, script = 0x6a28 + ref_hash(32) + last_txout_nonce(8) + { + // We need the ref_hash — recompute it from the share the same way share_init_verify does + PackStream ref_stream; + + // IDENTIFIER bytes + { + auto hex = params.active_identifier_hex(); + for (size_t i = 0; i + 1 < hex.size(); i += 2) + { + unsigned char byte = static_cast( + std::stoul(hex.substr(i, 2), nullptr, 16)); + ref_stream.write(std::span( + reinterpret_cast(&byte), 1)); + } + } + + // share_info fields (same as share_init_verify) + { + ref_stream << share.m_prev_hash; + ref_stream << share.m_coinbase; + ref_stream << share.m_nonce; + + if constexpr (requires { share.m_address; }) + ref_stream << share.m_address; + else if constexpr (requires { share.m_pubkey_type; }) + { + ref_stream << share.m_pubkey_hash; + ref_stream << share.m_pubkey_type; + } + else + ref_stream << share.m_pubkey_hash; + + if constexpr (ver >= 36) + ::Serialize(ref_stream, VarInt(share.m_subsidy)); + else + ref_stream << share.m_subsidy; + + ref_stream << share.m_donation; + { + uint8_t si = static_cast(share.m_stale_info); + ref_stream << si; + } + ::Serialize(ref_stream, VarInt(share.m_desired_version)); + + if constexpr (requires { share.m_segwit_data; }) + { + if constexpr (ver >= dgb::SEGWIT_ACTIVATION_VERSION) + { + // PossiblyNoneType: ALWAYS serialize (p2pool writes default when None). + // Must match share_init_verify — both paths must produce identical ref_hash. + if (share.m_segwit_data.has_value()) { + ref_stream << share.m_segwit_data.value(); + } else { + std::vector empty_branch; + ref_stream << empty_branch; + uint256 zero_root; + ref_stream << zero_root; + } + } + } + + if constexpr (ver >= 36) + { + if constexpr (requires { share.m_merged_addresses; }) + ref_stream << share.m_merged_addresses; + } + + if constexpr (ver < 34) + { + if constexpr (requires { share.m_tx_info; }) + ref_stream << share.m_tx_info; + } + + ref_stream << share.m_far_share_hash; + ref_stream << share.m_max_bits; + ref_stream << share.m_bits; + ref_stream << share.m_timestamp; + ref_stream << share.m_absheight; + + if constexpr (ver >= 36) + { + if constexpr (requires { share.m_abswork; }) + ::Serialize(ref_stream, Using(share.m_abswork)); + } + else + { + ref_stream << share.m_abswork; + } + + if constexpr (ver >= 36) + { + if constexpr (requires { share.m_merged_coinbase_info; }) + ref_stream << share.m_merged_coinbase_info; + if constexpr (requires { share.m_merged_payout_hash; }) + ref_stream << share.m_merged_payout_hash; + } + } + + // V36 ref_type includes message_data (must match verify_share) + if constexpr (ver >= 36) + { + if constexpr (requires { share.m_message_data; }) + ref_stream << share.m_message_data; + } + + auto ref_span = std::span( + reinterpret_cast(ref_stream.data()), ref_stream.size()); + uint256 hash_ref = Hash(ref_span); + uint256 ref_hash = check_merkle_link(hash_ref, share.m_ref_merkle_link); + + // Build OP_RETURN script: 0x6a 0x28 + ref_hash(32) + last_txout_nonce(8) + std::vector op_return_script; + op_return_script.push_back(0x6a); // OP_RETURN + op_return_script.push_back(0x28); // PUSH 40 bytes + op_return_script.insert(op_return_script.end(), ref_hash.data(), ref_hash.data() + 32); + { + uint64_t nonce = share.m_last_txout_nonce; + auto* p = reinterpret_cast(&nonce); + op_return_script.insert(op_return_script.end(), p, p + 8); + } + write_txout(0, op_return_script); + } + + // lock_time = 0 + { + uint32_t locktime = 0; + tx.write(std::span(reinterpret_cast(&locktime), 4)); + } + + // --- 5. Compute txid (double-SHA256 of non-witness serialization) --- + auto tx_span = std::span( + reinterpret_cast(tx.data()), tx.size()); + auto txid = Hash(tx_span); + + // V36 hash_link cross-check: compute prefix hash_link from our coinbase + // and compare with the share's stored hash_link. If states differ, + // the prefix (outputs/amounts) differs from what p2pool built. + if (dump_diag && use_v36_pplns) + { + if constexpr (requires { share.m_hash_link.m_extra_data; }) + { + auto gbr = compute_gentx_before_refhash(ver, params); + // prefix = full coinbase minus last 44 bytes (ref_hash 32 + nonce 8 + locktime 4) + size_t prefix_len = tx.size() - 44; + std::vector prefix( + reinterpret_cast(tx.data()), + reinterpret_cast(tx.data()) + prefix_len); + auto computed_hl = prefix_to_hash_link(prefix, gbr); + + bool state_match = (computed_hl.m_state.m_data == share.m_hash_link.m_state.m_data); + bool extra_match = (computed_hl.m_extra_data.m_data == share.m_hash_link.m_extra_data.m_data); + bool len_match = (computed_hl.m_length == share.m_hash_link.m_length); + + static const char* HXD = "0123456789abcdef"; + auto hex_fn = [&](const auto& v) { + std::string h; for (auto b : v) { h += HXD[(uint8_t)b >> 4]; h += HXD[(uint8_t)b & 0xf]; } return h; + }; + + LOG_WARNING << "[HASHLINK-CMP] state_match=" << (state_match ? "YES" : "NO") + << " extra_match=" << (extra_match ? "YES" : "NO") + << " len_match=" << (len_match ? "YES" : "NO") + << " c2pool_len=" << computed_hl.m_length + << " share_len=" << share.m_hash_link.m_length; + if (!state_match) { + LOG_WARNING << "[HASHLINK-CMP] c2pool_state=" << hex_fn(computed_hl.m_state.m_data); + LOG_WARNING << "[HASHLINK-CMP] share_state =" << hex_fn(share.m_hash_link.m_state.m_data); + } + if (!extra_match) { + LOG_WARNING << "[HASHLINK-CMP] c2pool_extra=" << hex_fn(computed_hl.m_extra_data.m_data) + << " (" << computed_hl.m_extra_data.m_data.size() << " bytes)"; + LOG_WARNING << "[HASHLINK-CMP] share_extra =" << hex_fn(share.m_hash_link.m_extra_data.m_data) + << " (" << share.m_hash_link.m_extra_data.m_data.size() << " bytes)"; + } + // Also dump prefix length and the last 60 bytes for comparison + LOG_WARNING << "[HASHLINK-CMP] prefix_len=" << prefix_len + << " gbr_len=" << gbr.size() + << " prefix_tail=" << hex_fn(std::vector( + prefix.end() - std::min(prefix.size(), size_t(60)), prefix.end())); + } + } + + // One-time full coinbase hex dump for cross-implementation debugging + { + static int coinbase_dump_count = 0; + if (coinbase_dump_count++ < 3) { + const char* HX = "0123456789abcdef"; + auto to_hex_fn = [&](const unsigned char* p, size_t len) { + std::string h; h.reserve(len * 2); + for (size_t i = 0; i < len; ++i) { h += HX[p[i] >> 4]; h += HX[p[i] & 0xf]; } + return h; + }; + auto* cp = reinterpret_cast(tx.data()); + LOG_INFO << "[COINBASE-HEX] len=" << tx.size() + << " txid=" << txid.GetHex() + << " share=" << share.m_hash.GetHex().substr(0, 16) + << " hex=" << to_hex_fn(cp, tx.size()); + } + } + + if (dump_diag) + { + const char* HX = "0123456789abcdef"; + auto to_hex = [&](const unsigned char* p, size_t len) { + std::string h; h.reserve(len * 2); + for (size_t i = 0; i < len; ++i) { h += HX[p[i] >> 4]; h += HX[p[i] & 0xf]; } + return h; + }; + + auto* cp = reinterpret_cast(tx.data()); + LOG_WARNING << "[GENTX-DIAG] coinbase_len=" << tx.size() << " txid=" << txid.GetHex(); + LOG_WARNING << "[GENTX-DIAG] coinbase_hex=" << to_hex(cp, tx.size()); + LOG_WARNING << "[GENTX-DIAG] pplns_outputs=" << payout_outputs.size() + << " donation_amount=" << donation_amount + << " n_outs=" << n_outs + << " has_segwit=" << has_segwit; + LOG_WARNING << "[GENTX-DIAG] total_weight=" << total_weight.GetHex() + << " total_don_weight=" << total_donation_weight.GetHex(); + for (size_t i = 0; i < payout_outputs.size(); ++i) { + auto& [s, a] = payout_outputs[i]; + LOG_WARNING << "[GENTX-DIAG] payout[" << i << "] amount=" << a + << " script=" << to_hex(s.data(), s.size()); + } + // First 5 + last 5 shares in PPLNS walk (for cross-impl comparison) + if (!share.m_prev_hash.IsNull() && tracker.chain.contains(share.m_prev_hash)) { + auto cl = std::min(tracker.chain.get_height(share.m_prev_hash), + static_cast(params.real_chain_length)); + LOG_WARNING << "[GENTX-DIAG] PPLNS walk_count=" << cl + << " total_weight=" << total_weight.GetHex() + << " total_don_weight=" << total_donation_weight.GetHex() + << " n_addrs=" << weights.size(); + // First 5 shares + auto wv = tracker.chain.get_chain(share.m_prev_hash, std::min(cl, int32_t(5))); + int si = 0; + for (auto [h, d] : wv) { + auto hash_copy = h; // Apple Clang: structured bindings can't be captured in lambdas + d.share.invoke([&, hash_copy](auto* obj) { + auto att = chain::target_to_average_attempts(chain::bits_to_target(obj->m_bits)); + auto sc = get_share_script(obj); + LOG_WARNING << "[GENTX-DIAG] walk[" << si << "] hash=" << hash_copy.ToString().substr(0,16) + << " bits=0x" << std::hex << obj->m_bits + << " max_bits=0x" << obj->m_max_bits << std::dec + << " att=" << att.GetHex() + << " don=" << obj->m_donation + << " script=" << to_hex(sc.data(), sc.size()); + }); + ++si; + } + // Last 5 shares in walk (tail of PPLNS window) + if (cl > 10) { + auto full_view = tracker.chain.get_chain(share.m_prev_hash, cl); + int si2 = 0; + for (auto [h, d] : full_view) { + if (si2 >= cl - 5) { + auto hash_copy = h; // Apple Clang: structured bindings can't be captured + d.share.invoke([&, hash_copy](auto* obj) { + auto att = chain::target_to_average_attempts(chain::bits_to_target(obj->m_bits)); + auto sc = get_share_script(obj); + LOG_WARNING << "[GENTX-DIAG] walk_tail[" << si2 << "/" << cl << "] hash=" << hash_copy.ToString().substr(0,16) + << " bits=0x" << std::hex << obj->m_bits + << " max_bits=0x" << obj->m_max_bits << std::dec + << " att=" << att.GetHex() + << " don=" << obj->m_donation + << " script=" << to_hex(sc.data(), sc.size()); + }); + } + ++si2; + } + LOG_WARNING << "[GENTX-DIAG] walk iterated " << si2 << " shares (expected " << cl << ")"; + } + } + } + + { + auto gst_t3 = std::chrono::steady_clock::now(); + static int64_t sp = 0, sa = 0, sc = 0, sn = 0; + sp += std::chrono::duration_cast(gst_t1 - gst_t0).count(); + sa += std::chrono::duration_cast(gst_t2 - gst_t1).count(); + sc += std::chrono::duration_cast(gst_t3 - gst_t2).count(); + ++sn; + if (sn % 50 == 0) + LOG_INFO << "[GST-SPLIT] pplns=" << (sp/sn) << "us amounts=" << (sa/sn) + << "us coinbase=" << (sc/sn) << "us count=" << sn; + } + return txid; +} + +// ============================================================================ +// verify_merged_coinbase_commitment() +// +// Full 7-step chain verification of merged coinbase (Python data.py:329-458). +// Ensures the merged coinbase committed in the share actually matches the +// canonical PPLNS construction. Without this, a node could commit valid +// m_merged_payout_hash (weights match) but build a DOGE coinbase that pays +// differently — the merkle proof would be for the real (malicious) coinbase. +// +// Verification chain: +// 1. Re-derive canonical DOGE coinbase from PPLNS weights +// 2. canonical_txid = hash256(canonical_coinbase) +// 3. check_merkle_link(canonical_txid, coinbase_merkle_link) == header.merkle_root +// 4. hash256(header) == doge_block_hash +// 5. doge_block_hash matches aux_merkle_root in LTC coinbase mm_data +// +// Returns empty string on success, error message on failure. +// ============================================================================ +template +std::string verify_merged_coinbase_commitment( + const ShareT& share, TrackerT& tracker, const core::CoinParams& params) +{ + if constexpr (ShareT::version < 36) + return {}; + if constexpr (!requires { share.m_merged_coinbase_info; }) + return {}; + + // No merged coinbase info → nothing to verify + if (share.m_merged_coinbase_info.empty()) + return {}; + + // Need enough chain history for reliable PPLNS verification + if (share.m_prev_hash.IsNull() || !tracker.chain.contains(share.m_prev_hash)) + return {}; + auto height = tracker.chain.get_height(share.m_prev_hash); + if (height < static_cast(params.real_chain_length)) + return {}; // Insufficient depth — skip (match Python behavior) + + auto block_target = chain::bits_to_target(share.m_bits); + auto max_weight = chain::target_to_average_attempts(block_target) + * 65535 * params.spread; + int32_t chain_len = std::min(height, + static_cast(params.real_chain_length)); + + // Parse mm_data from LTC coinbase scriptSig + const auto& coinbase = share.m_coinbase.m_data; + static const uint8_t MM_MAGIC[] = {0xfa, 0xbe, 0x6d, 0x6d}; + auto mm_pos = std::search(coinbase.begin(), coinbase.end(), + std::begin(MM_MAGIC), std::end(MM_MAGIC)); + if (mm_pos == coinbase.end()) { + return "merged_coinbase_info present but no mm_data marker in coinbase scriptSig"; + } + size_t mm_offset = std::distance(coinbase.begin(), mm_pos) + 4; + if (coinbase.size() - mm_offset < 40) + return "mm_data too short in coinbase scriptSig"; + + // aux_merkle_root: 32 bytes big-endian + uint256 aux_merkle_root; + { + const uint8_t* p = coinbase.data() + mm_offset; + // MM root is stored big-endian in the coinbase — reverse for internal uint256 + uint8_t* dst = reinterpret_cast(aux_merkle_root.begin()); + for (int i = 31; i >= 0; --i) + dst[i] = *p++; + } + uint32_t aux_size = 0; + { + const uint8_t* p = coinbase.data() + mm_offset + 32; + aux_size = p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24); + } + + // Get finder script for canonical coinbase construction + auto finder_script = get_share_script(&share); + auto finder_merged = normalize_script_for_merged(finder_script); + + for (const auto& info : share.m_merged_coinbase_info) { + uint32_t chain_id = info.m_chain_id; + + // Step 1: Get PPLNS weights for this merged chain + auto mw = tracker.get_merged_cumulative_weights( + share.m_prev_hash, chain_len, max_weight, chain_id); + + if (mw.weights.empty() || mw.total_weight.IsNull()) + continue; // No V36 shares → can't verify + + // Build payout list (same logic as payout_provider) + auto donation_script = params.donation_script_func(36); + uint64_t coinbase_value = info.m_coinbase_value; + + // Convert weights to integer payouts + std::map, uint64_t> output_amounts; + uint64_t total_distributed = 0; + double total_d = mw.total_weight.IsNull() ? 0.0 + : static_cast(mw.total_weight.GetLow64()); + for (auto& [key, weight] : mw.weights) { + auto script = resolve_merged_payout_script(key); + if (script.empty()) continue; + double frac = weight.IsNull() ? 0.0 + : static_cast(weight.GetLow64()) / total_d; + uint64_t amount = static_cast(coinbase_value * frac); + if (amount > 0) { + output_amounts[script] += amount; + total_distributed += amount; + } + } + uint64_t donation_amount = coinbase_value - total_distributed; + // Donation >= 1 satoshi + if (donation_amount < 1 && !output_amounts.empty()) { + // Deterministic tiebreak: (amount, script) — largest script wins when equal + auto it = std::max_element(output_amounts.begin(), output_amounts.end(), + [](auto& a, auto& b) { + if (a.second != b.second) return a.second < b.second; + return a.first < b.first; + }); + it->second -= 1; + donation_amount += 1; + } + + // Coinbase reconstruction removed: the merged_payout_hash check + // (Step 1 above) already verifies PPLNS weight correctness via the + // skip list. Reconstructing the full coinbase TX to verify merkle_root + // is redundant and fails on cross-implementation shares because: + // - scriptSig differs ("/c2pool/" vs "/P2Pool/") + // - OP_RETURN text differs + // - THE state_root presence differs + // - Float vs integer rounding in amount calculation + // All of these change txid → merkle_root without affecting PPLNS fairness. + + // Step 2: Verify header structure and extract block hash + if (info.m_block_header.m_data.size() < 80) + return "merged block header too short"; + + // Step 3: hash256(header) == doge_block_hash + auto hdr_span = std::span( + info.m_block_header.m_data.data(), 80); + uint256 doge_block_hash = Hash(hdr_span); + + // Step 6: Verify doge_block_hash against aux_merkle_root + if (aux_size == 1) { + // Single merged chain: aux_merkle_root == block_hash + if (doge_block_hash != aux_merkle_root) { + return "merged block hash " + doge_block_hash.GetHex() + + " != aux_merkle_root " + aux_merkle_root.GetHex() + + " for chain " + std::to_string(chain_id); + } + } + // Multi-chain: would need aux tree reconstruction (future) + } + + return {}; +} + +// ============================================================================ +// share_check() +// +// The check()-phase verification after init: +// 1. Timestamp not too far in the future +// 2. Version counting (stub — version upgrade enforcement) +// 3. Transaction hash resolution (for pre-v34 shares) +// 4. GenerateShareTransaction reconstruction & comparison +// 5. Merged payout hash + coinbase commitment verification +// +// Returns true if the share passes all checks. +// Throws on validation failure. +// ============================================================================ +template +bool share_check(const ShareT& share, + const uint256& share_hash, + const uint256& gentx_hash, + TrackerT& tracker, + const core::CoinParams& params) +{ + // 1. Timestamp check — must not be more than 600s in the future + auto now = static_cast( + std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count()); + if (share.m_timestamp > now + 600) + throw std::invalid_argument("share timestamp is too far in the future"); + + // 2. Version switch enforcement — canonical 60% weighted switch rule. + // p2pool data.py check() (lines 1396-1414): a version BOUNDARY + // (share.version != parent.version) is only valid when, in the sampling + // window [CHAIN_LENGTH*9/10, CHAIN_LENGTH] behind the PARENT, the new + // version holds >= 60% of the PPLNS-WEIGHTED desired-version counts. + // The weight is target_to_average_attempts per share — get_desired_version_weights, + // matching canonical get_desired_version_counts (data.py:2651) — NOT a flat count. + // F10/(b): the prior non-canonical 95%-obsolescence punish is removed; the + // canonical 60% switch rule is now the ONLY version gate. AutoRatchet stays + // count-based and does not gate peer shares here. + { + auto chain_length = static_cast(params.chain_length); + if (!share.m_prev_hash.IsNull() && tracker.chain.contains(share.m_prev_hash)) + { + // Parent's share version (the type the incoming share must legally follow) + int64_t parent_version = 0; + tracker.chain.get_share(share.m_prev_hash).invoke([&](auto* obj) { + parent_version = std::remove_pointer_t::version; + }); + int64_t share_ver = share.version; + + auto prev_height = tracker.chain.get_height(share.m_prev_hash); + if (prev_height >= chain_length) + { + if (share_ver == parent_version) + { + // same version — always valid (correct when created) + } + else if (share_ver == parent_version + 1) + { + // Upgrade by one version: requires >= 60% weighted support in + // window [CHAIN_LENGTH*9/10, CHAIN_LENGTH] behind the parent. + uint32_t window_start = (static_cast(chain_length) * 9) / 10; + uint32_t window_size = static_cast(chain_length) / 10; + auto ancestor = tracker.chain.get_nth_parent_key(share.m_prev_hash, window_start); + auto weights = tracker.get_desired_version_weights( + ancestor, static_cast(window_size)); + + uint288 new_ver_weight; // weight of shares desiring exactly share_ver + uint288 total_weight; + for (auto& [ver, w] : weights) + { + total_weight = total_weight + w; + if (static_cast(ver) == share_ver) + new_ver_weight = new_ver_weight + w; + } + // Canonical: counts.get(self.VERSION,0) < sum(counts)*60//100 + if (new_ver_weight * uint32_t(100) < total_weight * uint32_t(60)) + throw std::invalid_argument("switch without enough hash power upgraded"); + } + else if (parent_version == share_ver + 1) + { + // Downgrade by one (AutoRatchet deactivation: V35 may follow V36) + } + else + { + throw std::invalid_argument("invalid version jump from " + + std::to_string(parent_version) + " to " + std::to_string(share_ver)); + } + } + else if (share_ver == parent_version + 1) + { + // Not enough history for an upgrade boundary + throw std::invalid_argument("switch without enough history"); + } + } + } + + // 3. GenerateShareTransaction reconstruction & comparison + // Rebuild the expected coinbase from PPLNS weights and share fields, + // then verify its txid matches the gentx_hash from share_init_verify(). + // p2pool check(): v36_active = (self.VERSION >= 36) + // Use the SHARE's own version, not the tracker's runtime AutoRatchet state. + // This ensures V35 shares always verify with V35 PPLNS formula, even after + // the AutoRatchet transitions to ACTIVATED. + constexpr int64_t share_ver = ShareT::version; + bool v36_active = (share_ver >= 36); + if (!share.m_prev_hash.IsNull() && tracker.chain.contains(share.m_prev_hash)) + { + uint256 expected_gentx = generate_share_transaction(share, tracker, params, false, v36_active); + if (expected_gentx != gentx_hash) + { + LOG_WARNING << "GENTX-MISMATCH detail:" + << " share=" << share_hash.ToString().substr(0,16) + << " ver=" << share.version + << " subsidy=" << share.m_subsidy + << " donation=" << share.m_donation + << " prev=" << share.m_prev_hash.ToString().substr(0,16); + LOG_WARNING << " expected_gentx=" << expected_gentx.ToString().substr(0,32) + << " actual_gentx=" << gentx_hash.ToString().substr(0,32); + + auto chain_len = std::min( + tracker.chain.get_height(share.m_prev_hash), + static_cast(params.real_chain_length)); + // V35: log grandparent + height-1 window info + uint256 gp_hash; + if (tracker.chain.contains(share.m_prev_hash)) + tracker.chain.get(share.m_prev_hash).share.invoke([&](auto* s){ gp_hash = s->m_prev_hash; }); + int32_t v35_walk = std::max(0, chain_len - 1); + LOG_WARNING << " PPLNS chain_len=" << chain_len + << " v35_walk=" << v35_walk + << " grandparent=" << (gp_hash.IsNull() ? "null" : gp_hash.GetHex().substr(0,16)) + << " prev_height=" << tracker.chain.get_height(share.m_prev_hash) + << " real_chain_length=" << params.real_chain_length; + + // Compare share target: what c2pool computes vs what the share has + { + static int target_diag = 0; + if (target_diag++ < 10) { + auto [cst_max_bits, cst_bits] = tracker.compute_share_target( + share.m_prev_hash, share.m_timestamp, uint256()); + auto share_aps = tracker.get_pool_attempts_per_second( + share.m_prev_hash, params.target_lookbehind, true); + LOG_WARNING << "[GENTX-TARGET] share_bits=0x" << std::hex << share.m_bits + << " share_max_bits=0x" << share.m_max_bits + << " c2pool_bits=0x" << cst_bits + << " c2pool_max_bits=0x" << cst_max_bits << std::dec + << " aps_min=" << share_aps.GetLow64() + << " prev=" << share.m_prev_hash.GetHex().substr(0,16); + + // APS walk component dump for cross-impl comparison + int32_t dist = params.target_lookbehind; + auto near_hash = share.m_prev_hash; + auto far_hash = near_hash; + int32_t actual_dist = 0; + for (int32_t i = 0; i < dist - 1; ++i) { + if (far_hash.IsNull() || !tracker.chain.contains(far_hash)) break; + auto* idx = tracker.chain.get_index(far_hash); + far_hash = idx ? idx->tail : uint256(); + ++actual_dist; + } + uint32_t near_ts = 0, far_ts = 0; + if (tracker.chain.contains(near_hash)) + tracker.chain.get_share(near_hash).invoke([&](auto* s){ near_ts = s->m_timestamp; }); + if (!far_hash.IsNull() && tracker.chain.contains(far_hash)) + tracker.chain.get_share(far_hash).invoke([&](auto* s){ far_ts = s->m_timestamp; }); + + // Compare skip-list far vs naive-walk far + auto skip_far = tracker.chain.get_nth_parent_via_skip( + share.m_prev_hash, dist - 1); + bool skip_match = (!far_hash.IsNull() && skip_far == far_hash); + + // Also get delta via TrackerView for comparison + uint288 delta_min_work; + int32_t delta_height = 0; + if (!skip_far.IsNull() && tracker.chain.contains(skip_far)) { + auto dv = tracker.chain.get_delta(share.m_prev_hash, skip_far); + delta_min_work = dv.min_work; + delta_height = dv.height; + } + + LOG_WARNING << "[GENTX-APS] near=" << near_hash.GetHex().substr(0,16) + << " far_naive=" << (far_hash.IsNull() ? "null" : far_hash.GetHex().substr(0,16)) + << " far_skip=" << (skip_far.IsNull() ? "null" : skip_far.GetHex().substr(0,16)) + << " skip_match=" << (skip_match ? "YES" : "NO") + << " actual_dist=" << actual_dist + << " expected_dist=" << (dist - 1) + << " timespan=" << (int32_t(near_ts) - int32_t(far_ts)) + << " near_ts=" << near_ts << " far_ts=" << far_ts + << " chain_height=" << tracker.chain.get_height(share.m_prev_hash) + << " delta_h=" << delta_height + << " delta_min_work=" << delta_min_work.GetLow64(); + + // Previous share's max_target (clamp reference) + uint256 prev_max_target; + tracker.chain.get_share(share.m_prev_hash).invoke([&](auto* obj) { + prev_max_target = chain::bits_to_target(obj->m_max_bits); + }); + LOG_WARNING << "[GENTX-CLAMP] prev_max_bits=0x" << std::hex + << chain::target_to_bits_upper_bound(prev_max_target) << std::dec + << " clamp_lo=" << chain::target_to_bits_upper_bound(prev_max_target * 9 / 10) + << " clamp_hi=" << chain::target_to_bits_upper_bound(prev_max_target * 11 / 10); + } + } + // --- Detailed diagnostics: re-run with full dump --- + static int s_diag_count = 0; + if (s_diag_count++ < 5) + { + LOG_WARNING << "[GENTX-DIAG] Re-running generate_share_transaction with full dump (v36_active=" << v36_active << "):"; + generate_share_transaction(share, tracker, params, true, v36_active); + + // Per-share PPLNS walk dump — compare with p2pool's [PARENT-PPLNS] output. + // Uses same parameters as generate_share_transaction's V36 path. + if (v36_active && !share.m_prev_hash.IsNull()) { + auto diag_chain_len = static_cast(params.real_chain_length); + LOG_WARNING << "[GENTX-DIAG] Per-share V36 PPLNS walk from prev=" + << share.m_prev_hash.GetHex().substr(0, 16) << ":"; + tracker.dump_v36_pplns_walk(share.m_prev_hash, diag_chain_len); + } + } + + throw std::invalid_argument("GenerateShareTransaction mismatch — coinbase does not match PPLNS payouts"); + } + } + + // 4. V36+ merged_payout_hash verification + // Verify that the share's committed merged PPLNS hash matches what we + // independently compute from the share chain. Without this, a malicious + // node could steal all merged chain (DOGE) rewards while appearing honest + // on the parent chain (LTC payouts are consensus-enforced via gentx above). + if constexpr (ShareT::version >= 36) + { + if constexpr (requires { share.m_merged_payout_hash; }) + { + if (!share.m_merged_payout_hash.IsNull() && + !share.m_prev_hash.IsNull() && + tracker.chain.contains(share.m_prev_hash)) + { + // Use BLOCK target (from header bits), not share target. + // p2pool: block_target = self.header['bits'].target + auto block_target = chain::bits_to_target(share.m_min_header.m_bits); + auto expected_hash = tracker.compute_merged_payout_hash( + share.m_prev_hash, block_target); + + if (!expected_hash.IsNull() && share.m_merged_payout_hash != expected_hash) + { + LOG_WARNING << "merged_payout_hash REJECT: claimed " + << share.m_merged_payout_hash.GetHex() + << " != expected " << expected_hash.GetHex(); + throw std::invalid_argument( + "merged_payout_hash mismatch — merged chain reward theft attempt"); + } + } + } + } + + // 5. V36+ merged coinbase commitment verification (7-step chain) + // Verifies the actual merged coinbase matches canonical PPLNS construction. + if constexpr (ShareT::version >= 36) + { + auto mcv_err = verify_merged_coinbase_commitment(share, tracker, params); + if (!mcv_err.empty()) + throw std::invalid_argument("merged coinbase verification: " + mcv_err); + } + + return true; +} + +// ============================================================================ +// verify_share() +// +// Combined entry point: runs both init-phase and check-phase verification. +// Returns the computed share hash. +// ============================================================================ +template +uint256 verify_share(const ShareT& share, TrackerT& tracker, const core::CoinParams& params) +{ + auto vt0 = std::chrono::steady_clock::now(); + // share_init_verify computes gentx_hash along the way — we need it + // for the GenerateShareTransaction comparison in share_check. + // Skip scrypt PoW re-check when hash was already computed in Phase 1 + // (processing_shares offloads scrypt to m_verify_pool; no need to repeat). + uint256 hash = share_init_verify(share, params, share.m_hash.IsNull()); + auto vt1 = std::chrono::steady_clock::now(); + + // Verify recomputed hash matches stored hash (informational). + // For locally created shares the hash was set during create_local_share; + // a mismatch means the header reconstruction diverged (e.g., genesis PPLNS + // race). The share_check phase uses share.m_hash for chain lookups. + if (!share.m_hash.IsNull() && hash != share.m_hash) { + static int hash_mismatch_log = 0; + if (hash_mismatch_log++ < 10) + LOG_WARNING << "[verify_share] hash mismatch: recomputed=" + << hash.GetHex().substr(0, 16) + << " stored=" << share.m_hash.GetHex().substr(0, 16); + } + + // Re-derive gentx_hash for the check phase + constexpr int64_t ver = ShareT::version; + auto gentx_before_refhash = compute_gentx_before_refhash(ver, params); + + // Rebuild ref_hash + hash_link_data the same way init does + PackStream ref_stream; + { + auto hex = params.active_identifier_hex(); + for (size_t i = 0; i + 1 < hex.size(); i += 2) + { + unsigned char byte = static_cast( + std::stoul(hex.substr(i, 2), nullptr, 16)); + ref_stream.write(std::span( + reinterpret_cast(&byte), 1)); + } + } + { + ref_stream << share.m_prev_hash; + ref_stream << share.m_coinbase; + ref_stream << share.m_nonce; + + if constexpr (requires { share.m_address; }) + ref_stream << share.m_address; + else if constexpr (requires { share.m_pubkey_type; }) + { + ref_stream << share.m_pubkey_hash; + ref_stream << share.m_pubkey_type; + } + else + ref_stream << share.m_pubkey_hash; + + if constexpr (ver >= 36) + ::Serialize(ref_stream, VarInt(share.m_subsidy)); + else + ref_stream << share.m_subsidy; + + ref_stream << share.m_donation; + { + uint8_t si = static_cast(share.m_stale_info); + ref_stream << si; + } + ::Serialize(ref_stream, VarInt(share.m_desired_version)); + + if constexpr (requires { share.m_segwit_data; }) + { + if constexpr (ver >= dgb::SEGWIT_ACTIVATION_VERSION) + { + // PossiblyNoneType: ALWAYS serialize (p2pool writes default when None) + if (share.m_segwit_data.has_value()) { + ref_stream << share.m_segwit_data.value(); + } else { + std::vector empty_branch; + ref_stream << empty_branch; + uint256 zero_root; + ref_stream << zero_root; + } + } + } + + if constexpr (ver >= 36) + { + if constexpr (requires { share.m_merged_addresses; }) + ref_stream << share.m_merged_addresses; + } + + if constexpr (ver < 34) + { + if constexpr (requires { share.m_tx_info; }) + ref_stream << share.m_tx_info; + } + + ref_stream << share.m_far_share_hash; + ref_stream << share.m_max_bits; + ref_stream << share.m_bits; + ref_stream << share.m_timestamp; + ref_stream << share.m_absheight; + + if constexpr (ver >= 36) + { + if constexpr (requires { share.m_abswork; }) + ::Serialize(ref_stream, Using(share.m_abswork)); + } + else + { + ref_stream << share.m_abswork; + } + + if constexpr (ver >= 36) + { + if constexpr (requires { share.m_merged_coinbase_info; }) + ref_stream << share.m_merged_coinbase_info; + if constexpr (requires { share.m_merged_payout_hash; }) + ref_stream << share.m_merged_payout_hash; + } + } + + // V36 ref_type includes message_data + if constexpr (ver >= 36) + { + if constexpr (requires { share.m_message_data; }) + ref_stream << share.m_message_data; + } + + auto ref_span = std::span( + reinterpret_cast(ref_stream.data()), ref_stream.size()); + uint256 hash_ref = Hash(ref_span); + + std::vector hash_link_data; + { + uint256 ref_hash = check_merkle_link(hash_ref, share.m_ref_merkle_link); + hash_link_data.insert(hash_link_data.end(), ref_hash.data(), ref_hash.data() + 32); + uint64_t nonce = share.m_last_txout_nonce; + auto* p = reinterpret_cast(&nonce); + hash_link_data.insert(hash_link_data.end(), p, p + 8); + uint32_t zero = 0; + auto* z = reinterpret_cast(&zero); + hash_link_data.insert(hash_link_data.end(), z, z + 4); + } + + uint256 gentx_hash = check_hash_link(share.m_hash_link, hash_link_data, gentx_before_refhash); + + // V36+: Validate message_data (reject shares with invalid encrypted messages) + if constexpr (ver >= 36) + { + if constexpr (requires { share.m_message_data; }) + { + auto err = validate_message_data(share.m_message_data.m_data); + if (!err.empty()) + throw std::invalid_argument("share " + err); + } + } + + share_check(share, hash, gentx_hash, tracker, params); + { + auto vt2 = std::chrono::steady_clock::now(); + auto init_us = std::chrono::duration_cast(vt1 - vt0).count(); + auto check_us = std::chrono::duration_cast(vt2 - vt1).count(); + static int64_t s_init = 0, s_check = 0, s_cnt = 0; + s_init += init_us; s_check += check_us; ++s_cnt; + if (s_cnt % 50 == 0) + LOG_INFO << "[VERIFY-SPLIT] init_avg=" << (s_init/s_cnt) + << "us check_avg=" << (s_check/s_cnt) << "us count=" << s_cnt; + } + return hash; +} + +// ============================================================================ +// create_local_share_v35() +// +// Constructs a PaddingBugfixShare (V35) from locally-generated block data. +// This is the V35 counterpart of create_local_share (V36). +// Key differences from V36: +// - Uses m_address (string) instead of m_pubkey_hash + m_pubkey_type +// - No merged_addresses, merged_coinbase_info, merged_payout_hash +// - Fixed uint64 subsidy (not VarInt) +// - Fixed uint128 abswork (not VarInt) +// - HashLinkType (no extra_data) instead of V36HashLinkType +// - DONATION_SCRIPT (P2PK 67b) instead of COMBINED_DONATION_SCRIPT (P2SH 23b) +// ============================================================================ +template +uint256 create_local_share_v35( + TrackerT& tracker, + const core::CoinParams& params, + const coin::SmallBlockHeaderType& min_header, + const BaseScript& coinbase, + uint64_t subsidy, + const uint256& prev_share, + const std::vector& merkle_branches, + const std::vector& payout_script, + uint16_t donation = 50, + StaleInfo stale_info = StaleInfo::none, + bool segwit_active = false, + const std::string& witness_commitment_hex = {}, + const std::vector& actual_coinbase_bytes = {}, + const uint256& witness_root = uint256(), + uint32_t override_max_bits = 0, + uint32_t override_bits = 0, + uint32_t frozen_absheight = 0, + uint128 frozen_abswork = uint128(), + uint256 frozen_far_share_hash = uint256(), + uint32_t frozen_timestamp = 0, + bool has_frozen = false, + const std::vector& frozen_merkle_branches = {}, + const uint256& frozen_witness_root = uint256(), + uint64_t desired_version = 36) +{ + PaddingBugfixShare share; + share.m_min_header = min_header; + share.m_coinbase = coinbase; + share.m_subsidy = subsidy; + share.m_prev_hash = prev_share; + share.m_donation = donation; + share.m_stale_info = stale_info; + share.m_desired_version = desired_version; + + // Timestamp: clip to at least previous_share.timestamp + 1 + share.m_timestamp = min_header.m_timestamp; + if (!prev_share.IsNull() && tracker.chain.contains(prev_share)) { + uint32_t prev_ts = 0; + tracker.chain.get(prev_share).share.invoke([&](auto* prev) { + prev_ts = prev->m_timestamp; + }); + if (share.m_timestamp <= prev_ts) + share.m_timestamp = prev_ts + 1; + } + + // Compute share target + auto desired_target = chain::bits_to_target(min_header.m_bits); + auto [share_max_bits, share_bits] = tracker.compute_share_target( + prev_share, share.m_timestamp, desired_target); + share.m_max_bits = share_max_bits; + share.m_bits = share_bits; + share.m_nonce = 0; + + // V35: address as string (VarStr), convert from payout_script + { + uint160 pubkey_hash; + uint8_t pubkey_type = 0; + if (payout_script.size() == 25 && payout_script[0] == 0x76) { + std::memcpy(pubkey_hash.data(), payout_script.data() + 3, 20); + pubkey_type = 0; + } else if (payout_script.size() == 23 && payout_script[0] == 0xa9) { + std::memcpy(pubkey_hash.data(), payout_script.data() + 2, 20); + pubkey_type = 2; + } else if (payout_script.size() == 22 && payout_script[0] == 0x00) { + std::memcpy(pubkey_hash.data(), payout_script.data() + 2, 20); + pubkey_type = 1; + } else if (payout_script.size() >= 20) { + std::memcpy(pubkey_hash.data(), payout_script.data(), 20); + } + std::string addr_str = pubkey_hash_to_address(pubkey_hash, pubkey_type, params); + share.m_address.m_data.assign(addr_str.begin(), addr_str.end()); + { + auto roundtrip = core::address_to_script(addr_str); + static const char* H = "0123456789abcdef"; + std::string ps_hex, rt_hex; + for (auto b : payout_script) { ps_hex += H[b>>4]; ps_hex += H[b&0xf]; } + for (auto b : roundtrip) { rt_hex += H[b>>4]; rt_hex += H[b&0xf]; } + LOG_INFO << "[V35-ADDR] type=" << (int)pubkey_type + << " addr=" << addr_str + << " payout_script=" << ps_hex + << " roundtrip=" << rt_hex + << " match=" << (payout_script == roundtrip ? "YES" : "NO"); + } + } + + // Chain position: absheight and abswork + if (!prev_share.IsNull() && tracker.chain.contains(prev_share)) { + tracker.chain.get(prev_share).share.invoke([&](auto* prev) { + share.m_absheight = prev->m_absheight + 1; + }); + { + auto current_attempts = chain::target_to_average_attempts( + chain::bits_to_target(share.m_bits)); + uint128 prev_abswork; + tracker.chain.get(prev_share).share.invoke([&](auto* prev) { + prev_abswork = prev->m_abswork; + }); + share.m_abswork = prev_abswork + uint128(current_attempts.GetLow64()); + } + // far_share_hash: 99th ancestor + { + auto [prev_height, last] = tracker.chain.get_height_and_last(prev_share); + if (last.IsNull() && prev_height < 99) { + share.m_far_share_hash = uint256(); + } else { + try { + share.m_far_share_hash = tracker.chain.get_nth_parent_key(prev_share, 99); + } catch (const std::exception&) { + share.m_far_share_hash = uint256(); + } + } + } + } else { + share.m_absheight = 1; + share.m_abswork = uint128(chain::target_to_average_attempts( + chain::bits_to_target(share.m_bits)).GetLow64()); + share.m_far_share_hash = uint256(); + } + + // Apply frozen fields from template time + if (has_frozen) { + share.m_absheight = frozen_absheight; + share.m_abswork = frozen_abswork; + share.m_far_share_hash = frozen_far_share_hash; + share.m_timestamp = frozen_timestamp; + if (override_max_bits) share.m_max_bits = override_max_bits; + if (override_bits) share.m_bits = override_bits; + } + + // Random last_txout_nonce + share.m_last_txout_nonce = static_cast(std::time(nullptr)) ^ + (static_cast(min_header.m_nonce) << 32); + + // ref_merkle_link: empty + share.m_ref_merkle_link.m_branch.clear(); + share.m_ref_merkle_link.m_index = 0; + // merkle_link: from Stratum + share.m_merkle_link.m_branch = merkle_branches; + share.m_merkle_link.m_index = 0; + + // Segwit data + if (segwit_active && !witness_commitment_hex.empty()) + { + SegwitData sd; + sd.m_txid_merkle_link.m_branch = (has_frozen && !frozen_merkle_branches.empty()) + ? frozen_merkle_branches : merkle_branches; + sd.m_txid_merkle_link.m_index = 0; + // Priority: frozen > direct. The zero root (0x00..00) is VALID for + // coinbase-only blocks — p2pool uses it to compute the OP_RETURN + // witness commitment. Never treat IsNull() as "not set" here. + sd.m_wtxid_merkle_root = !frozen_witness_root.IsNull() + ? frozen_witness_root : witness_root; + share.m_segwit_data = sd; + } + + // --- Compute ref_hash (V35 format) --- + PackStream ref_stream; + { + auto hex = params.active_identifier_hex(); + for (size_t i = 0; i + 1 < hex.size(); i += 2) { + unsigned char byte = static_cast( + std::stoul(hex.substr(i, 2), nullptr, 16)); + ref_stream.write(std::span( + reinterpret_cast(&byte), 1)); + } + } + ref_stream << share.m_prev_hash; + ref_stream << share.m_coinbase; + ref_stream << share.m_nonce; + // V35: address as VarStr + ref_stream << share.m_address; + // V35: subsidy as fixed uint64 + ref_stream << share.m_subsidy; + ref_stream << share.m_donation; + { uint8_t si = static_cast(share.m_stale_info); ref_stream << si; } + ::Serialize(ref_stream, VarInt(share.m_desired_version)); + // segwit_data + if (share.m_segwit_data.has_value()) { + ref_stream << share.m_segwit_data.value(); + } else { + std::vector empty_branch; + ref_stream << empty_branch; + uint256 zero_root; + ref_stream << zero_root; + } + // V35: NO merged_addresses + ref_stream << share.m_far_share_hash; + ref_stream << share.m_max_bits; + ref_stream << share.m_bits; + ref_stream << share.m_timestamp; + ref_stream << share.m_absheight; + // V35: abswork as fixed uint128 + ref_stream << share.m_abswork; + // V35: NO merged_coinbase_info, NO merged_payout_hash, NO message_data + + auto ref_span_v = std::span( + reinterpret_cast(ref_stream.data()), ref_stream.size()); + uint256 hash_ref = Hash(ref_span_v); + uint256 ref_hash = check_merkle_link(hash_ref, share.m_ref_merkle_link); + + // Use ref_hash from actual coinbase if available + if (!actual_coinbase_bytes.empty() && actual_coinbase_bytes.size() > 44) { + uint256 coinbase_ref_hash; + std::memcpy(coinbase_ref_hash.data(), + actual_coinbase_bytes.data() + actual_coinbase_bytes.size() - 44, 32); + ref_hash = coinbase_ref_hash; + } + + // --- Derive hash_link (V35: HashLinkType, no extra_data) --- + auto gentx_before_refhash = compute_gentx_before_refhash(int64_t(35), params); + + std::vector coinbase_bytes_for_hashlink; + if (!actual_coinbase_bytes.empty()) { + coinbase_bytes_for_hashlink = actual_coinbase_bytes; + } else { + // Fallback: reconstruct coinbase from V35 PPLNS walk + // V35: flat weights, grandparent start, height-1 window, 199/200 formula, finder fee + // Reference: p2pool data.py lines 878-965 + std::map, uint288> weights; + uint288 total_weight; + + if (!prev_share.IsNull() && tracker.chain.contains(prev_share)) + { + // V35: start from grandparent (prev_share.prev_hash) + uint256 pplns_start; + tracker.chain.get(prev_share).share.invoke([&](auto* s) { + pplns_start = s->m_prev_hash; + }); + + if (!pplns_start.IsNull()) { + auto height = tracker.chain.get_height(prev_share); + int32_t max_shares = std::max(0, std::min(height, + static_cast(params.real_chain_length)) - 1); + auto block_target = chain::bits_to_target(share.m_min_header.m_bits); + auto desired_weight = chain::target_to_average_attempts(block_target) + * uint288(params.spread) * uint288(65535); + // Flat weight accumulation (not decayed) + auto result = tracker.get_cumulative_weights(pplns_start, max_shares, desired_weight); + weights = std::move(result.weights); + total_weight = result.total_weight; + } + } + + std::map, uint64_t> amounts; + if (!total_weight.IsNull()) { + for (auto& [script, weight] : weights) { + // V35: 99.5% to PPLNS — subsidy * 199 * weight / (200 * total_weight) + uint64_t amount = (uint288(subsidy) * uint288(199) * weight + / (uint288(200) * total_weight)).GetLow64(); + if (amount > 0) amounts[script] = amount; + } + } + // V35: add 0.5% finder fee to the share creator's payout script + amounts[payout_script] = (amounts.count(payout_script) ? amounts[payout_script] : 0) + + subsidy / 200; + uint64_t sum_amounts = 0; + for (auto& [s, a] : amounts) sum_amounts += a; + // V35: no minimum donation enforcement (unlike v36) + uint64_t donation_amount = (subsidy > sum_amounts) ? (subsidy - sum_amounts) : 0; + + std::vector, uint64_t>> payout_outputs( + amounts.begin(), amounts.end()); + std::sort(payout_outputs.begin(), payout_outputs.end(), + [](const auto& a, const auto& b) { + if (a.second != b.second) return a.second < b.second; + return a.first < b.first; + }); + if (payout_outputs.size() > 4000) + payout_outputs.erase(payout_outputs.begin(), payout_outputs.end() - 4000); + + PackStream gentx; + { uint32_t v = 1; gentx.write(std::span(reinterpret_cast(&v), 4)); } + { unsigned char one = 1; gentx.write(std::span(reinterpret_cast(&one), 1)); } + { uint256 z; gentx << z; } + { uint32_t idx = 0xffffffff; gentx.write(std::span(reinterpret_cast(&idx), 4)); } + gentx << share.m_coinbase; + { uint32_t seq = 0xffffffff; gentx.write(std::span(reinterpret_cast(&seq), 4)); } + size_t n_outs = payout_outputs.size() + 1 + 1; + bool has_segwit_fb = share.m_segwit_data.has_value(); + if (has_segwit_fb) n_outs += 1; + if (n_outs < 253) { uint8_t cnt = (uint8_t)n_outs; gentx.write(std::span(reinterpret_cast(&cnt), 1)); } + else { uint8_t m = 0xfd; gentx.write(std::span(reinterpret_cast(&m), 1)); uint16_t cnt = (uint16_t)n_outs; gentx.write(std::span(reinterpret_cast(&cnt), 2)); } + auto write_txout = [&](uint64_t value, const std::vector& script) { + gentx.write(std::span(reinterpret_cast(&value), 8)); + BaseScript bs; bs.m_data = script; gentx << bs; + }; + if (has_segwit_fb) { + auto& sd = share.m_segwit_data.value(); + std::vector wscript = {0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed}; + uint256 commitment = compute_p2pool_witness_commitment(sd.m_wtxid_merkle_root); + auto cb = commitment.GetChars(); + wscript.insert(wscript.end(), cb.begin(), cb.end()); + write_txout(0, wscript); + } + for (auto& [script, amount] : payout_outputs) write_txout(amount, script); + // V35: use pre-V36 DONATION_SCRIPT + write_txout(donation_amount, params.donation_script_func(int64_t(35))); + { std::vector op; op.push_back(0x6a); op.push_back(0x28); + op.insert(op.end(), ref_hash.data(), ref_hash.data() + 32); + uint64_t n = share.m_last_txout_nonce; auto* p = reinterpret_cast(&n); + op.insert(op.end(), p, p + 8); write_txout(0, op); } + { uint32_t lt = 0; gentx.write(std::span(reinterpret_cast(<), 4)); } + + coinbase_bytes_for_hashlink.assign( + reinterpret_cast(gentx.data()), + reinterpret_cast(gentx.data()) + gentx.size()); + } + + // Compute hash_link (V35: HashLinkType) + constexpr size_t suffix_len = 32 + 8 + 4; + if (coinbase_bytes_for_hashlink.size() > suffix_len) { + std::vector prefix( + coinbase_bytes_for_hashlink.begin(), coinbase_bytes_for_hashlink.end() - suffix_len); + share.m_hash_link = prefix_to_hash_link_v35(prefix, gentx_before_refhash); + + size_t nonce_offset = coinbase_bytes_for_hashlink.size() - 4 - 8; + uint64_t extracted_nonce = 0; + std::memcpy(&extracted_nonce, coinbase_bytes_for_hashlink.data() + nonce_offset, 8); + share.m_last_txout_nonce = extracted_nonce; + } + + // --- Compute share hash (block header double-SHA256) --- + PackStream header_stream; + { uint32_t v = static_cast(min_header.m_version); + header_stream << v; } + header_stream << min_header.m_previous_block; + + uint256 gentx_hash_for_header; + if (!actual_coinbase_bytes.empty()) { + auto actual_span = std::span( + actual_coinbase_bytes.data(), actual_coinbase_bytes.size()); + gentx_hash_for_header = Hash(actual_span); + } else { + auto cb_span = std::span( + coinbase_bytes_for_hashlink.data(), coinbase_bytes_for_hashlink.size()); + gentx_hash_for_header = Hash(cb_span); + } + uint256 merkle_root = check_merkle_link(gentx_hash_for_header, share.m_merkle_link); + + header_stream << merkle_root; + header_stream << min_header.m_timestamp; + header_stream << min_header.m_bits; + header_stream << min_header.m_nonce; + + auto hdr_span = std::span( + reinterpret_cast(header_stream.data()), header_stream.size()); + + uint256 share_hash = Hash(hdr_span); + share.m_hash = share_hash; + + // PoW check against share target + { + uint256 target = chain::bits_to_target(share.m_bits); + if (!target.IsNull()) { + uint256 pow_hash = params.pow_func(hdr_span); + + if (pow_hash > target) { + return uint256(); // didn't meet share target + } + LOG_INFO << "[Pool] V35 SHARE CREATED! pow=" << pow_hash.GetHex().substr(0, 16) + << " target=" << target.GetHex().substr(0, 16) + << " diff=" << chain::target_to_difficulty(target); + } + } + + // Add to tracker + auto* heap_share = new PaddingBugfixShare(share); + tracker.add(heap_share); + LOG_INFO << "create_local_share_v35: added share " << share_hash.GetHex() + << " height=" << share.m_absheight + << " prev=" << prev_share.GetHex().substr(0, 16) << "..."; + + return share_hash; +} + +// ============================================================================ +// create_local_share() +// +// Constructs a share (V35 PaddingBugfixShare or V36 MergedMiningShare) from +// locally-generated block data and adds it to the share tracker. +// Returns the share hash (block header double-SHA256). +// +// Parameters: +// tracker — the ShareTracker to insert the new share into +// min_header — parsed SmallBlockHeaderType from the found block +// coinbase — the p2pool coinbase scriptSig (BIP34 height + pool marker) +// subsidy — block reward (coinbasevalue) +// prev_share — previous best share hash from the tracker +// merkle_branches — Stratum merkle branches (coinbase txid → merkle root) +// payout_script — finder's scriptPubKey +// donation — donation bps (e.g. 50 = 0.5%) +// merged_addrs — optional merged mining addresses +// share_version — 35 or 36 (default 36) +// +// This builds the p2pool coinbase in the same format as +// generate_share_transaction() and computes the hash_link so remote peers +// can verify the share. +// ============================================================================ +template +uint256 create_local_share( + TrackerT& tracker, + const core::CoinParams& params, + const coin::SmallBlockHeaderType& min_header, + const BaseScript& coinbase, + uint64_t subsidy, + const uint256& prev_share, + const std::vector& merkle_branches, + const std::vector& payout_script, + uint16_t donation = 50, + const std::vector& merged_addrs = {}, + StaleInfo stale_info = StaleInfo::none, + bool segwit_active = false, + const std::string& witness_commitment_hex = {}, + const std::vector& message_data = {}, + const std::vector& actual_coinbase_bytes = {}, + const uint256& witness_root = uint256(), + uint32_t override_max_bits = 0, + uint32_t override_bits = 0, + // Frozen share fields from template time — when set, override computed values + // to ensure ref_hash matches the one embedded in the coinbase. + uint32_t frozen_absheight = 0, + uint128 frozen_abswork = uint128(), + uint256 frozen_far_share_hash = uint256(), + uint32_t frozen_timestamp = 0, + uint256 frozen_merged_payout_hash = uint256(), + bool has_frozen = false, + const std::vector& frozen_merkle_branches = {}, + const uint256& frozen_witness_root = uint256(), + const std::vector& frozen_merged_coinbase_info = {}, + int64_t share_version = 36, + uint64_t desired_version = 36) +{ + // V35 path: delegate to version-specific implementation + if (share_version <= 35) + return create_local_share_v35( + tracker, params, min_header, coinbase, subsidy, prev_share, merkle_branches, + payout_script, donation, stale_info, segwit_active, witness_commitment_hex, + actual_coinbase_bytes, witness_root, override_max_bits, override_bits, + frozen_absheight, frozen_abswork, frozen_far_share_hash, frozen_timestamp, + has_frozen, frozen_merkle_branches, frozen_witness_root, desired_version); + + MergedMiningShare share; + share.m_min_header = min_header; + share.m_coinbase = coinbase; + share.m_subsidy = subsidy; + share.m_prev_hash = prev_share; + share.m_donation = donation; + share.m_stale_info = stale_info; + share.m_desired_version = desired_version; + + // Timestamp: clip to at least previous_share.timestamp + 1 (matches Python) + share.m_timestamp = min_header.m_timestamp; + if (!prev_share.IsNull() && tracker.chain.contains(prev_share)) { + uint32_t prev_ts = 0; + tracker.chain.get(prev_share).share.invoke([&](auto* prev) { + prev_ts = prev->m_timestamp; + }); + if (share.m_timestamp <= prev_ts) + share.m_timestamp = prev_ts + 1; + } + + // Compute pool-level share target AFTER timestamp clipping (matches ref_hash_fn). + auto desired_target = chain::bits_to_target(min_header.m_bits); + auto [share_max_bits, share_bits] = tracker.compute_share_target( + prev_share, share.m_timestamp, desired_target); + share.m_max_bits = share_max_bits; + share.m_bits = share_bits; + + share.m_nonce = 0; // share commitment nonce (not block nonce) + share.m_merged_addresses = merged_addrs; + + // Diagnostic: confirm merged_addresses are populated for DOGE skiplist Tier 1 + { + LOG_INFO << "[create_local_share] merged_addresses=" << merged_addrs.size(); + for (const auto& entry : merged_addrs) { + auto to_hex = [](const std::vector& s) { + static const char* H = "0123456789abcdef"; + std::string r; for (auto b : s) { r += H[b>>4]; r += H[b&0xf]; } return r; + }; + LOG_INFO << "[create_local_share] chain_id=" << entry.m_chain_id + << " script=" << to_hex(entry.m_script.m_data); + } + } + + // Embed encrypted message_data (from create_message_data()) if provided + if (!message_data.empty()) + share.m_message_data.m_data = message_data; + + // Compute merged_payout_hash: deterministic hash of V36-only PPLNS + // weight distribution so peers can verify merged mining payouts. + if (!prev_share.IsNull() && tracker.chain.contains(prev_share)) + { + auto block_target = chain::bits_to_target(min_header.m_bits); + share.m_merged_payout_hash = tracker.compute_merged_payout_hash( + prev_share, block_target); + } + + // Payout identity — extract pubkey_hash + pubkey_type from scriptPubKey. + // Must match p2pool V36: 0=P2PKH, 1=P2WPKH, 2=P2SH. + if (payout_script.size() >= 20) { + 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) { + // P2PKH: 76 a9 14 88 ac + std::memcpy(share.m_pubkey_hash.data(), payout_script.data() + 3, 20); + share.m_pubkey_type = 0; + } else if (payout_script.size() == 23 && + payout_script[0] == 0xa9 && payout_script[1] == 0x14 && + payout_script[22] == 0x87) { + // P2SH: a9 14 87 + std::memcpy(share.m_pubkey_hash.data(), payout_script.data() + 2, 20); + share.m_pubkey_type = 2; + } else if (payout_script.size() == 22 && + payout_script[0] == 0x00 && payout_script[1] == 0x14) { + // P2WPKH: 00 14 + // P2WPKH: store raw witness program bytes directly. + // No reversal — c2pool uses raw bytes throughout, unlike p2pool's + // IntType(160) LE integer convention. The wire serialization of + // uint160 preserves the byte order from memcpy. + std::memcpy(share.m_pubkey_hash.data(), payout_script.data() + 2, 20); + share.m_pubkey_type = 1; + } else { + // Fallback: store first 20 bytes as P2PKH + std::memcpy(share.m_pubkey_hash.data(), payout_script.data(), 20); + share.m_pubkey_type = 0; + } + } + + // Chain position: absheight and abswork from previous share + if (!prev_share.IsNull() && tracker.chain.contains(prev_share)) { + auto [prev_height, last] = tracker.chain.get_height_and_last(prev_share); + tracker.chain.get(prev_share).share.invoke([&](auto* prev) { + share.m_absheight = prev->m_absheight + 1; + }); + + // abswork: prev_abswork + target_to_average_attempts(THIS share's bits) + // Python: abswork = (prev.abswork + target_to_average_attempts(bits.target)) % 2^128 + { + auto current_attempts = chain::target_to_average_attempts( + chain::bits_to_target(share.m_bits)); + uint128 prev_abswork; + tracker.chain.get(prev_share).share.invoke([&](auto* prev) { + prev_abswork = prev->m_abswork; + }); + share.m_abswork = prev_abswork + uint128(current_attempts.GetLow64()); + } + + // far_share_hash: 99th ancestor (matches Python: get_nth_parent_hash(prev_hash, 99)) + if (last.IsNull() && prev_height < 99) { + // 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); + } + } else { + // Genesis: p2pool always does (prev_absheight + 1), (prev_abswork + aps) + // With prev=None: absheight = 0 + 1 = 1, abswork = 0 + aps(bits) + share.m_absheight = 1; + share.m_abswork = uint128(chain::target_to_average_attempts( + chain::bits_to_target(share.m_bits)).GetLow64()); + share.m_far_share_hash = uint256(); + } + + // Override with frozen fields from template time (if available). + // These must match what ref_hash_fn computed when building the coinbase. + if (has_frozen) { + LOG_INFO << "[frozen] Applying: absheight=" << frozen_absheight + << " bits=" << std::hex << override_bits + << " max_bits=" << override_max_bits << std::dec + << " ts=" << frozen_timestamp; + share.m_absheight = frozen_absheight; + share.m_abswork = frozen_abswork; + share.m_far_share_hash = frozen_far_share_hash; + share.m_timestamp = frozen_timestamp; + share.m_merged_payout_hash = frozen_merged_payout_hash; + if (override_max_bits) share.m_max_bits = override_max_bits; + if (override_bits) share.m_bits = override_bits; + // Deserialize frozen merged_coinbase_info blob → vector + if (!frozen_merged_coinbase_info.empty()) { + try { + PackStream ps; + ps.write(std::span( + reinterpret_cast(frozen_merged_coinbase_info.data()), + frozen_merged_coinbase_info.size())); + ps >> share.m_merged_coinbase_info; + } catch (const std::exception& e) { + LOG_WARNING << "[frozen] Failed to deserialize merged_coinbase_info (" + << frozen_merged_coinbase_info.size() << " bytes): " << e.what(); + } + } + } + + // Random last_txout_nonce for OP_RETURN uniqueness + share.m_last_txout_nonce = static_cast(std::time(nullptr)) ^ + (static_cast(min_header.m_nonce) << 32); + + // --- Build the p2pool coinbase in the same format as generate_share_transaction --- + // This is needed to compute hash_link and to verify the share locally. + // The coinbase format is: version(4) + vin(1 input) + vout(outputs...) + locktime(4) + // + // For the hash_link, we need to split the coinbase at the ref_hash boundary: + // prefix = everything up to (and including) gentx_before_refhash + // suffix = ref_hash + last_txout_nonce + locktime (= hash_link_data) + // + // We compute generate_share_transaction's coinbase from the share fields, + // then extract the prefix and compute the hash_link. + + // ref_merkle_link: empty branch (ref_hash = hash_ref directly) + share.m_ref_merkle_link.m_branch.clear(); + share.m_ref_merkle_link.m_index = 0; + + // merkle_link: from Stratum merkle branches + share.m_merkle_link.m_branch = merkle_branches; + share.m_merkle_link.m_index = 0; + + // Populate segwit_data for V36 when segwit is active + if (segwit_active && !witness_commitment_hex.empty()) + { + SegwitData sd; + // txid_merkle_link: use FROZEN merkle branches from template time if available. + // The ref_hash was computed with these branches at template time. If new transactions + // arrived between template creation and share submission, the branches change → + // ref_hash mismatch → p2pool rejects the share as "PoW invalid". + sd.m_txid_merkle_link.m_branch = (has_frozen && !frozen_merkle_branches.empty()) + ? frozen_merkle_branches : merkle_branches; + sd.m_txid_merkle_link.m_index = 0; + // Debug: log only when frozen/current diverge (indicates the fix is active) + if (has_frozen && !frozen_merkle_branches.empty() && + frozen_merkle_branches.size() != merkle_branches.size()) { + LOG_INFO << "[segwit-freeze] branches changed: frozen=" + << frozen_merkle_branches.size() + << " current=" << merkle_branches.size(); + } + // wtxid_merkle_root: use frozen witness root if available. + // Priority: frozen > direct witness_root > zero (SegwitDataDefault). + // NEVER extract from witness_commitment_hex — that contains the + // p2pool commitment (Hash(root, nonce)), not the raw root. + // Storing it in m_wtxid_merkle_root causes generate_share_transaction + // to double-hash: Hash(Hash(root, nonce), nonce) → GENTX mismatch. + // Priority: frozen > direct. Zero root (0x00..00) is VALID — it's + // the correct witness root for coinbase-only blocks. p2pool uses it + // to compute SHA256d(0x00..00 || '[P2Pool]'*4) as the OP_RETURN commitment. + sd.m_wtxid_merkle_root = !frozen_witness_root.IsNull() + ? frozen_witness_root : witness_root; + share.m_segwit_data = sd; + } + + // --- Compute the ref_hash --- + PackStream ref_stream; + { + auto hex = params.active_identifier_hex(); + for (size_t i = 0; i + 1 < hex.size(); i += 2) { + unsigned char byte = static_cast( + std::stoul(hex.substr(i, 2), nullptr, 16)); + ref_stream.write(std::span( + reinterpret_cast(&byte), 1)); + } + } + ref_stream << share.m_prev_hash; + ref_stream << share.m_coinbase; + ref_stream << share.m_nonce; + ref_stream << share.m_pubkey_hash; + ref_stream << share.m_pubkey_type; + ::Serialize(ref_stream, VarInt(share.m_subsidy)); + ref_stream << share.m_donation; + { uint8_t si = static_cast(share.m_stale_info); ref_stream << si; } + ::Serialize(ref_stream, VarInt(share.m_desired_version)); + // segwit_data: PossiblyNoneType in p2pool — ALWAYS serialize. + // When None, write default: {txid_merkle_link: {branch: [], index: 0}, wtxid_merkle_root: 0} + // = varint(0) [empty branch list] + uint256(0) [wtxid_merkle_root] = 33 bytes. + if (share.m_segwit_data.has_value()) { + ref_stream << share.m_segwit_data.value(); + } else { + // Write PossiblyNoneType default: empty branch list + zero wtxid_merkle_root + std::vector empty_branch; + ref_stream << empty_branch; // varint(0) + uint256 zero_root; + ref_stream << zero_root; // 32 zero bytes + } + ref_stream << share.m_merged_addresses; + ref_stream << share.m_far_share_hash; + ref_stream << share.m_max_bits; + ref_stream << share.m_bits; + ref_stream << share.m_timestamp; + ref_stream << share.m_absheight; + ::Serialize(ref_stream, Using(share.m_abswork)); + ref_stream << share.m_merged_coinbase_info; + ref_stream << share.m_merged_payout_hash; + // V36 ref_type includes message_data (empty BaseScript → varint(0) = 0x00) + ref_stream << share.m_message_data; + + auto ref_span_v = std::span( + reinterpret_cast(ref_stream.data()), ref_stream.size()); + uint256 hash_ref = Hash(ref_span_v); + uint256 ref_hash = check_merkle_link(hash_ref, share.m_ref_merkle_link); + + // Dump ref_stream for cross-impl comparison (always, for diagnostics) + { + if (!actual_coinbase_bytes.empty()) { + static const char* HX = "0123456789abcdef"; + std::string full_hex; + auto* rd = reinterpret_cast(ref_stream.data()); + for (size_t i = 0; i < ref_stream.size(); ++i) { full_hex += HX[rd[i]>>4]; full_hex += HX[rd[i]&0xf]; } + LOG_INFO << "[REF-HASH] ref_packed_len=" << ref_stream.size() + << " ref_hash=" << hash_ref.GetHex() + << " prev=" << share.m_prev_hash.GetHex().substr(0, 16) + << " abs=" << share.m_absheight + << " bits=" << std::hex << share.m_bits + << " maxbits=" << share.m_max_bits << std::dec; + LOG_INFO << "[REF-HASH-FULL] " << full_hex; + } + } + + // If we have actual coinbase bytes, extract the ref_hash from the coinbase. + // The coinbase has ref_hash embedded at position [len-44 : len-44+32]. + // This is the ref_hash computed at template time (by ref_hash_fn) — it + // uses the FROZEN tracker state, not the current state which may have changed. + // Using the coinbase's ref_hash ensures hash_link consistency. + // Always use ref_hash from the actual coinbase (frozen at template creation time). + // The recomputed ref_hash may differ because share chain state changed between + // template creation and share submission (absheight, far_share_hash, bits, etc.). + // This is the EXACT same approach as p2pool: the gentx is captured once in a + // closure and never re-derived. + if (!actual_coinbase_bytes.empty() && actual_coinbase_bytes.size() > 44) { + uint256 coinbase_ref_hash; + std::memcpy(coinbase_ref_hash.data(), + actual_coinbase_bytes.data() + actual_coinbase_bytes.size() - 44, 32); + ref_hash = coinbase_ref_hash; + } + + // Diagnostic: log ref_hash match/mismatch (first few only to avoid log spam) + { + static int ref_diag = 0; + bool match = (hash_ref == ref_hash); + if (!match || ref_diag < 3) { + LOG_INFO << "[ref_stream] total=" << ref_stream.size() + << " ref_hash_match=" << (match ? "YES" : "NO"); + ++ref_diag; + } + } + + // --- Derive hash_link from the actual mined coinbase --- + // Python p2pool captures the gentx at template creation time in a closure + // and NEVER re-computes PPLNS. We follow the same pattern: the coinbase + // bytes from refresh_work() are the single source of truth. + // + // When actual_coinbase_bytes is provided (normal mining path), we use it + // directly. This eliminates the race where the share chain changes between + // template creation and share submission, which caused GENTX mismatches + // and p2pool peer bans. + auto gentx_before_refhash = compute_gentx_before_refhash(int64_t(36), params); + + std::vector coinbase_bytes_for_hashlink; + if (!actual_coinbase_bytes.empty()) { + // Use the actual mined coinbase — matches exactly what the miner solved. + coinbase_bytes_for_hashlink = actual_coinbase_bytes; + } else { + // Fallback (no actual bytes): must reconstruct from PPLNS. + // This path is only used in unit tests or if the caller doesn't + // provide actual_coinbase_bytes. + std::map, uint288> weights; + uint288 total_weight; + + if (!prev_share.IsNull() && tracker.chain.contains(prev_share)) + { + // Pass REAL_CHAIN_LENGTH — walk naturally stops at chain end. + auto chain_len = static_cast(params.real_chain_length); + auto block_target = chain::bits_to_target(share.m_min_header.m_bits); + auto max_weight = chain::target_to_average_attempts(block_target) + * params.spread * 65535; + auto result = tracker.get_v36_decayed_cumulative_weights(prev_share, chain_len, max_weight); + weights = std::move(result.weights); + total_weight = result.total_weight; + } + + std::map, uint64_t> amounts; + if (!total_weight.IsNull()) { + for (auto& [script, weight] : weights) { + uint64_t amount = (uint288(subsidy) * weight / total_weight).GetLow64(); + if (amount > 0) + amounts[script] = amount; + } + } + uint64_t sum_amounts = 0; + for (auto& [s, a] : amounts) sum_amounts += a; + uint64_t donation_amount = (subsidy > sum_amounts) ? (subsidy - sum_amounts) : 0; + if (donation_amount < 1 && subsidy > 0 && !amounts.empty()) { + // Deterministic tiebreak: (amount, script) — largest script wins when equal + auto largest = std::max_element(amounts.begin(), amounts.end(), + [](const auto& a, const auto& b) { + if (a.second != b.second) return a.second < b.second; + return a.first < b.first; + }); + if (largest != amounts.end() && largest->second > 0) { + largest->second -= 1; sum_amounts -= 1; + donation_amount = subsidy - sum_amounts; + } + } + + std::vector, uint64_t>> payout_outputs( + amounts.begin(), amounts.end()); + std::sort(payout_outputs.begin(), payout_outputs.end(), + [](const auto& a, const auto& b) { + if (a.second != b.second) return a.second < b.second; + return a.first < b.first; + }); + if (payout_outputs.size() > 4000) + payout_outputs.erase(payout_outputs.begin(), payout_outputs.end() - 4000); + + PackStream gentx; + { uint32_t v = 1; gentx.write(std::span(reinterpret_cast(&v), 4)); } + { unsigned char one = 1; gentx.write(std::span(reinterpret_cast(&one), 1)); } + { uint256 z; gentx << z; } + { uint32_t idx = 0xffffffff; gentx.write(std::span(reinterpret_cast(&idx), 4)); } + gentx << share.m_coinbase; + { uint32_t seq = 0xffffffff; gentx.write(std::span(reinterpret_cast(&seq), 4)); } + size_t n_outs = payout_outputs.size() + 1 + 1; + bool has_segwit_fb = share.m_segwit_data.has_value(); + if (has_segwit_fb) n_outs += 1; + if (n_outs < 253) { uint8_t cnt = (uint8_t)n_outs; gentx.write(std::span(reinterpret_cast(&cnt), 1)); } + else { uint8_t m = 0xfd; gentx.write(std::span(reinterpret_cast(&m), 1)); uint16_t cnt = (uint16_t)n_outs; gentx.write(std::span(reinterpret_cast(&cnt), 2)); } + auto write_txout = [&](uint64_t value, const std::vector& script) { + gentx.write(std::span(reinterpret_cast(&value), 8)); + BaseScript bs; bs.m_data = script; gentx << bs; + }; + if (has_segwit_fb) { + auto& sd = share.m_segwit_data.value(); + std::vector wscript = {0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed}; + uint256 commitment = compute_p2pool_witness_commitment(sd.m_wtxid_merkle_root); + auto cb = commitment.GetChars(); + wscript.insert(wscript.end(), cb.begin(), cb.end()); + write_txout(0, wscript); + } + for (auto& [script, amount] : payout_outputs) write_txout(amount, script); + write_txout(donation_amount, params.donation_script_func(int64_t(36))); + { std::vector op; op.push_back(0x6a); op.push_back(0x28); + op.insert(op.end(), ref_hash.data(), ref_hash.data() + 32); + uint64_t n = share.m_last_txout_nonce; auto* p = reinterpret_cast(&n); + op.insert(op.end(), p, p + 8); write_txout(0, op); } + { uint32_t lt = 0; gentx.write(std::span(reinterpret_cast(<), 4)); } + + coinbase_bytes_for_hashlink.assign( + reinterpret_cast(gentx.data()), + reinterpret_cast(gentx.data()) + gentx.size()); + } + + // The split point: everything before ref_hash + last_txout_nonce + locktime + // = coinbase minus last (32 + 8 + 4) = 44 bytes + constexpr size_t suffix_len = 32 + 8 + 4; // ref_hash + last_txout_nonce + locktime + if (coinbase_bytes_for_hashlink.size() > suffix_len) { + std::vector prefix( + coinbase_bytes_for_hashlink.begin(), coinbase_bytes_for_hashlink.end() - suffix_len); + share.m_hash_link = prefix_to_hash_link(prefix, gentx_before_refhash); + + // Verify hash_link round-trip: does check_hash_link(hash_link, suffix) == Hash(full)? + { + static int hl_diag = 0; + if (hl_diag < 5) { + std::vector suffix( + coinbase_bytes_for_hashlink.end() - suffix_len, + coinbase_bytes_for_hashlink.end()); + uint256 hl_result = check_hash_link(share.m_hash_link, suffix, gentx_before_refhash); + auto full_span = std::span( + coinbase_bytes_for_hashlink.data(), coinbase_bytes_for_hashlink.size()); + uint256 direct_result = Hash(full_span); + bool match = (hl_result == direct_result); + + // Check if prefix ends with const_ending + size_t ce_len = gentx_before_refhash.size(); + bool ce_match = (prefix.size() >= ce_len) && + std::equal(gentx_before_refhash.begin(), gentx_before_refhash.end(), + prefix.end() - ce_len); + + LOG_INFO << "[hash_link-roundtrip] MATCH=" << (match ? "YES" : "NO") + << " CE_match=" << (ce_match ? "YES" : "NO") + << " prefix=" << prefix.size() + << " cb_total=" << coinbase_bytes_for_hashlink.size() + << " CE=" << ce_len; + + if (!match || !ce_match) { + LOG_WARNING << "[hash_link-roundtrip] MISMATCH! direct=" << direct_result.GetHex() + << " hashlink=" << hl_result.GetHex(); + // Dump boundary bytes for debugging + static const char* HX = "0123456789abcdef"; + if (prefix.size() >= ce_len) { + std::string pfx_tail, ce_hex; + for (size_t i = prefix.size() - ce_len; i < prefix.size(); ++i) { + pfx_tail += HX[prefix[i] >> 4]; + pfx_tail += HX[prefix[i] & 0xf]; + } + for (size_t i = 0; i < ce_len; ++i) { + ce_hex += HX[gentx_before_refhash[i] >> 4]; + ce_hex += HX[gentx_before_refhash[i] & 0xf]; + } + LOG_WARNING << "[hash_link-roundtrip] prefix_tail=" << pfx_tail; + LOG_WARNING << "[hash_link-roundtrip] expected_CE=" << ce_hex; + } + } + ++hl_diag; + } + } + + // Extract last_txout_nonce (8 bytes before locktime) + size_t nonce_offset = coinbase_bytes_for_hashlink.size() - 4 - 8; + uint64_t extracted_nonce = 0; + std::memcpy(&extracted_nonce, coinbase_bytes_for_hashlink.data() + nonce_offset, 8); + share.m_last_txout_nonce = extracted_nonce; + { + static int nonce_log = 0; + if (nonce_log++ < 5) { + static const char* HX = "0123456789abcdef"; + std::string nonce_hex; + for (int i = 0; i < 8; ++i) { + uint8_t b = coinbase_bytes_for_hashlink[nonce_offset + i]; + nonce_hex += HX[b>>4]; nonce_hex += HX[b&0xf]; + } + LOG_INFO << "[NONCE-EXTRACT] offset=" << nonce_offset + << " nonce_hex=" << nonce_hex + << " nonce_u64=0x" << std::hex << extracted_nonce << std::dec + << " cb_total=" << coinbase_bytes_for_hashlink.size() + << " src=" << (actual_coinbase_bytes.empty() ? "reconstructed" : "actual_mined"); + // Dump FULL actual coinbase hex for byte-by-byte comparison with p2pool + std::string full_cb_hex; + for (size_t i = 0; i < coinbase_bytes_for_hashlink.size(); ++i) { + full_cb_hex += HX[coinbase_bytes_for_hashlink[i]>>4]; + full_cb_hex += HX[coinbase_bytes_for_hashlink[i]&0xf]; + } + LOG_INFO << "[ACTUAL-CB] hex=" << full_cb_hex; + } + } + } + + // --- Compute share hash --- + // Build the full 80-byte block header and double-SHA256 it + PackStream header_stream; + { uint32_t v = static_cast(min_header.m_version); + header_stream << v; } + header_stream << min_header.m_previous_block; + + // Compute merkle root for the block header. + // The MINER hashed the coinbase with the REAL extranonce2, so we must use + // actual_coinbase_bytes for the merkle root in the block header. + uint256 gentx_hash_for_header; + if (!actual_coinbase_bytes.empty()) { + auto actual_span = std::span( + actual_coinbase_bytes.data(), actual_coinbase_bytes.size()); + gentx_hash_for_header = Hash(actual_span); + } else { + // Use the coinbase bytes we already computed for hash_link (either actual or reconstructed) + auto cb_span = std::span( + coinbase_bytes_for_hashlink.data(), coinbase_bytes_for_hashlink.size()); + gentx_hash_for_header = Hash(cb_span); + } + // For the BLOCK HEADER merkle root, always use the actual job merkle branches + // (share.m_merkle_link), NOT the frozen segwit_data.txid_merkle_link. + // The frozen branches are for the ref_hash/share_info serialization only. + // The block header must match the actual transactions in the template. + uint256 merkle_root = check_merkle_link(gentx_hash_for_header, share.m_merkle_link); + + header_stream << merkle_root; + header_stream << min_header.m_timestamp; + header_stream << min_header.m_bits; + header_stream << min_header.m_nonce; + + auto hdr_span = std::span( + reinterpret_cast(header_stream.data()), header_stream.size()); + + // Diagnostic: dump header for PoW debugging + { + static int diag_count = 0; + if (diag_count < 5 && !actual_coinbase_bytes.empty()) { + std::string hdr_hex; + static const char* HX = "0123456789abcdef"; + for (size_t i = 0; i < hdr_span.size(); ++i) { + hdr_hex += HX[hdr_span[i] >> 4]; + hdr_hex += HX[hdr_span[i] & 0xf]; + } + LOG_INFO << "[create_local_share-diag] header(80)=" << hdr_hex; + LOG_INFO << "[create_local_share-diag] gentx_hash=" << gentx_hash_for_header.GetHex(); + LOG_INFO << "[create_local_share-diag] merkle_root=" << merkle_root.GetHex(); + LOG_INFO << "[create_local_share-diag] prev_block=" << min_header.m_previous_block.GetHex(); + LOG_INFO << "[create_local_share-diag] nonce=" << min_header.m_nonce + << " timestamp=" << min_header.m_timestamp + << " bits=" << std::hex << min_header.m_bits << std::dec; + ++diag_count; + } + } + + uint256 share_hash = Hash(hdr_span); + + // Set the share's identity hash + share.m_hash = share_hash; + + // Self-validation: PoW check against share target. + // Also run share_init_verify to confirm peers will accept this share + // (same check they'll run when they receive it). + { + uint256 target = chain::bits_to_target(share.m_bits); + if (!target.IsNull()) { + uint256 pow_hash = params.pow_func(hdr_span); + + if (pow_hash > target) { + // Expected: most stratum pseudoshares don't meet share target + return uint256(); + } + LOG_INFO << "[Pool] REAL SHARE CREATED! pow=" << pow_hash.GetHex().substr(0, 16) + << " target=" << target.GetHex().substr(0, 16) + << " diff=" << chain::target_to_difficulty(target); + // Dump raw 80-byte header for byte-level comparison with p2pool + { + static int hdr_dump = 0; + if (hdr_dump++ < 5) { + static const char* HX = "0123456789abcdef"; + std::string hex; + auto* rd = reinterpret_cast(hdr_span.data()); + for (size_t i = 0; i < hdr_span.size() && i < 80; ++i) { + hex += HX[rd[i]>>4]; hex += HX[rd[i]&0xf]; + } + LOG_INFO << "[HDR-DUMP] " << hex + << " hash=" << share.m_hash.GetHex().substr(0, 16); + } + } + // Log fields for comparison with p2pool's SHARE-REJECT + { + static int sl = 0; + if (sl++ < 5) { + LOG_INFO << "[SHARE-FIELDS] header_bits=" << std::hex << share.m_min_header.m_bits + << " share_bits=" << share.m_bits + << " max_bits=" << share.m_max_bits << std::dec + << " absheight=" << share.m_absheight + << " ts=" << share.m_timestamp + << " donation=" << share.m_donation + << " segwit=" << (share.m_segwit_data.has_value() ? "YES" : "NO") + << " prev=" << share.m_prev_hash.GetHex().substr(0, 16); + } + } + + // Cross-check: run the SAME verification that peers will run. + // If this fails, peers will reject the share as "PoW invalid". + // Cross-check: verify hash_link round-trip with the COINBASE ref_hash. + // The coinbase ref_hash was frozen at template time. If check_hash_link + // with this ref_hash produces the same gentx_hash as direct Hash(coinbase), + // peers will accept the share. + { + auto gentx_before_refhash_xc = compute_gentx_before_refhash(int64_t(36), params); + + // Use the ref_hash from the coinbase (same as what hash_link was built with) + std::vector xc_data; + xc_data.insert(xc_data.end(), ref_hash.data(), ref_hash.data() + 32); + { uint64_t n = share.m_last_txout_nonce; + auto* p = reinterpret_cast(&n); + xc_data.insert(xc_data.end(), p, p + 8); } + { uint32_t z = 0; + auto* p = reinterpret_cast(&z); + xc_data.insert(xc_data.end(), p, p + 4); } + + uint256 xc_gentx = check_hash_link(share.m_hash_link, xc_data, gentx_before_refhash_xc); + + if (xc_gentx != gentx_hash_for_header) { + // Cross-check failure is diagnostic only — p2pool has no such check. + // The share was already constructed with consistent hash_link + coinbase. + // Mismatch here means gentx_before_refhash() doesn't match the frozen + // template (PPLNS changed between template and submission). This is + // expected during genesis or when chain state changes rapidly. + // The share is still valid — peers verify via their own check_hash_link. + static int xc_warn = 0; + if (xc_warn++ < 10) + LOG_WARNING << "[Pool] Cross-check mismatch (non-blocking)" + << " hl_gentx=" << xc_gentx.GetHex().substr(0, 16) + << " direct_gentx=" << gentx_hash_for_header.GetHex().substr(0, 16); + } else { + LOG_INFO << "[Pool] Cross-check PASSED"; + } + } + } + } + + // One-time hex dump of the share_info portion for wire format debugging + { + static int wire_dump = 0; + if (wire_dump++ < 1) { + // Pack just the share_info fields to see exact wire bytes + // Dump share_data fields manually for wire format comparison + { + PackStream sd_pack; + // Serialize share_data portion (same as share.hpp lines 155-190) + // prev_hash (uint256 = 32 bytes) + sd_pack << share.m_prev_hash; + // coinbase (VarStr) + sd_pack << share.m_coinbase; + // nonce (uint32) + sd_pack.write(std::span( + reinterpret_cast(&share.m_nonce), 4)); + auto sd_span = sd_pack.get_span(); + auto* sp = reinterpret_cast(sd_span.data()); + std::string sd_hex; + for (size_t i = 0; i < sd_span.size(); ++i) { + static const char* H = "0123456789abcdef"; + sd_hex += H[sp[i] >> 4]; sd_hex += H[sp[i] & 0xf]; + } + LOG_INFO << "[WIRE-DUMP] share_data_head(" << sd_span.size() + << "): " << sd_hex.substr(0, 160); + LOG_INFO << "[WIRE-DUMP] m_coinbase(" << share.m_coinbase.size() + << "): " << sd_hex.substr(64, std::min(size_t(100), sd_hex.size()-64)); + } + LOG_INFO << "[WIRE-DUMP] m_bits=0x" << std::hex << share.m_bits + << " m_max_bits=0x" << share.m_max_bits << std::dec + << " m_timestamp=" << share.m_timestamp + << " m_absheight=" << share.m_absheight + << " m_donation=" << share.m_donation + << " m_subsidy=" << share.m_subsidy; + // Dump the share_info fields as they would be serialized + PackStream info_pack; + // far_share_hash (PossiblyNoneType(0, IntType(256))) + if (share.m_far_share_hash.IsNull()) { + uint8_t z = 0; info_pack.write(std::span(reinterpret_cast(&z), 1)); + } else { + uint8_t one = 1; info_pack.write(std::span(reinterpret_cast(&one), 1)); + info_pack.write(std::span(reinterpret_cast(share.m_far_share_hash.data()), 32)); + } + // max_bits, bits, timestamp, absheight (4 bytes each, LE) + info_pack.write(std::span(reinterpret_cast(&share.m_max_bits), 4)); + info_pack.write(std::span(reinterpret_cast(&share.m_bits), 4)); + info_pack.write(std::span(reinterpret_cast(&share.m_timestamp), 4)); + info_pack.write(std::span(reinterpret_cast(&share.m_absheight), 4)); + auto info_span = info_pack.get_span(); + std::string info_hex; + auto* ip = reinterpret_cast(info_span.data()); + for (size_t i = 0; i < info_span.size(); ++i) { + static const char* H = "0123456789abcdef"; + info_hex += H[ip[i] >> 4]; info_hex += H[ip[i] & 0xf]; + } + LOG_INFO << "[WIRE-DUMP] share_info_tail=" << info_hex; + } + } + + // Add to tracker (heap-allocate; ShareChain takes ownership via raw pointer) + auto* heap_share = new MergedMiningShare(share); + tracker.add(heap_share); + LOG_INFO << "create_local_share: added share " << share_hash.GetHex() + << " height=" << share.m_absheight + << " prev=" << prev_share.GetHex().substr(0, 16) << "..."; + + // Cross-check: call generate_share_transaction on the share we just created + // to verify p2pool would produce the same gentx_hash. + { + static int xcheck_count = 0; + if (true) { // Always cross-check (was: xcheck_count < 5) + uint256 verify_hash = generate_share_transaction(*heap_share, tracker, params, true, (MergedMiningShare::version >= 36)); + bool xcheck_ok = (verify_hash == gentx_hash_for_header); + if (xcheck_ok) { + LOG_INFO << "[Pool] Cross-check PASSED"; + } else { + LOG_WARNING << "[Pool] Cross-check FAILED! mined_gentx=" << gentx_hash_for_header.GetHex() + << " verify_gentx=" << verify_hash.GetHex(); + // Dump actual mined coinbase hex for byte-level comparison + if (!actual_coinbase_bytes.empty()) { + static const char* H = "0123456789abcdef"; + std::string mined_hex; + mined_hex.reserve(actual_coinbase_bytes.size() * 2); + for (unsigned char b : actual_coinbase_bytes) { mined_hex += H[b>>4]; mined_hex += H[b&0xf]; } + LOG_WARNING << "[XCHECK-MINED] len=" << actual_coinbase_bytes.size() + << " hex=" << mined_hex; + } + } + ++xcheck_count; + } + } + + return share_hash; +} + +} // namespace dgb diff --git a/src/impl/dgb/share_tracker.hpp b/src/impl/dgb/share_tracker.hpp new file mode 100644 index 000000000..58ca1edcf --- /dev/null +++ b/src/impl/dgb/share_tracker.hpp @@ -0,0 +1,2814 @@ +#pragma once + +// Portable 128-bit multiply-shift: (a * b) >> shift +// GCC/Clang have __uint128_t; MSVC uses _umul128 intrinsic. +// shift must be in [1, 63] to avoid undefined behavior from 64-bit shifts. +#ifdef _MSC_VER +#include +inline uint64_t mul128_shift(uint64_t a, uint64_t b, unsigned shift) { + uint64_t hi; + uint64_t lo = _umul128(a, b, &hi); + if (shift == 0) return lo; // (a*b) >> 0 — just the low 64 bits + if (shift >= 64) return hi >> (shift - 64); + return (hi << (64 - shift)) | (lo >> shift); +} +#else +inline uint64_t mul128_shift(uint64_t a, uint64_t b, unsigned shift) { + return static_cast((static_cast<__uint128_t>(a) * b) >> shift); +} +#endif + +#include "share.hpp" +#include "share_check.hpp" +#include "config_pool.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace dgb +{ + +struct StaleCounts +{ + uint64_t orphan_count = 0; + uint64_t doa_count = 0; + uint64_t total = 0; +}; + +// --- Scoring types --- + +struct TailScore +{ + int32_t chain_len{}; + uint288 hashrate; + uint288 best_head_work; // tiebreak: raw chain work of best head + + friend bool operator<(const TailScore& a, const TailScore& b) + { + return std::tie(a.chain_len, a.hashrate, a.best_head_work) + < std::tie(b.chain_len, b.hashrate, b.best_head_work); + } +}; + +struct HeadScore +{ + // p2pool: (work - min(punish,1)*ata(target), -reason, -time_seen) + // Sorted ascending, .back() = best. Standard < comparison. + uint288 adjusted_work; // work - punishment_deduction + int32_t neg_reason{}; // -reason (higher = less punished = better) + int64_t neg_time_seen{}; // -time_seen (higher = seen earlier = better) + + friend bool operator<(const HeadScore& a, const HeadScore& b) + { + if (a.adjusted_work < b.adjusted_work) return true; + if (b.adjusted_work < a.adjusted_work) return false; + return std::tie(a.neg_reason, a.neg_time_seen) < std::tie(b.neg_reason, b.neg_time_seen); + } +}; + +struct TraditionalScore +{ + // p2pool: (work, -time_seen, -reason) + // No is_local. Sorted ascending, .back() = best. + uint288 work; + int64_t neg_time_seen{}; + int32_t neg_reason{}; + + friend bool operator<(const TraditionalScore& a, const TraditionalScore& b) + { + if (a.work < b.work) return true; + if (b.work < a.work) return false; + return std::tie(a.neg_time_seen, a.neg_reason) < std::tie(b.neg_time_seen, b.neg_reason); + } +}; + +template +struct DecoratedData +{ + ScoreT score; + uint256 hash; + + friend bool operator<(const DecoratedData& a, const DecoratedData& b) + { + return a.score < b.score; + } +}; + +// --- Result types --- + +struct TrackerThinkResult +{ + uint256 best; + std::vector> desired; + std::set bad_peer_addresses; + bool punish_aggressively{false}; + // Top-5 scored heads from Phase 4 — used by clean_tracker() to protect + // the best chains from head pruning (p2pool node.py:363). + std::vector top5_heads; +}; + +struct CumulativeWeights +{ + std::map, uint288> weights; + uint288 total_weight; + uint288 total_donation_weight; +}; + +// ── Dense PPLNS Ring Buffer ────────────────────────────────────────────── +// Stores PPLNS-relevant data for each share in the sliding window as a +// contiguous array. Eliminates hash map pointer-chasing during PPLNS walks: +// 8640 sequential reads over ~500KB (fits L2 cache) vs 8640 random hash map +// lookups. ~10x faster per PPLNS computation. +// +// The precomputed decay table guarantees bit-exact results: it uses the same +// iterative truncation as the walk-based code (decay_fp[d+1] = (decay_fp[d] +// * decay_per) >> PRECISION), just stored in a table instead of recomputed. + +struct PPLNSEntry { + uint288 att; // target_to_average_attempts(bits_to_target(bits)) + uint32_t donation{0}; // share donation field + std::vector script; // payout script (variable-length) +}; + +class DensePPLNSWindow { +public: + static constexpr uint64_t DECAY_PRECISION = 40; + static constexpr uint64_t DECAY_SCALE = uint64_t(1) << DECAY_PRECISION; + static constexpr uint64_t LN2_MICRO = 693147; + + // Precomputed decay table: decay_table[d] = iterative decay factor at depth d. + // Computed once, reused for every PPLNS walk. Thread-safe after init. + static std::vector s_decay_table; + static uint64_t s_decay_per; + static bool s_table_initialized; + + static void init_decay_table(uint32_t chain_len) { + if (s_table_initialized && s_decay_table.size() == chain_len) + return; + uint32_t half_life = std::max(chain_len / 4, uint32_t(1)); + s_decay_per = DECAY_SCALE - (DECAY_SCALE * LN2_MICRO) / (uint64_t(1000000) * half_life); + s_decay_table.resize(chain_len); + s_decay_table[0] = DECAY_SCALE; + for (uint32_t d = 1; d < chain_len; ++d) + s_decay_table[d] = mul128_shift(s_decay_table[d-1], s_decay_per, DECAY_PRECISION); + s_table_initialized = true; + } + + // Deque of share entries: index 0 = depth 0 (shallowest/newest in window). + // Deque gives O(1) push/pop at both ends for forward and backward slides. + std::deque m_entries; + uint256 m_tip_hash; // hash of the share whose prev_hash starts this window + int32_t m_chain_len{0}; // target window size + + // Build ring from chain data. O(chain_len) hash map lookups — done once. + // After rebuild, m_entries[0] = share at start (depth 0), + // m_entries[n-1] = deepest share in window. + template + void rebuild(ChainT& chain, const uint256& start, int32_t chain_len) { + init_decay_table(static_cast(chain_len)); + m_entries.clear(); + m_tip_hash = start; + m_chain_len = chain_len; + + auto cur = start; + while (!cur.IsNull() && chain.contains(cur) + && static_cast(m_entries.size()) < chain_len) + { + PPLNSEntry entry; + chain.get_share(cur).invoke([&](auto* obj) { + entry.att = chain::target_to_average_attempts( + chain::bits_to_target(obj->m_bits)); + entry.donation = obj->m_donation; + entry.script = get_share_script(obj); + }); + m_entries.push_back(std::move(entry)); + + auto* idx = chain.get_index(cur); + cur = idx ? idx->tail : uint256(); + } + } + + // Slide window forward: new share enters at front (depth 0), oldest drops. + // Used when extending verification toward the chain tip. + void slide_forward(PPLNSEntry entering) { + m_entries.push_front(std::move(entering)); + if (static_cast(m_entries.size()) > m_chain_len) + m_entries.pop_back(); + } + + // Slide window backward: shallowest share drops, new deeper share added. + // Used by think() Phase 2 which walks backward via get_chain(last, N). + // Each successive share to verify needs PPLNS shifted 1 step deeper. + void slide_backward(PPLNSEntry deeper_entry) { + if (!m_entries.empty()) + m_entries.pop_front(); + m_entries.push_back(std::move(deeper_entry)); + } + + // Compute V36 decayed PPLNS weights from ring buffer. + // Bit-exact with the walk-based code: uses precomputed s_decay_table. + // Sequential-ish memory access (deque blocks) — ~80μs vs ~500μs hash map. + CumulativeWeights compute_v36_weights() const { + CumulativeWeights result; + int32_t n = static_cast(m_entries.size()); + for (int32_t d = 0; d < n; ++d) { + auto& e = m_entries[d]; + uint288 decayed_att = (e.att * uint288(s_decay_table[d])) >> DECAY_PRECISION; + auto addr_w = decayed_att * static_cast(65535 - e.donation); + auto don_w = decayed_att * e.donation; + auto this_total = addr_w + don_w; + result.weights[e.script] += addr_w; + result.total_weight += this_total; + result.total_donation_weight += don_w; + } + return result; + } + + bool empty() const { return m_entries.empty(); } + int32_t size() const { return static_cast(m_entries.size()); } +}; + +// ── Head-Level Incremental PPLNS ───────────────────────────────────────── +// Maintains a DensePPLNSWindow + cached CumulativeWeights per active head. +// When verifying consecutive shares along a chain: +// 1. rebuild() at first share's parent: O(chain_len) — one hash map walk +// 2. extend() for each subsequent share: O(1) ring push + O(chain_len) dense recompute +// Total for N shares: O(chain_len + N × chain_len) sequential ops +// vs current: O(N × chain_len) random hash map ops — ~10x faster per op. + +class HeadPPLNS { + DensePPLNSWindow m_ring; + CumulativeWeights m_cached; + bool m_dirty{true}; + +public: + // Cold start: build ring from chain. O(chain_len) hash map lookups. + template + void rebuild(ChainT& chain, const uint256& start, int32_t chain_len) { + m_ring.rebuild(chain, start, chain_len); + m_dirty = true; + } + + // Extend forward: new share enters at depth 0. O(1) ring update. + // Used when verifying toward chain tip (new shares extending verified head). + template + void extend_forward(ChainT& chain, const uint256& new_share_hash) { + PPLNSEntry entry; + chain.get_share(new_share_hash).invoke([&](auto* obj) { + entry.att = chain::target_to_average_attempts( + chain::bits_to_target(obj->m_bits)); + entry.donation = obj->m_donation; + entry.script = get_share_script(obj); + }); + m_ring.slide_forward(std::move(entry)); + m_dirty = true; + } + + // Extend backward: depth-0 drops, new deeper share added. O(1) ring update. + // Used by think() Phase 2 which walks backward via get_chain(last, N). + template + void extend_backward(ChainT& chain, const uint256& deeper_share_hash) { + PPLNSEntry entry; + chain.get_share(deeper_share_hash).invoke([&](auto* obj) { + entry.att = chain::target_to_average_attempts( + chain::bits_to_target(obj->m_bits)); + entry.donation = obj->m_donation; + entry.script = get_share_script(obj); + }); + m_ring.slide_backward(std::move(entry)); + m_dirty = true; + } + + // Get cached weights. Recomputes from ring if dirty. + // O(chain_len) dense sequential reads, ~50μs. + const CumulativeWeights& weights() { + if (m_dirty) { + m_cached = m_ring.compute_v36_weights(); + m_dirty = false; + } + return m_cached; + } + + bool valid() const { return !m_ring.empty(); } + int32_t window_size() const { return m_ring.size(); } + const DensePPLNSWindow& ring() const { return m_ring; } +}; + +// --- ShareTracker --- + +class ShareTracker +{ +public: + ShareChain chain; + ShareChain verified; + + // Coin parameters (non-owning). Set by the owning dgb::NodeImpl right + // after construction (m_tracker.m_params = &m_coin_params). Drives the + // parameterized verify_share() and protocol-version lookups (PR-0 S1). + const core::CoinParams* m_params = nullptr; + + + // Set by think() Phase 2 when verification budget is exhausted. + // Checked by run_think() to schedule a deferred continuation. + bool m_think_needs_continue{false}; + +private: + // Retry counter for log throttling only — p2pool retries every think() + // with no limit. Counter is cleared on successful verification or + // when the share is removed from the chain (on_removed signal). + std::unordered_map m_verify_fail_count; + + static int64_t now_seconds() + { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + } + +public: + // Callback fired when a share passes verification (for LevelDB persistence) + std::function m_on_share_verified; + // Callback fired when a verified share meets the block target (found block) + std::function m_on_block_found; + // Callback fired when a verified share meets a merged chain target (DOGE block) + // Args: share_hash, pow_hash (for target comparison by caller) + std::function m_on_merged_block_check; + // Callback fired for every verified share with its difficulty + miner script. + // Used to track best share difficulty for dashboard display. + std::function m_on_share_difficulty; + + // Scan the verified best chain for block solutions after startup. + // Uses cached pow_hash from index (stored during original attempt_verify). + // No scrypt recomputation needed — O(1) per share. + void scan_chain_for_blocks(const uint256& tip, int max_shares) + { + if (tip.IsNull() || max_shares <= 0) return; + int scanned = 0, found_ltc = 0, found_merged = 0, no_pow = 0; + uint256 pos = tip; + for (int i = 0; i < max_shares && !pos.IsNull(); ++i) { + if (!chain.contains(pos)) break; + auto* idx = chain.get_index(pos); + if (!idx) break; + + chain.get(pos).share.invoke([&](auto* s) { + ++scanned; + uint256 pow = idx->pow_hash; + if (pow.IsNull()) { ++no_pow; pos = s->m_prev_hash; return; } + + // Check LTC block target + uint256 block_target = chain::bits_to_target(s->m_min_header.m_bits); + if (!block_target.IsNull() && pow <= block_target) { + idx->is_block_solution = true; + if (m_on_block_found) { m_on_block_found(pos); ++found_ltc; } + } + // Check merged targets (V36 shares carry exact data in m_merged_coinbase_info) + if (m_on_merged_block_check) + m_on_merged_block_check(pos, pow); + + pos = s->m_prev_hash; + }); + } + LOG_INFO << "[BLOCK-SCAN] Scanned " << scanned << "/" << max_shares + << " shares: " << found_ltc << " LTC block(s), " + << no_pow << " without cached pow_hash"; + } + + ShareTracker() { + // p2pool SubsetTracker pattern: verified shares navigation + // through the MAIN tracker's skip list. + // SubsetTracker.get_nth_parent_hash = subset_of.get_nth_parent_hash + verified.set_parent_chain(&chain); + + // Connect weight skip list invalidation to chain's removed signal. + // p2pool: WeightsSkipList subscribes to tracker.removed via watch_weakref. + // Without this, pruned shares leave stale entries in weight caches. + chain.on_removed([this](const uint256& hash) { + invalidate_weight_caches(hash); + m_verify_fail_count.erase(hash); + }); + } + ~ShareTracker() + { + // verified borrows raw share pointers from chain — free its + // indexes only, then let chain's destructor free the share data. + verified.clear_unowned(); + } + + // -- Add share to the main chain -- + template + void add(ShareT* share) + { + if (!chain.contains(share->m_hash)) + { + try_register_merged_addr(share); + chain.add(share); + } + } + + void add(ShareType share) + { + auto h = share.hash(); + if (!chain.contains(h)) + { + // Register before chain.add() which may move the variant + share.invoke([this](auto* obj) { try_register_merged_addr(obj); }); + chain.add(share); + } + } + + // -- Attempt to verify a share -- + // Returns true if share is verified (already or newly). + // P2: share.check() will be wired here; for now we accept shares + // that have sufficient chain depth. + bool attempt_verify(const uint256& share_hash) + { + if (verified.contains(share_hash)) + return true; + + // p2pool has NO permanently-unverifiable concept — it retries + // share.check() every think() call. Shares that failed during a + // temporary fork may succeed once the fork resolves and the PPLNS + // walk changes. Skipping re-verification created permanent gaps + // in the verified chain → fragmentation (52 verified_tails). + + // NO parent-in-verified filter. p2pool doesn't have one. + // p2pool's attempt_verify has only the guard (height < CL+1 && unrooted). + // score() uses verified for height/work/chain (matching p2pool), + // so fragmentation in the raw chain tracker doesn't affect scoring. + + // p2pool: height, last = self.get_height_and_last(share.hash) + // p2pool's get_height uses get_delta_to_last() which walks through + // SubsetTracker's cached deltas — equivalent to our acc cache. + // Using acc cache height (not O(n) walk) matches p2pool's pattern + // and gives correct height after pruning. + auto acc_height = chain.get_acc_height(share_hash); + auto last = chain.get_last(share_hash); + + // p2pool: if height < self.net.CHAIN_LENGTH + 1 and last is not None: + // raise AssertionError() + // Chain too short and unrooted — cannot verify yet. + // The share isn't bad; its parents haven't arrived to fill the gap. + // DO NOT bypass this guard even if the parent is verified — + // generate_share_transaction needs CHAIN_LENGTH ancestors for correct + // PPLNS. Verifying with 3 ancestors produces wrong coinbase → GENTX-MISMATCH. + // Phase 2 naturally extends verification when parents arrive. + if (acc_height < static_cast(PoolConfig::chain_length()) + 1 && !last.IsNull()) + { + return false; + } + + // P2: init-phase verification (hash-link, merkle, PoW) + check-phase + try + { + auto t0 = std::chrono::steady_clock::now(); + auto& share_var = chain.get_share(share_hash); + share_var.ACTION({ + auto computed_hash = verify_share(*obj, *this, *m_params); + (void)computed_hash; + }); + { + auto dt = std::chrono::duration_cast( + std::chrono::steady_clock::now() - t0).count(); + static int64_t total_us = 0, count = 0; + total_us += dt; ++count; + if (count % 50 == 0) + LOG_INFO << "[VERIFY-PERF] last=" << dt << "us avg=" + << (total_us/count) << "us count=" << count; + } + } + catch (const std::exception& e) + { + // Counter for log throttling only — p2pool retries every think(). + auto& cnt = m_verify_fail_count[share_hash]; + ++cnt; + // Log first 3 failures verbosely, then every 10th to avoid spam. + if (cnt <= 3 || cnt % 10 == 0) + LOG_WARNING << "attempt_verify FAILED (" << cnt + << ") for " << share_hash.ToString().substr(0,16) + << " acc_height=" << acc_height << " last=" << (last.IsNull() ? "null" : last.ToString().substr(0,16)) + << " error: " << e.what(); + return false; + } + + // Success — clear any previous fail count + m_verify_fail_count.erase(share_hash); + + // Cache pow_hash on the index (for restart block scan — avoids recomputing scrypt) + if (!g_last_pow_hash.IsNull()) { + auto* idx = chain.get_index(share_hash); + if (idx) idx->pow_hash = g_last_pow_hash; + } + + // Add to verified chain + auto& share_var = chain.get_share(share_hash); + if (!verified.contains(share_hash)) + verified.add(share_var); + + // Notify LevelDB persistence layer + if (m_on_share_verified) + m_on_share_verified(share_hash); + + // Block detection: if share_init_verify flagged this share as a block solution, + // fire the callback. Matches p2pool's tracker.verified.added watcher (node.py:289). + if (m_on_block_found && g_last_init_is_block) { + g_last_init_is_block = false; + auto* idx = chain.get_index(share_hash); + if (idx) idx->is_block_solution = true; + m_on_block_found(share_hash); + } + + // Report share difficulty for best-share dashboard tracking + if (m_on_share_difficulty) { + share_var.invoke([&](auto* s) { + double diff = chain::target_to_difficulty(chain::bits_to_target(s->m_bits)); + std::string miner; + if constexpr (requires { s->m_pubkey_hash; }) + miner = s->m_pubkey_hash.GetHex(); + else if constexpr (requires { s->m_address; }) + miner = HexStr(s->m_address.m_data); + m_on_share_difficulty(diff, miner); + }); + } + + // Merged block detection: check ALL verified shares against DOGE target. + // A share can meet the DOGE target without meeting the LTC target + // (DOGE difficulty is much lower). The pow_hash was cached by share_init_verify. + if (m_on_merged_block_check && !g_last_pow_hash.IsNull()) { + m_on_merged_block_check(share_hash, g_last_pow_hash); + } + + // Naughty propagation: if parent is naughty, increment (up to 6 generations) + // Python data.py:1432-1438 — ancestor punishment for invalid block shares + { + uint256 prev_hash; + share_var.invoke([&](auto* obj) { prev_hash = obj->m_prev_hash; }); + if (!prev_hash.IsNull() && chain.contains(prev_hash)) { + auto* parent_idx = chain.get_index(prev_hash); + if (parent_idx && parent_idx->naughty > 0) { + auto* my_idx = chain.get_index(share_hash); + if (my_idx) { + my_idx->naughty = parent_idx->naughty + 1; + if (my_idx->naughty > 6) my_idx->naughty = 0; // reset after 6 generations + } + } + } + } + + // Block weight/size check: Python data.py:1508-1511 checks gentx + txs + // against BLOCK_MAX_WEIGHT/SIZE, but only when other_txs is available. + // For V34+ (including V36), other_txs is always None (shares use + // transaction_hash_refs, not embedded txs), so Python SKIPS this check. + // Therefore this is intentionally not computed for V36 shares. + // The coinbase correctness is already verified by share_check() via + // generate_share_transaction comparison. + + return true; + } + + // -- Score a chain from share_hash to CHAIN_LENGTH*15/16 ancestor -- + // Returns (chain_len, hashrate_score) — higher is better. + // p2pool data.py:2335-2347 — uses self.verified for ALL operations. + // May throw if the chain is concurrently modified. + TailScore score(const uint256& share_hash, + const std::function& block_rel_height_func) + { + uint288 score_res; + + // p2pool: head_height = self.verified.get_height(share_hash) + // Must use VERIFIED height — using chain inflates height with + // unverified shares, causing short verified chains to tie on + // chain_len with long chains and win on hashrate tiebreak. + auto head_height = verified.get_acc_height(share_hash); + if (head_height < static_cast(PoolConfig::chain_length())) + return {head_height, score_res}; + + // p2pool: end_point = self.verified.get_nth_parent_hash( + // share_hash, self.net.CHAIN_LENGTH*15//16) + // SubsetTracker delegates to parent's skip list (shared navigation). + auto end_point = verified.get_nth_parent_via_skip(share_hash, + (PoolConfig::chain_length() * 15) / 16); + + // p2pool: self.verified.get_chain(end_point, self.net.CHAIN_LENGTH//16) + std::optional block_height; + auto tail_count = std::min( + static_cast(PoolConfig::chain_length() / 16), + verified.get_acc_height(end_point)); + if (tail_count <= 0) + return {static_cast(PoolConfig::chain_length()), score_res}; + + auto tail_view = verified.get_chain(end_point, tail_count); + for (auto [hash, data] : tail_view) + { + uint256 prev_block; + data.share.invoke([&](auto* obj) { + prev_block = obj->m_min_header.m_previous_block; + }); + + auto bh = block_rel_height_func(prev_block); + if (!block_height.has_value() || bh > block_height.value()) + block_height = bh; + } + + // c2pool returns confirmations (1=tip, 0=unknown, -1=off-main-chain). + // p2pool returns relative height (0=tip, -N=behind, -1e9=unknown). + // When p2pool can't resolve a block, it computes score = work / (1e9 * 150) + // — tiny but non-zero, so both chains get similar scores and the one + // with more work wins (stable). c2pool must match: use a very large + // confirmation count so the score is tiny but non-zero, preventing + // oscillation where short chains beat long chains simply because the + // long chain's old blocks are unresolvable. + if (!block_height.has_value() || block_height.value() <= 0) + block_height = 1000000; // ~1M confirmations → time_span ≈ 150M seconds + + // p2pool: self.verified.get_delta(share_hash, end_point).work + auto total_work = verified.get_delta_work(share_hash, end_point); + + // p2pool: (0 - block_height + 1) * BLOCK_PERIOD + // c2pool confirmations: 1=tip → 150s, 4 → 600s (matches p2pool). + auto time_span = block_height.value() * 15; // DGB BLOCK_PERIOD = 15s (farsider350 digibyte.py:21) + if (time_span <= 0) + time_span = 1; + + score_res = total_work / static_cast(time_span); + return {static_cast(PoolConfig::chain_length()), score_res}; + } + + // -- Best-chain selection with verification and punishment -- + // bootstrap_mode: when true, removes verification budget limit so the + // entire chain is verified in one call. Used during initial sync when + // stratum isn't serving work — no IO needs the tracker lock. + // Matches p2pool where think() runs synchronously on the reactor and + // blocks everything else until verification completes. + TrackerThinkResult think(const std::function& block_rel_height_func, + const uint256& previous_block, + uint32_t bits, + bool bootstrap_mode = false) + { + // p2pool: desired is a set of (peer_addr, hash, max_timestamp, min_target) + // The timestamp is used to filter stale requests at return time. + struct DesiredEntry { + NetService peer; + uint256 hash; + uint32_t max_timestamp{0}; // max timestamp from chain head's recent shares + }; + std::vector desired; + std::set desired_hashes; // p2pool: desired = set() — dedup by hash + std::set bad_peer_addresses; + + // Phase 1: Verify unverified heads, remove bad shares. + // Exact translation of p2pool data.py:2077-2108. + // For each unverified head: walk backward, try to verify. + // If verification fails: remove the share (it's bad). + // If no verification possible and chain unrooted: request parents. + std::vector bads; + { + // Snapshot heads — we'll modify chain during iteration + auto heads_snapshot = chain.get_heads(); + int p1_skipped = 0, p1_walk0 = 0, p1_walked = 0, p1_verified = 0, p1_caught = 0; + { + static int p1_log = 0; + if (p1_log++ % 10 == 0) + LOG_INFO << "[think-P1] raw_heads=" << heads_snapshot.size() + << " verified_heads=" << verified.get_heads().size(); + } + for (auto& [head_hash, tail_hash] : heads_snapshot) + { + if (verified.get_heads().contains(head_hash)) { + ++p1_skipped; + continue; + } + + if (!chain.contains(head_hash)) continue; + + auto [head_height, last] = chain.get_height_and_last(head_hash); + + // get_height now returns accumulated height (including parent + // chain for new_fork shares). get_last follows segments to + // the true last. No special fork detection needed. + auto walk_count = last.IsNull() + ? head_height + : std::min(5, std::max(0, head_height - static_cast(PoolConfig::chain_length()))); + + if (walk_count <= 0) { + ++p1_walk0; + // p2pool: when walk_count=0, get_chain returns nothing, + // no shares added to bads. for/else requests parents. + // Do NOT remove height-1 unrooted heads as "stumps" — + // they're new peer shares whose parents haven't arrived yet. + // clean_tracker() eats truly stale heads after 300s. + if (!last.IsNull()) { + // Option A: skip parent requests for chains already in the + // pruning zone (height >= 2*CHAIN_LENGTH+10). These parents + // would be immediately re-pruned by clean_tracker. + auto CL_prune = static_cast(PoolConfig::chain_length()); + if (head_height >= 2 * CL_prune + 10) { + static int prune_skip_log = 0; + if (prune_skip_log++ % 20 == 0) + LOG_INFO << "[think-P1] pruning-zone skip: head=" + << head_hash.GetHex().substr(0,16) + << " height=" << head_height + << " threshold=" << (2*CL_prune+10); + } else if (!desired_hashes.count(last)) { + // Option C: dedup by hash (p2pool: desired = set()) + NetService peer; + uint32_t head_ts = 0; + chain.get_share(head_hash).invoke([&](auto* obj) { + peer = obj->peer_addr; + head_ts = obj->m_timestamp; + }); + desired_hashes.insert(last); + desired.push_back({peer, last, head_ts}); + } + } + continue; + } + + ++p1_walked; + bool verified_one = false; + try { + auto chain_view = chain.get_chain(head_hash, walk_count); + for (auto [hash, data] : chain_view) + { + if (attempt_verify(hash)) + { + verified_one = true; + ++p1_verified; + break; + } + // p2pool data.py:2215: bads.append(share.hash) + // ALL failing shares go into bads — p2pool has no + // permanently-unverifiable filter. remove() returns + // false for mid-chain shares (NotImplementedError in + // p2pool), which is caught below. + bads.push_back(hash); + } + } catch (const std::exception& ex) { + ++p1_caught; + LOG_WARNING << "[think-P1] exception walking head " << head_hash.GetHex().substr(0,16) + << " height=" << head_height << " walk=" << walk_count + << ": " << ex.what(); + continue; + } + + // Python for/else: if loop completed without break AND unrooted + if (!verified_one && !last.IsNull()) + { + // Option A: skip if chain already in pruning zone + auto CL_prune = static_cast(PoolConfig::chain_length()); + if (head_height >= 2 * CL_prune + 10) { + static int prune_skip2_log = 0; + if (prune_skip2_log++ % 20 == 0) + LOG_INFO << "[think-P1] pruning-zone skip (for/else): head=" + << head_hash.GetHex().substr(0,16) + << " height=" << head_height; + } else if (!desired_hashes.count(last)) { + // Option C: dedup by hash + NetService peer; + uint32_t head_ts = 0; + chain.get_share(head_hash).invoke([&](auto* obj) { + peer = obj->peer_addr; + head_ts = obj->m_timestamp; + }); + desired_hashes.insert(last); + desired.push_back({peer, last, head_ts}); + } + } + } + + // Diagnostic: per-cycle summary + { + static int p1_sum_log = 0; + if (p1_sum_log++ % 5 == 0) + LOG_INFO << "[think-P1-summary] heads=" << heads_snapshot.size() + << " skipped_verified=" << p1_skipped + << " walk0=" << p1_walk0 + << " walked=" << p1_walked + << " verified=" << p1_verified + << " caught=" << p1_caught + << " bads=" << bads.size(); + } + } + + // Phase 1.5 removed — was NOT in p2pool and caused GENTX-MISMATCH. + // It tried to forward-propagate verification from verified parents to + // unverified children, but children on stub forks (chain_len < CHAIN_LENGTH) + // got verified with truncated PPLNS → wrong coinbase. + // p2pool's Phase 2 naturally handles forward extension via rooted chains. + + // Remove bad shares (p2pool data.py:2224-2236). + // p2pool tries self.remove(bad) for ALL bads — catches + // NotImplementedError for mid-chain shares (have dependents). + // c2pool's chain.remove() returns false for that case. + // NO leaf-only filter — p2pool doesn't have one. + { + int removed_count = 0; + for (const auto& bad : bads) + { + if (verified.contains(bad)) + continue; // p2pool: raise ValueError (should never happen) + if (!chain.contains(bad)) continue; + + NetService bad_peer; + try { + chain.get_share(bad).invoke([&](auto* obj) { + bad_peer = obj->peer_addr; + }); + } catch (...) {} + if (bad_peer.port() != 0) + bad_peer_addresses.insert(bad_peer); + + // p2pool data.py:2233-2236: + // try: self.remove(bad) + // except NotImplementedError: pass + // chain.remove() returns false for mid-chain shares + // (equivalent to NotImplementedError). + try { + invalidate_weight_caches(bad); + if (verified.contains(bad)) + verified.remove(bad); + if (chain.remove(bad)) + ++removed_count; + } catch (...) {} + } + if (removed_count > 0) { + LOG_INFO << "[think-P1] removed " << removed_count + << " shares (bads=" << bads.size() << " + descendants)"; + } + } + + // Phase 2: Extend verification from verified heads. + // Budget-limited: verify up to THINK_VERIFY_BUDGET shares per call to + // prevent blocking the io_context for tens of seconds. Remaining shares + // are picked up by the next run_think() call (deferred via post()). + // p2pool avoids this problem by persisting verified status — we do too + // now, so budgeting is a safety net for cold starts only. + constexpr int THINK_VERIFY_BUDGET = 100; + int budget_remaining = bootstrap_mode ? INT_MAX : THINK_VERIFY_BUDGET; + m_think_needs_continue = false; + { + static int p2_skip_log = 0; + if (p2_skip_log++ % 20 == 0) + LOG_INFO << "[think-P2-iter] verified_heads=" << verified.get_heads().size() + << " chain=" << chain.size() << " verified=" << verified.size(); + } + // Sort verified heads by work (descending) so the best chain gets + // budget priority. p2pool iterates in arbitrary order but has no + // budget — with our budget, a low-scored side chain can starve the + // best chain if it goes first. + std::vector> sorted_vheads( + verified.get_heads().begin(), verified.get_heads().end()); + std::sort(sorted_vheads.begin(), sorted_vheads.end(), + [this](const auto& a, const auto& b) { + auto wa = verified.contains(a.first) ? verified.get_work(a.first) : uint288{}; + auto wb = verified.contains(b.first) ? verified.get_work(b.first) : uint288{}; + return wa > wb; // highest work first + }); + for (auto& [head_hash, tail_hash] : sorted_vheads) + { + if (budget_remaining <= 0) { + m_think_needs_continue = true; + break; + } + + if (!chain.contains(head_hash)) { + static int skip1 = 0; + if (skip1++ < 5) LOG_WARNING << "[think-P2] skip: head not in chain " << head_hash.GetHex().substr(0,16); + continue; + } + + auto [head_height, last_hash] = verified.get_height_and_last(head_hash); + if (!chain.contains(last_hash)) { + static int skip2 = 0; + if (skip2++ < 5) LOG_WARNING << "[think-P2] skip: last not in chain " << last_hash.GetHex().substr(0,16) + << " head_height=" << head_height; + continue; + } + + auto [last_height, last_last_hash] = chain.get_height_and_last(last_hash); + + // p2pool data.py:2098-2103 EXACTLY: + // want = max(self.net.CHAIN_LENGTH - head_height, 0) + // can = max(last_height - 1 - self.net.CHAIN_LENGTH, 0) if last_last_hash is not None else last_height + // get = min(want, can) + auto CL = static_cast(PoolConfig::chain_length()); + auto want = std::max(CL - head_height, 0); + auto can = last_last_hash.IsNull() + ? last_height + : std::max(last_height - 1 - CL, 0); + auto to_get = std::min(want, can); + + { + static int p2_log = 0; + if (p2_log++ % 20 == 0) + LOG_INFO << "[think-P2] head_height=" << head_height + << " last_height=" << last_height + << " last_rooted=" << last_last_hash.IsNull() + << " to_get=" << to_get; + } + + if (to_get > 0) + { + // ── Carry-forward verification with HeadPPLNS ────────── + // Build the PPLNS ring buffer once at the first share's + // prev_hash, then slide backward for each subsequent share. + // The ring buffer result is primed into the decayed cache so + // attempt_verify() → generate_share_transaction() → + // get_v36_decayed_cumulative_weights() hits O(1) cache + // instead of O(chain_len) hash map walk. + HeadPPLNS head_pplns; + bool pplns_active = false; + auto CL_i32 = static_cast(PoolConfig::chain_length()); + + auto chain_view = chain.get_chain(last_hash, to_get); + int p2_verified_count = 0; + for (auto [hash, data] : chain_view) + { + if (budget_remaining <= 0) { + m_think_needs_continue = true; + LOG_INFO << "[think-P2] budget exhausted after " << p2_verified_count + << " verifications, deferring remainder"; + break; + } + + // Prime PPLNS cache from ring buffer (if V36 and chain long enough) + // Derive from share version (matches p2pool check(): VERSION >= 36), + // not the removed mutable global. + uint256 prev_hash; + int share_ver = 0; + data.share.invoke([&](auto* obj) { + prev_hash = obj->m_prev_hash; + share_ver = obj->version; + }); + if (share_ver >= 36) { + + if (!prev_hash.IsNull() && chain.contains(prev_hash)) { + if (!pplns_active) { + // First share: full ring build — O(chain_len) hash map lookups + auto ring_t0 = std::chrono::steady_clock::now(); + head_pplns.rebuild(chain, prev_hash, CL_i32); + auto ring_us = std::chrono::duration_cast( + std::chrono::steady_clock::now() - ring_t0).count(); + pplns_active = true; + LOG_INFO << "[PPLNS-RING] built: window=" << head_pplns.window_size() + << " chain_len=" << CL_i32 << " build_time=" << ring_us << "us"; + } else { + // Subsequent shares: slide PPLNS window one step deeper. + // The new window drops the shallowest entry and adds the + // share at depth (chain_len - 1) from the new start. + // + // FIX: deep_hash IS the correct new tail entry (at depth + // chain_len-1 from new start). The previous code took + // deep_hash.tail (one step FURTHER), causing an off-by-one + // that shifted the tail entry one position too deep each + // extension — accumulating PPLNS weight errors and causing + // GENTX mismatches for all V36 shares. + auto ring_depth = head_pplns.window_size(); + if (ring_depth > 0) { + auto deep_hash = chain.get_nth_parent_via_skip( + prev_hash, std::min(ring_depth - 1, CL_i32 - 1)); + if (!deep_hash.IsNull() && chain.contains(deep_hash)) { + head_pplns.extend_backward(chain, deep_hash); + } else { + // Can't extend — rebuild from scratch + head_pplns.rebuild(chain, prev_hash, CL_i32); + } + } + } + + // Prime the existing decayed cache with ring buffer result + if (pplns_active && head_pplns.valid()) { + auto prime_t0 = std::chrono::steady_clock::now(); + auto& w = const_cast(head_pplns).weights(); + prime_pplns_cache(prev_hash, CL_i32, w); + auto prime_us = std::chrono::duration_cast( + std::chrono::steady_clock::now() - prime_t0).count(); + static int prime_log = 0; + if (prime_log++ % 20 == 0) + LOG_INFO << "[PPLNS-RING] cache primed: addrs=" << w.weights.size() + << " total_weight=" << w.total_weight.GetLow64() + << " compute=" << prime_us << "us" + << " share=" << p2_verified_count; + } + } + } + + // p2pool has no budget — it verifies all shares synchronously. + // With our budget, don't count already-verified shares against it. + // Head B's walk through Head A's verified territory should be free. + bool was_already_verified = verified.contains(hash); + if (!attempt_verify(hash)) + break; + ++p2_verified_count; + if (!was_already_verified) { + --budget_remaining; + } + // Throttled progress log: contabo freeze-diag (2026-05-24) caught + // this loop wedging the io_context for 2-7.5s at a time when emit + // was every 50 entries (173 lines per think cycle on an 8639-share + // chain). Boost::log internal mutex starves the io_context handler + // that runs the same think cycle. Bumped to every 1000 (~9 lines/ + // cycle); the loop runs at >1 verify/ms so a line/sec cadence is + // plenty for diagnostics. + if (p2_verified_count % 1000 == 0) + LOG_INFO << "[think-P2] verifying: " << p2_verified_count << "/" << to_get + << " new=" << (was_already_verified ? "no" : "yes") + << " budget=" << budget_remaining + << " verified_total=" << verified.size(); + } + } + + // Request more shares if verified chain is short + if (head_height < static_cast(PoolConfig::chain_length()) && !last_last_hash.IsNull()) + { + // Option A: check MAIN chain height (not verified height). + // If main chain is already in pruning zone, the unverified + // shares exist — they just need verification, not more parents. + auto main_ht = chain.get_height(head_hash); + auto CL_prune = static_cast(PoolConfig::chain_length()); + if (main_ht >= 2 * CL_prune + 10) { + static int p2_prune_log = 0; + if (p2_prune_log++ % 20 == 0) + LOG_INFO << "[think-P2] pruning-zone skip: head=" + << head_hash.GetHex().substr(0,16) + << " verified_ht=" << head_height + << " main_ht=" << main_ht; + } else if (!desired_hashes.count(last_last_hash)) { + // Option C: dedup by hash + NetService peer; + uint32_t head_ts = 0; + chain.get_share(head_hash).invoke([&](auto* obj) { + peer = obj->peer_addr; + head_ts = obj->m_timestamp; + }); + desired_hashes.insert(last_last_hash); + desired.push_back({peer, last_last_hash, head_ts}); + } + } + } + + // Phase 3: Score tails — pick the best tail + // p2pool: decorated_tails = sorted((self.score( + // max(self.verified.tails[tail_hash], key=self.verified.get_work), ... + // Uses VERIFIED.get_work (verified TrackerView), NOT chain.get_work. + // SubsetTracker.get_work walks verified items only. + std::vector> decorated_tails; + for (auto& [tail_hash, head_hashes] : verified.get_tails()) + { + // p2pool: max(verified.tails[tail_hash], key=verified.get_work) + uint256 best_head; + uint288 best_work; + bool first = true; + for (const auto& hh : head_hashes) + { + if (!verified.contains(hh)) continue; + auto w = verified.get_work(hh); + if (first || w > best_work) + { + best_work = w; + best_head = hh; + first = false; + } + } + + if (!best_head.IsNull()) + { + try { + auto s = score(best_head, block_rel_height_func); + s.best_head_work = best_work; // tiebreak by total work + decorated_tails.push_back({s, tail_hash}); + } catch (const std::exception&) { + // Chain was concurrently modified (trim removed an + // ancestor). Skip this tail — will be scored next cycle. + } + } + } + std::sort(decorated_tails.begin(), decorated_tails.end()); + + uint256 best_tail; + TailScore best_tail_score{}; + if (!decorated_tails.empty()) + { + best_tail = decorated_tails.back().hash; + best_tail_score = decorated_tails.back().score; + } + + // Debug: log scoring when multiple tails compete + if (decorated_tails.size() > 1) { + static int score_log = 0; + if (score_log++ % 10 == 0) { + for (auto& dt : decorated_tails) { + LOG_INFO << "[SCORE-TAIL] tail=" << dt.hash.GetHex().substr(0,16) + << " chain_len=" << dt.score.chain_len + << " hashrate=" << dt.score.hashrate.IsNull() + << (dt.hash == best_tail ? " ← WINNER" : ""); + } + } + } + + // Phase 4: Score heads within the best tail — pick the best head + std::vector> decorated_heads; + std::vector> traditional_sort; + + if (verified.get_tails().contains(best_tail)) + { + const auto& head_hashes = verified.get_tails().at(best_tail); + for (const auto& hh : head_hashes) + { + if (!verified.contains(hh)) + continue; + + try { + // p2pool Phase 4: + // self.verified.get_work(self.verified.get_nth_parent_hash(h, min(5, self.verified.get_height(h)))) + // verified.get_height uses verified TrackerView + // verified.get_nth_parent_hash uses SHARED skip list (main tracker) + // verified.get_work uses verified TrackerView + auto v_height = verified.get_acc_height(hh); + auto recent_ancestor = verified.get_nth_parent_via_skip(hh, std::min(5, v_height)); + uint288 work_score = verified.get_work(recent_ancestor); + + auto* head_idx = chain.get_index(hh); + if (!head_idx) continue; + int64_t ts = head_idx->time_seen; + + // Punish heads: naughty (invalid block) only. + // F10/(b): the non-canonical 95%-version-obsolescence punish + // is dropped — canonical Phase-4 head-scoring punishes only + // naughty heads. The version gate is the check() 60% weighted + // switch rule (share_check), not head-scoring. + int32_t reason = 0; + { + auto* idx = chain.get_index(hh); + if (idx && idx->naughty > 0) + reason = idx->naughty; + } + + // p2pool: sort key = (work - min(punish,1)*ata(target), -reason, -time_seen) + // peer_addr is None is COMMENTED OUT in p2pool — no is_local dimension. + // Punishment deducted from work: a punished share is mathematically behind. + uint288 adjusted_work = work_score; + if (reason > 0) { + // Deduct one share's worth of attempts (min(reason,1) * ata(target)) + auto* share_idx = chain.get_index(hh); + if (share_idx) + adjusted_work = adjusted_work - share_idx->work; + } + decorated_heads.push_back({{adjusted_work, -reason, -ts}, hh}); + traditional_sort.push_back({{work_score, -ts, -reason}, hh}); + } catch (const std::exception&) { + // Chain concurrently modified — skip this head, retry next cycle + } + } + std::sort(decorated_heads.begin(), decorated_heads.end()); + std::sort(traditional_sort.begin(), traditional_sort.end()); + } + + // p2pool: self.punish = punish value from Phase 5 (walk-back result) + bool punish_aggressively = !traditional_sort.empty() && traditional_sort.back().score.neg_reason != 0; + + // Phase 5: Determine best share — p2pool data.py:2142-2166 + // Walk back through punished shares, then find best non-naughty descendent. + uint256 best; + int32_t punish_val = 0; + if (!decorated_heads.empty()) + best = decorated_heads.back().hash; + + if (!best.IsNull() && chain.contains(best)) + { + // Check if best share should be punished + auto* best_idx = chain.get_index(best); + if (best_idx && best_idx->naughty > 0) + { + // Walk back through punished shares + while (best_idx && best_idx->naughty > 0) + { + uint256 prev; + chain.get_share(best).invoke([&](auto* obj) { + prev = obj->m_prev_hash; + }); + if (prev.IsNull() || !chain.contains(prev)) break; + best = prev; + best_idx = chain.get_index(best); + + // p2pool: if not punish, find best descendent + if (best_idx && best_idx->naughty == 0) + { + // Find deepest non-naughty child via reverse map + std::function(const uint256&, int)> best_desc; + best_desc = [&](const uint256& h, int limit) -> std::pair { + if (limit < 0) return {0, h}; + auto& rev = chain.get_reverse(); + auto rit = rev.find(h); + if (rit == rev.end() || rit->second.empty()) + return {0, h}; + std::pair best_kid = {-1, h}; + for (const auto& child : rit->second) { + auto* cidx = chain.get_index(child); + if (cidx && cidx->naughty > 0) continue; + auto [gen, hash] = best_desc(child, limit - 1); + if (gen + 1 > best_kid.first) + best_kid = {gen + 1, hash}; + } + return best_kid.first >= 0 ? best_kid : std::pair{0, h}; + }; + auto [gens, desc_hash] = best_desc(best, 20); + best = desc_hash; + break; + } + } + } + punish_val = (best_idx && best_idx->naughty > 0) ? best_idx->naughty : 0; + } + + // Phase 6: Compute cutoffs for desired shares filtering + uint32_t timestamp_cutoff; + if (!best.IsNull() && chain.contains(best)) + { + uint32_t best_ts = 0; + chain.get_share(best).invoke([&](auto* obj) { + best_ts = obj->m_timestamp; + }); + timestamp_cutoff = std::min(static_cast(now_seconds()), best_ts) - 3600; + } + else + { + timestamp_cutoff = static_cast(now_seconds()) - 24 * 60 * 60; + } + + // Filter desired by timestamp cutoff — p2pool data.py:2374 EXACTLY: + // return best, [(peer_addr, hash) for peer_addr, hash, ts, targ in desired + // if ts >= timestamp_cutoff] + // This prevents requesting shares from stale/pruned chain tails that + // peers no longer have. Without this, we hammer peers with unanswerable + // requests and they eventually drop us. + std::vector> desired_result; + for (auto& d : desired) { + if (d.max_timestamp >= timestamp_cutoff) { + desired_result.emplace_back(d.peer, d.hash); + } else { + static int filter_log = 0; + if (filter_log++ % 10 == 0) + LOG_INFO << "[think-DESIRED] filtered stale request: hash=" + << d.hash.GetHex().substr(0,16) + << " ts=" << d.max_timestamp + << " cutoff=" << timestamp_cutoff + << " age=" << (timestamp_cutoff - d.max_timestamp + 3600) << "s"; + } + } + + // Extract top-5 scored heads for clean_tracker (p2pool node.py:363) + std::vector top5; + { + size_t start = decorated_heads.size() > 5 ? decorated_heads.size() - 5 : 0; + for (size_t i = start; i < decorated_heads.size(); ++i) + top5.push_back(decorated_heads[i].hash); + } + + return {best, desired_result, bad_peer_addresses, punish_aggressively, std::move(top5)}; + } + + // -- Pool hashrate estimation -- + /// Pool hashrate estimation — matches p2pool get_pool_attempts_per_second exactly. + /// Uses skip list O(log n) for ancestor lookup + TrackerView delta cache O(1) for work sum. + /// + /// p2pool (data.py:2489-2499): + /// near = tracker.items[previous_share_hash] + /// far = tracker.items[tracker.get_nth_parent_hash(previous_share_hash, dist - 1)] + /// attempts = tracker.get_delta(near.hash, far.hash).min_work (if min_work) + /// = tracker.get_delta(near.hash, far.hash).work (otherwise) + /// time = near.timestamp - far.timestamp + /// return attempts // time (integer=True in generate_transaction) + uint288 get_pool_attempts_per_second(const uint256& share_hash, int32_t dist, bool use_min_work = false) + { + if (dist < 2 || !chain.contains(share_hash)) + return uint288(0); + + // p2pool: far = tracker.items[tracker.get_nth_parent_hash(share_hash, dist - 1)] + auto far_hash = chain.get_nth_parent_via_skip(share_hash, dist - 1); + if (far_hash.IsNull() || !chain.contains(far_hash)) + return uint288(0); + + // Verify skip list vs naive walk (periodic — detect stale pointers) + { + static int skip_verify = 0; + if (skip_verify++ % 100 == 0) { + try { + auto naive_far = chain.get_nth_parent_key(share_hash, dist - 1); + if (naive_far != far_hash) { + LOG_ERROR << "[SKIP-MISMATCH] dist=" << dist + << " skip=" << far_hash.GetHex().substr(0,16) + << " naive=" << naive_far.GetHex().substr(0,16) + << " near=" << share_hash.GetHex().substr(0,16); + } + } catch (...) {} + } + } + + // p2pool: tracker.get_delta(near.hash, far.hash) — O(1) via TrackerView + auto delta = chain.get_delta(share_hash, far_hash); + uint288 attempts = use_min_work ? delta.min_work : delta.work; + + // p2pool: time = near.timestamp - far.timestamp; if time <= 0: time = 1 + uint32_t near_ts = 0, far_ts = 0; + chain.get_share(share_hash).invoke([&](auto* obj) { near_ts = obj->m_timestamp; }); + chain.get_share(far_hash).invoke([&](auto* obj) { far_ts = obj->m_timestamp; }); + + int32_t time_span = static_cast(near_ts) - static_cast(far_ts); + if (time_span <= 0) time_span = 1; + + return attempts / uint288(time_span); + } + + // -- Share target computation -- + // Computes max_bits and bits for a new share, matching p2pool-v36 + // BaseShare.generate_transaction(): + // 1. Derive pre_target from pool hashrate estimate + // 2. Clamp to ±10% of previous share's max_target + // 3. Apply emergency time-based decay (death spiral prevention) + // 4. Clamp to [MIN_TARGET, MAX_TARGET] + // Returns {max_bits, bits}. + struct ShareTarget { + uint32_t max_bits; + uint32_t bits; + }; + + ShareTarget compute_share_target( + const uint256& prev_share_hash, + uint32_t desired_timestamp, + const uint256& desired_target) + { + // MAX_TARGET: network-specific share difficulty floor + // Mainnet: 2^236 - 1, Testnet: 2^256/20 - 1 (from PoolConfig) + const uint256 MAX_TARGET = PoolConfig::max_target(); + + if (prev_share_hash.IsNull() || !chain.contains(prev_share_hash)) + { + // Genesis or unknown prev: use MAX_TARGET for max_bits, + // clip desired_target to [MAX_TARGET/30, MAX_TARGET] for bits. + // Matches p2pool generate_transaction (data.py:746-774). + auto pre_target3 = MAX_TARGET; + auto max_bits = chain::target_to_bits_upper_bound(pre_target3); + // No round-up guard needed — truncation matches p2pool's actual behavior + uint256 bits_lo = pre_target3 / 30; + if (bits_lo.IsNull()) bits_lo = uint256(1); + uint256 bits_target = desired_target; + if (bits_target < bits_lo) bits_target = bits_lo; + if (bits_target > pre_target3) bits_target = pre_target3; + auto bits = chain::target_to_bits_upper_bound(bits_target); + // No round-up guard needed — truncation matches p2pool's actual behavior + return {max_bits, bits}; + } + + // Use accumulated height from skip list cache — O(1) and correct + // even after pruning (get_height walks until chain end, stops at + // pruned tail → returns short value → triggers MAX_TARGET fallback). + auto acc_height = chain.get_acc_height(prev_share_hash); + + // Not enough chain depth for proper difficulty calculation. + if (acc_height < static_cast(PoolConfig::TARGET_LOOKBEHIND)) + { + // Collapse detection: many shares exist but best chain is short + auto total_shares = chain.size(); + if (total_shares > 2 * PoolConfig::chain_length() + && acc_height < static_cast(PoolConfig::TARGET_LOOKBEHIND)) { + static int collapse_warn = 0; + if (collapse_warn++ < 20) { + // Walk raw chain to find actual contiguous height + int32_t walked = 0; + auto cur = prev_share_hash; + while (!cur.IsNull() && chain.contains(cur) && walked < 1000) { + ++walked; + auto* idx = chain.get_index(cur); + cur = idx ? idx->tail : uint256(); + } + LOG_WARNING << "[COLLAPSE-DETECT] Chain structurally broken:" + << " total_shares=" << total_shares + << " acc_height=" << acc_height + << " walked_height=" << walked + << " tails=" << chain.get_tails().size() + << " heads=" << chain.get_heads().size() + << " TARGET_LOOKBEHIND=" << PoolConfig::TARGET_LOOKBEHIND + << " prev=" << prev_share_hash.GetHex().substr(0,16); + } + } + auto pre_target3 = MAX_TARGET; + auto max_bits = chain::target_to_bits_upper_bound(pre_target3); + // No round-up guard needed — truncation matches p2pool's actual behavior + uint256 bits_lo = pre_target3 / 30; + if (bits_lo.IsNull()) bits_lo = uint256(1); + uint256 bits_target = desired_target; + if (bits_target < bits_lo) bits_target = bits_lo; + if (bits_target > pre_target3) bits_target = pre_target3; + auto bits = chain::target_to_bits_upper_bound(bits_target); + // No round-up guard needed — truncation matches p2pool's actual behavior + return {max_bits, bits}; + } + + // Step 1: Derive target from pool hashrate. + // Use prev_share_hash directly — all shares counted equally. + // p2pool's algorithm: aps from the entire chain, no filtering. + auto aps = get_pool_attempts_per_second(prev_share_hash, + PoolConfig::TARGET_LOOKBEHIND, /*min_work=*/true); + + // Full APS diagnostic for cross-implementation comparison. + // Dumps all inputs so p2pool's values can be compared. + { + static int cst_diag = 0; + if (cst_diag++ % 50 == 0) { + auto far_hash = chain.get_nth_parent_via_skip(prev_share_hash, + static_cast(PoolConfig::TARGET_LOOKBEHIND) - 1); + uint32_t near_ts = 0, far_ts = 0; + chain.get_share(prev_share_hash).invoke([&](auto* obj) { near_ts = obj->m_timestamp; }); + if (!far_hash.IsNull() && chain.contains(far_hash)) + chain.get_share(far_hash).invoke([&](auto* obj) { far_ts = obj->m_timestamp; }); + auto delta = (!far_hash.IsNull() && chain.contains(far_hash)) + ? chain.get_delta(prev_share_hash, far_hash) + : decltype(chain.get_delta(prev_share_hash, prev_share_hash)){}; + LOG_INFO << "[CST-APS] aps=" << aps.GetLow64() + << " height=" << acc_height + << " prev=" << prev_share_hash.GetHex().substr(0,16) + << " far=" << (far_hash.IsNull() ? "null" : far_hash.GetHex().substr(0,16)) + << " near_ts=" << near_ts << " far_ts=" << far_ts + << " timespan=" << (int32_t(near_ts) - int32_t(far_ts)) + << " delta_h=" << delta.height + << " delta_min_work=" << delta.min_work.GetLow64() + << " delta_work=" << delta.work.GetLow64(); + } + } + + uint256 pre_target; + if (aps.IsNull()) + { + pre_target = MAX_TARGET; + } + else + { + // pre_target = 2^256 / (SHARE_PERIOD * aps) - 1 + uint288 two_256; + two_256.SetHex("10000000000000000000000000000000000000000000000000000000000000000"); + uint288 divisor = aps * static_cast(PoolConfig::share_period()); + if (divisor.IsNull()) + divisor = uint288(1); + uint288 result = two_256 / divisor; + if (result > uint288(1)) + result = result - uint288(1); + // Clamp to 256-bit range + uint288 max_288; + max_288.SetHex(MAX_TARGET.GetHex()); + if (result > max_288) + { + pre_target = MAX_TARGET; + } + else + { + pre_target.SetHex(result.GetHex()); + } + } + + // Step 2: Get previous share's max_target + uint256 prev_max_target; + chain.get_share(prev_share_hash).invoke([&](auto* obj) { + prev_max_target = chain::bits_to_target(obj->m_max_bits); + }); + + // Step 3: Emergency time-based decay (death spiral prevention) + // Phase 1b from p2pool-v36: doubles target every SHARE_PERIOD * 10 + // seconds past the threshold of SHARE_PERIOD * 20 seconds since last share. + uint256 clamp_ref_target = prev_max_target; + uint32_t prev_ts = 0; + chain.get_share(prev_share_hash).invoke([&](auto* obj) { + prev_ts = obj->m_timestamp; + }); + + if (prev_ts > 0 && desired_timestamp > prev_ts) + { + auto time_since_share = desired_timestamp - prev_ts; + auto emergency_threshold = PoolConfig::share_period() * 20; + if (time_since_share > emergency_threshold) + { + auto half_life = PoolConfig::share_period() * 10; + auto excess = time_since_share - emergency_threshold; + auto halvings = excess / half_life; + auto remainder = excess % half_life; + // 2^halvings with linear interpolation for fractional part + uint256 eased = prev_max_target; + if (halvings < 256) + eased <<= halvings; + else + eased = MAX_TARGET; + // Linear interpolation: eased = eased * (half_life + remainder) / half_life + uint288 eased_288; + eased_288.SetHex(eased.GetHex()); + eased_288 = eased_288 * static_cast(half_life + remainder); + eased_288 = eased_288 / static_cast(half_life); + uint288 max_288; + max_288.SetHex(MAX_TARGET.GetHex()); + if (eased_288 > max_288) + clamp_ref_target = MAX_TARGET; + else + clamp_ref_target.SetHex(eased_288.GetHex()); + } + } + + // Step 4: Clamp pre_target to ±10% of clamp_ref_target + // pre_target2 = clip(pre_target, (clamp_ref * 9/10, clamp_ref * 11/10)) + // p2pool: target * 9 // 10 (multiply first, then divide) + uint256 lo; + { + uint288 lo_288; + lo_288.SetHex(clamp_ref_target.GetHex()); + lo_288 = lo_288 * 9 / 10; + uint288 max_288; + max_288.SetHex(MAX_TARGET.GetHex()); + if (lo_288 > max_288) + lo = MAX_TARGET; + else + lo.SetHex(lo_288.GetHex()); + } + uint256 hi; + { + uint288 hi_288; + hi_288.SetHex(clamp_ref_target.GetHex()); + hi_288 = hi_288 * 11; + hi_288 = hi_288 / 10; + uint288 max_288; + max_288.SetHex(MAX_TARGET.GetHex()); + if (hi_288 > max_288) + hi = MAX_TARGET; + else + hi.SetHex(hi_288.GetHex()); + } + + uint256 pre_target2 = pre_target; + if (pre_target2 < lo) pre_target2 = lo; + if (pre_target2 > hi) pre_target2 = hi; + + // Step 5: Clamp to network limits [MIN_TARGET, MAX_TARGET] + // Ensure target is never zero (would produce bits=0 → "share target is zero" error) + uint256 pre_target3 = pre_target2; + if (pre_target3.IsNull()) pre_target3 = uint256(1); + if (pre_target3 > MAX_TARGET) pre_target3 = MAX_TARGET; + + auto max_bits = chain::target_to_bits_upper_bound(pre_target3); + + // No round-up guard needed — truncation matches p2pool's actual behavior + + // Apply 1.67% share cap: desired_target cannot be easier than + // pool_target / 0.0167 ≈ pool_target * 60. This ensures no single + // miner can produce more than ~1.67% of all shares (anti-spam). + // P2pool ref: work.py line 2402: hashrate * SHARE_PERIOD / 0.0167 + uint256 cap_target = pre_target3; // default: pool target (1 share/period) + // cap_target represents the easiest target a single miner should use. + // At pool target difficulty, a miner with all pool hashrate produces + // 1 share/period. At pre_target3 (the pool target), that's 100%. + // desired_target is already clipped to [pre_target3//30, pre_target3] + // so the cap is inherently enforced by the upper clamp to pre_target3. + + // DUST threshold: if the pool share target is too hard for tiny miners, + // allow shares at up to 30x easier difficulty (pre_target3 * 30 would be + // out of bounds, so the 30x range [pre_target3//30, pre_target3] is the + // full allowed range — this is already how p2pool works). + // The key fix is that desired_target = MAX_TARGET (from caller), which + // gets clipped to pre_target3 here — shares at pool difficulty. + // Tiny miners simply find them less often, proportional to hashrate. + + // bits = from_target_upper_bound(clip(desired_target, (pre_target3/30, pre_target3))) + uint256 bits_lo = pre_target3 / 30; + if (bits_lo.IsNull()) bits_lo = uint256(1); + uint256 bits_target = desired_target; + if (bits_target < bits_lo) bits_target = bits_lo; + if (bits_target > pre_target3) bits_target = pre_target3; + auto bits = chain::target_to_bits_upper_bound(bits_target); + + // Periodic full-chain diagnostic for cross-implementation comparison. + // Dumps all intermediate values so p2pool's computation can be matched. + { + static int cst_full = 0; + if (cst_full++ % 200 == 0) { + LOG_INFO << "[CST-FULL] max_bits=0x" << std::hex << max_bits + << " bits=0x" << bits << std::dec + << " aps=" << aps.GetLow64() + << " pre_target_bits=0x" << std::hex + << chain::target_to_bits_upper_bound(pre_target) + << " clamp_ref_bits=0x" + << chain::target_to_bits_upper_bound(clamp_ref_target) + << " lo_bits=0x" << chain::target_to_bits_upper_bound(lo) + << " hi_bits=0x" << chain::target_to_bits_upper_bound(hi) + << " pre2_bits=0x" << chain::target_to_bits_upper_bound(pre_target2) + << " pre3_bits=0x" << chain::target_to_bits_upper_bound(pre_target3) + << std::dec + << " height=" << acc_height + << " prev=" << prev_share_hash.GetHex().substr(0,16); + } + } + return {max_bits, bits}; + } + + // -- Minimum viable hashrate for dashboard display -- + // Returns the minimum hashrate (H/s) needed to produce at least 1 share + // per PPLNS window. With DUST (30x range), tiny miners get easier shares. + struct MinerThresholds { + double min_hashrate_normal; // H/s at pool share difficulty + double min_hashrate_dust; // H/s at 30x easier (DUST) + double min_payout_ltc; // LTC per share at pool difficulty + double pool_hashrate; // current pool H/s estimate + }; + + MinerThresholds get_miner_thresholds(const uint256& prev_share_hash, + uint64_t block_subsidy_sat) + { + MinerThresholds t{}; + if (prev_share_hash.IsNull() || !chain.contains(prev_share_hash)) + return t; + + auto [height, last] = chain.get_height_and_last(prev_share_hash); + if (height < 2) return t; + + auto lookback = std::min(height, + static_cast(PoolConfig::TARGET_LOOKBEHIND)); + auto aps = get_pool_attempts_per_second(prev_share_hash, + lookback, /*min_work=*/true); + + double pool_hr = 0; + { + uint288 aps_288 = aps; + // Convert uint288 to double (approximate) + for (int i = uint288::WIDTH - 1; i >= 0; --i) { + pool_hr = pool_hr * 4294967296.0 + aps_288.pn[i]; + } + } + t.pool_hashrate = pool_hr; + + double share_period = static_cast(PoolConfig::share_period()); + double chain_length = static_cast(PoolConfig::real_chain_length()); + + // min_hashrate = pool_share_att / window = pool_hr / chain_length + t.min_hashrate_normal = pool_hr / chain_length; + t.min_hashrate_dust = t.min_hashrate_normal / 30.0; // 30x DUST range + + // min_payout = block_subsidy / chain_length (one share in full window) + t.min_payout_ltc = static_cast(block_subsidy_sat) / 1e8 / chain_length; + + return t; + } + + // -- PPLNS cumulative weights computation (O(log n) via skip list) -- + CumulativeWeights get_cumulative_weights(const uint256& start, int32_t max_shares, const uint288& desired_weight) + { + if (start.IsNull()) + return {}; + + ensure_weights_skiplist(); + auto sl_result = m_weights_skiplist->query(start, max_shares, desired_weight); + return CumulativeWeights{ + std::move(sl_result.weights), + sl_result.total_weight, + sl_result.total_donation_weight + }; + } + + // -- V36 PPLNS with exponential depth-decay -- + // Matches Python: get_decayed_cumulative_weights() + // half_life = CHAIN_LENGTH // 4 + // Each share's weight is multiplied by 2^(-depth/half_life) + // Fixed-point arithmetic with 40-bit precision. + // + // Result is cached keyed by (start_hash, max_shares) — invalidated + // when the chain head changes. This makes repeated calls for the + // same share (e.g. during verify_share + get_expected_payouts) O(1). + CumulativeWeights get_v36_decayed_cumulative_weights( + const uint256& start, int32_t max_shares, const uint288& desired_weight) + { + if (start.IsNull()) + return {}; + + // Cache: keyed by (start_hash, max_shares, desired_weight). + // Same inputs always produce same outputs (deterministic walk). + // Invalidated when chain head changes (new shares added/removed). + // No consensus risk — cached result is byte-identical to fresh computation. + if (m_decayed_cache_valid && m_decayed_cache_start == start + && m_decayed_cache_shares == max_shares + && m_decayed_cache_desired == desired_weight) + return m_decayed_cache_result; + + static constexpr uint64_t DECAY_PRECISION = 40; + static constexpr uint64_t DECAY_SCALE = uint64_t(1) << DECAY_PRECISION; + static constexpr uint64_t LN2_MICRO = 693147; + + uint32_t half_life = std::max(PoolConfig::chain_length() / 4, uint32_t(1)); + uint64_t decay_per = DECAY_SCALE - (DECAY_SCALE * LN2_MICRO) / (uint64_t(1000000) * half_life); + + CumulativeWeights result; + int32_t share_count = 0; + uint64_t decay_fp = DECAY_SCALE; // starts at 1.0 + + // Single-pass walk matching p2pool's while loop in + // get_decayed_cumulative_weights. No pre-collection needed. + auto cur = start; + while (!cur.IsNull() && chain.contains(cur) && share_count < max_shares) + { + chain.get_share(cur).invoke([&](auto* obj) { + auto att = chain::target_to_average_attempts( + chain::bits_to_target(obj->m_bits)); + uint32_t don = obj->m_donation; + + uint288 decayed_att = (att * uint288(decay_fp)) >> DECAY_PRECISION; + + auto addr_w = decayed_att * static_cast(65535 - don); + auto don_w = decayed_att * don; + auto this_total = addr_w + don_w; // = decayed_att * 65535 + + if (result.total_weight + this_total > desired_weight) { + auto remaining = desired_weight - result.total_weight; + if (!this_total.IsNull()) { + addr_w = addr_w * remaining / this_total; + don_w = don_w * remaining / this_total; + } + this_total = remaining; + } + + auto script = get_share_script(obj); + result.weights[script] += addr_w; + result.total_weight += this_total; + result.total_donation_weight += don_w; + }); + + ++share_count; + if (result.total_weight >= desired_weight) + break; + + decay_fp = mul128_shift(decay_fp, decay_per, DECAY_PRECISION); + + auto* idx = chain.get_index(cur); + cur = idx ? idx->tail : uint256(); + } + + // Cache result (single-entry, invalidated on chain change) + m_decayed_cache_start = start; + m_decayed_cache_shares = max_shares; + m_decayed_cache_desired = desired_weight; + m_decayed_cache_result = result; + m_decayed_cache_valid = true; + + return result; + } + + // -- Diagnostic: per-share V36 PPLNS walk dump -- + // Walks from start_hash backward, logging every share's contribution. + // Output matches p2pool's [PARENT-PPLNS] stderr format for diff comparison. + // Call only from GENTX mismatch handler — never on the hot path. + void dump_v36_pplns_walk(const uint256& start_hash, int32_t max_shares) + { + if (start_hash.IsNull() || !chain.contains(start_hash)) + { + LOG_WARNING << "[PPLNS-WALK] start=" << start_hash.GetHex().substr(0,16) + << " NOT IN CHAIN — walk aborted"; + return; + } + + static constexpr uint64_t DECAY_PRECISION = 40; + static constexpr uint64_t DECAY_SCALE = uint64_t(1) << DECAY_PRECISION; + static constexpr uint64_t LN2_MICRO = 693147; + + uint32_t half_life = std::max(PoolConfig::chain_length() / 4, uint32_t(1)); + uint64_t decay_per = DECAY_SCALE - (DECAY_SCALE * LN2_MICRO) / (uint64_t(1000000) * half_life); + + int32_t share_count = 0; + uint64_t decay_fp = DECAY_SCALE; + uint288 running_total; + uint288 running_donation; + std::map, uint288> per_addr_weight; + + auto height = chain.get_height(start_hash); + auto last = chain.get_last(start_hash); + + LOG_WARNING << "[PPLNS-WALK] start=" << start_hash.GetHex().substr(0, 16) + << " max_shares=" << max_shares + << " height=" << height + << " last=" << (last.IsNull() ? "null" : last.GetHex().substr(0, 16)) + << " half_life=" << half_life + << " decay_per=" << decay_per; + + auto cur = start_hash; + while (!cur.IsNull() && chain.contains(cur) && share_count < max_shares) + { + chain.get_share(cur).invoke([&](auto* obj) { + auto target = chain::bits_to_target(obj->m_bits); + auto att = chain::target_to_average_attempts(target); + uint32_t don = obj->m_donation; + + uint288 decayed_att = (att * uint288(decay_fp)) >> DECAY_PRECISION; + auto addr_w = decayed_att * static_cast(65535 - don); + auto don_w = decayed_att * don; + + auto script = get_share_script(obj); + per_addr_weight[script] += addr_w; + running_total += addr_w + don_w; + running_donation += don_w; + + // Script hex prefix (first 10 bytes, like p2pool's key.encode('hex')[:40]) + static const char* HX = "0123456789abcdef"; + std::string sh; + for (size_t i = 0; i < std::min(script.size(), size_t(20)); ++i) { + sh += HX[script[i] >> 4]; + sh += HX[script[i] & 0xf]; + } + + LOG_WARNING << "[PPLNS-WALK] #" << share_count + << " hash=" << cur.GetHex().substr(0, 16) + << " script=" << sh + << " bits=0x" << std::hex << obj->m_bits << std::dec + << " don=" << don + << " att=" << att.GetLow64() + << " decay_fp=" << decay_fp + << " decayed=" << decayed_att.GetLow64() + << " addr_w=" << addr_w.GetLow64() + << " running=" << running_total.GetLow64(); + }); + + ++share_count; + + decay_fp = mul128_shift(decay_fp, decay_per, DECAY_PRECISION); + auto* idx = chain.get_index(cur); + cur = idx ? idx->tail : uint256(); + } + + // Summary matching p2pool's [PARENT-PPLNS] format + LOG_WARNING << "[PPLNS-WALK] SUMMARY: shares=" << share_count + << " addrs=" << per_addr_weight.size() + << " total_w=" << running_total.GetLow64() + << " don_w=" << running_donation.GetLow64(); + for (const auto& [script, weight] : per_addr_weight) + { + static const char* HX = "0123456789abcdef"; + std::string sh; + for (size_t i = 0; i < std::min(script.size(), size_t(20)); ++i) { + sh += HX[script[i] >> 4]; + sh += HX[script[i] & 0xf]; + } + double pct = running_total.IsNull() ? 0.0 : + static_cast(weight.GetLow64()) / static_cast(running_total.GetLow64()) * 100.0; + LOG_WARNING << "[PPLNS-WALK] " << sh + << " w=" << weight.GetLow64() + << " pct=" << std::fixed << std::setprecision(2) << pct << "%"; + } + + // Chain gap detection: check if walk terminated early (not at chain bottom) + if (share_count < max_shares && !cur.IsNull()) { + LOG_WARNING << "[PPLNS-WALK] CHAIN GAP: walk stopped at share #" << share_count + << " — next hash " << cur.GetHex().substr(0, 16) + << " is " << (chain.contains(cur) ? "IN chain (walk bug)" : "NOT IN chain (missing share)") + << ". Expected " << max_shares << " shares."; + } + } + + // -- Expected payouts from PPLNS weights -- + // Uses exact integer arithmetic matching generate_share_transaction(): + // V36: amount = (uint288(subsidy) * weight / total_weight).GetLow64() + // donation = subsidy - sum(amounts) + std::map, double> + get_expected_payouts(const uint256& best_share_hash, const uint256& block_target, uint64_t subsidy, + const std::vector& donation_script) + { + // Pass REAL_CHAIN_LENGTH as max_shares — the walk naturally stops + // when the chain ends, matching p2pool's direct iteration pattern. + // Do NOT use cached get_height() which can be stale in multi-threaded context. + auto chain_len = static_cast(PoolConfig::real_chain_length()); + // V36: remove desired_weight cap — exponential decay handles windowing. + // See generate_share_transaction() for detailed rationale. + uint288 unlimited_weight; + unlimited_weight.SetHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + { + static int ep_log = 0; + if (ep_log++ % 20 == 0) { + LOG_DEBUG_DIAG << "[EP-PPLNS] v36 start=" << best_share_hash.GetHex().substr(0, 16) + << " chain_len=" << chain_len + << " subsidy=" << subsidy; + } + } + auto [weights, total_weight, donation_weight] = get_v36_decayed_cumulative_weights(best_share_hash, chain_len, unlimited_weight); + + std::map, double> result; + uint64_t sum = 0; + + if (!total_weight.IsNull()) + { + for (const auto& [script, weight] : weights) + { + // Exact integer division matching generate_share_transaction (V36) + uint64_t amount = (uint288(subsidy) * weight / total_weight).GetLow64(); + if (amount > 0) + { + result[script] = static_cast(amount); + sum += amount; + } + } + } + + // Remainder goes to donation (matches generate_share_transaction) + uint64_t donation_amount = (subsidy > sum) ? (subsidy - sum) : 0; + + // V36 consensus: donation output must carry >= 1 satoshi (a60f7f7f) + if (donation_amount < 1 && subsidy > 0 && !result.empty()) { + // Deterministic tiebreak: (amount, script) — largest script wins when equal + auto largest = std::max_element(result.begin(), result.end(), + [](const auto& a, const auto& b) { + if (a.second != b.second) return a.second < b.second; + return a.first < b.first; + }); + if (largest != result.end() && largest->second >= 1.0) { + largest->second -= 1.0; + sum -= 1; + donation_amount = subsidy - sum; + } + } + + result[donation_script] = (result.contains(donation_script) ? result[donation_script] : 0.0) + + static_cast(donation_amount); + + return result; + } + + // -- V35 PPLNS expected payouts -- + // Flat (non-decayed) weights, GRANDPARENT start, height-1 window. + // Returns amounts WITHOUT finder fee — caller adds subsidy/200 to the + // share creator's script. Donation absorbs the remainder. + // Reference: p2pool data.py lines 878-965 + std::map, double> + get_v35_expected_payouts(const uint256& best_share_hash, const uint256& block_target, uint64_t subsidy, + const std::vector& donation_script) + { + // V35: PPLNS starts from the GRANDPARENT (prev_share.prev_hash) + // Reference: data.py line 884 + uint256 pplns_start; + if (!best_share_hash.IsNull() && chain.contains(best_share_hash)) { + chain.get(best_share_hash).share.invoke([&](auto* s) { + pplns_start = s->m_prev_hash; // grandparent + }); + } + + if (pplns_start.IsNull()) { + // No grandparent: all subsidy to donation + std::map, double> result; + result[donation_script] = static_cast(subsidy); + return result; + } + + // V35: max_shares = max(0, min(height, REAL_CHAIN_LENGTH) - 1) + // Reference: data.py line 885 + auto height = chain.get_height(best_share_hash); + int32_t max_shares = std::max(0, std::min(height, static_cast(PoolConfig::real_chain_length())) - 1); + + // V35: desired_weight = 65535 * SPREAD * target_to_average_attempts(block_target) + // Reference: data.py line 886 + uint288 desired_weight = chain::target_to_average_attempts(block_target) + * uint288(PoolConfig::SPREAD) * uint288(65535); + + { + static int ep35_log = 0; + if (ep35_log++ % 20 == 0) { + LOG_DEBUG_DIAG << "[EP-PPLNS] v35 start=" << pplns_start.GetHex().substr(0, 16) + << " max_shares=" << max_shares + << " desired_w=" << desired_weight.GetLow64() + << " subsidy=" << subsidy + << " best=" << best_share_hash.GetHex().substr(0, 16); + } + } + // Flat weight accumulation with hard cap (existing get_cumulative_weights) + auto [weights, total_weight, donation_weight] = get_cumulative_weights(pplns_start, max_shares, desired_weight); + + std::map, double> result; + uint64_t sum = 0; + + if (!total_weight.IsNull()) + { + for (const auto& [script, weight] : weights) + { + // V35: 99.5% to PPLNS — subsidy * 199 * weight / (200 * total_weight) + // Reference: data.py line 924 + uint64_t amount = (uint288(subsidy) * uint288(199) * weight / (uint288(200) * total_weight)).GetLow64(); + if (amount > 0) + { + result[script] = static_cast(amount); + sum += amount; + } + } + } + + // Remainder goes to donation (includes the ~0.5% finder fee portion; + // caller subtracts subsidy/200 for the finder and assigns it per-connection) + // V35: NO minimum donation enforcement (unlike v36) + uint64_t donation_amount = (subsidy > sum) ? (subsidy - sum) : 0; + result[donation_script] = (result.contains(donation_script) ? result[donation_script] : 0.0) + + static_cast(donation_amount); + + // Periodic diagnostic dump for cross-impl comparison + { + static int v35_dump = 0; + if (v35_dump++ < 10 || v35_dump % 60 == 0) { + LOG_DEBUG_DIAG << "[V35-PPLNS] subsidy=" << subsidy << " addrs=" << weights.size() + << " total_w=" << total_weight.GetLow64() + << " max_shares=" << max_shares << " sum=" << sum + << " donation=" << donation_amount + << " prev=" << best_share_hash.GetHex().substr(0, 16) + << " grandparent=" << pplns_start.GetHex().substr(0, 16); + } + } + + return result; + } + + // -- Stale share proportion -- + float get_average_stale_prop(const uint256& share_hash, uint64_t lookbehind) + { + auto height = chain.get_height(share_hash); + auto actual_lookbehind = std::min(static_cast(lookbehind), height); + if (actual_lookbehind <= 0) + return 0.0f; + + float stale_count = 0; + auto view = chain.get_chain(share_hash, actual_lookbehind); + for (auto [hash, data] : view) + { + StaleInfo si = StaleInfo::none; + data.share.invoke([&](auto* obj) { si = obj->m_stale_info; }); + if (si != StaleInfo::none) + stale_count += 1.0f; + } + + return stale_count / (stale_count + static_cast(actual_lookbehind)); + } + + // -- Stale share counts by type -- + StaleCounts get_stale_counts(const uint256& share_hash, uint64_t lookbehind) + { + StaleCounts counts; + auto height = chain.get_height(share_hash); + auto actual_lookbehind = std::min(static_cast(lookbehind), height); + if (actual_lookbehind <= 0) + return counts; + + auto view = chain.get_chain(share_hash, actual_lookbehind); + for (auto [hash, data] : view) + { + StaleInfo si = StaleInfo::none; + data.share.invoke([&](auto* obj) { si = obj->m_stale_info; }); + if (si == StaleInfo::orphan) + counts.orphan_count++; + else if (si == StaleInfo::doa) + counts.doa_count++; + } + counts.total = counts.orphan_count + counts.doa_count; + return counts; + } + + // -- Stale change callback registration -- + using stale_callback_t = std::function; + + void subscribe_stale_change(stale_callback_t cb) + { + m_stale_callbacks.push_back(std::move(cb)); + } + + void notify_stale_change(const uint256& share_hash, StaleInfo info) + { + for (auto& cb : m_stale_callbacks) + cb(share_hash, info); + } + + // -- Version counting for AutoRatchet upgrade coordination -- + // Walks back `lookbehind` shares from `share_hash` and counts + // how many desire each version. Returns map of version → count. + // Python ref: tracker.get_desired_version_counts(...) + std::map get_desired_version_counts(const uint256& share_hash, int32_t lookbehind) + { + std::map counts; + if (!chain.contains(share_hash)) + return counts; + auto height = chain.get_height(share_hash); + auto actual = std::min(lookbehind, height); + if (actual <= 0) + return counts; + + auto view = chain.get_chain(share_hash, actual); + for (auto [hash, data] : view) + { + uint64_t dv = 0; + data.share.invoke([&](auto* obj) { dv = obj->m_desired_version; }); + counts[dv]++; + } + return counts; + } + + // -- PPLNS-weighted version counting for the consensus 60% switch rule -- + // Canonical p2pool get_desired_version_counts (data.py:2651) weights each + // share by target_to_average_attempts(share.target) — NOT a flat count. + // The check()-phase 60% switch rule (share_check step 2) is a consensus gate + // and MUST use these weights to stay byte-identical with p2pool. AutoRatchet + // and its tail guard deliberately keep the FLAT-COUNT variant above + // (count-based per F10 finding #1 — weighting them would shift activation + // timing across the soak). Weight = ShareIndex::work (share.hpp). + std::map get_desired_version_weights(const uint256& share_hash, int32_t lookbehind) + { + std::map weights; + if (!chain.contains(share_hash)) + return weights; + auto height = chain.get_height(share_hash); + auto actual = std::min(lookbehind, height); + if (actual <= 0) + return weights; + + auto view = chain.get_chain(share_hash, actual); + for (auto [hash, data] : view) + { + uint64_t dv = 0; + data.share.invoke([&](auto* obj) { dv = obj->m_desired_version; }); + auto* idx = chain.get_index(hash); + if (idx) + weights[dv] = weights[dv] + idx->work; + } + return weights; + } + + // -- Merged mining: per-chain PPLNS weights -- + // For a specific aux chain_id, walk the share chain and accumulate PPLNS + // weights for V36-signaling shares. Uses O(log n) skip list. + CumulativeWeights get_merged_cumulative_weights( + const uint256& start, int32_t max_shares, + const uint288& desired_weight, uint32_t target_chain_id) + { + if (start.IsNull()) + return {}; + + auto& sl = ensure_merged_skiplist(target_chain_id); + auto result = sl.query(start, max_shares, desired_weight); + + // [DOGE-PPLNS] Per-address breakdown — shows whether autoconverted + // addresses appear in merged PPLNS weights. Log once per new height. + { + static int32_t s_last_doge_height = -1; + auto h = chain.get_height(start); + if (h != s_last_doge_height) { + s_last_doge_height = h; + auto to_dec = [](const uint288& val) -> std::string { + if (val.IsNull()) return "0"; + uint288 tmp = val; std::string r; + while (!tmp.IsNull()) { + uint32_t rem = 0; + for (int i = uint288::WIDTH - 1; i >= 0; --i) { + uint64_t cur = (static_cast(rem) << 32) | tmp.pn[i]; + tmp.pn[i] = static_cast(cur / 10); + rem = static_cast(cur % 10); + } + r.push_back('0' + static_cast(rem)); + } + std::reverse(r.begin(), r.end()); + return r; + }; + auto to_hex = [](const std::vector& s) -> std::string { + static const char* H = "0123456789abcdef"; + std::string r; r.reserve(s.size() * 2); + for (auto b : s) { r += H[b >> 4]; r += H[b & 0xf]; } + return r; + }; + // Classify script type for diagnostics + auto classify = [](const std::vector& s) -> const char* { + if (is_merged_key(s)) return "MERGED"; + if (s.size() == 25 && s[0] == 0x76 && s[1] == 0xa9) return "P2PKH"; + if (s.size() == 23 && s[0] == 0xa9) return "P2SH"; + if (s.size() == 22 && s[0] == 0x00 && s[1] == 0x14) return "P2WPKH-RAW"; + if (s.size() == 34 && s[0] == 0x00 && s[1] == 0x20) return "P2WSH-RAW"; + if (s.size() == 34 && s[0] == 0x51 && s[1] == 0x20) return "P2TR-RAW"; + return "OTHER"; + }; + LOG_INFO << "[DOGE-PPLNS] chain_id=" << target_chain_id + << " height=" << h + << " max_shares=" << max_shares + << " addrs=" << result.weights.size() + << " total_w=" << to_dec(result.total_weight) + << " don_w=" << to_dec(result.total_donation_weight) + << " start=" << start.GetHex().substr(0, 16); + for (const auto& [key, w] : result.weights) { + double pct = 0; + if (!result.total_weight.IsNull()) + pct = (w * 10000 / result.total_weight).GetLow64() / 100.0; + auto hex = to_hex(key); + LOG_INFO << "[DOGE-PPLNS] " << classify(key) + << " " << hex.substr(0, 40) + << " w=" << to_dec(w) + << " pct=" << std::fixed << std::setprecision(2) << pct << "%"; + } + } + } + + return {std::move(result.weights), result.total_weight, result.total_donation_weight}; + } + + // -- V36-only unified merged weights (no chain_id) -- + // Accumulates PPLNS weights for V36-signaling shares ONLY, keyed by + // parent chain address. Uses O(log n) skip list. + CumulativeWeights get_v36_merged_weights( + const uint256& start, int32_t max_shares, const uint288& desired_weight) + { + if (start.IsNull()) + return {}; + + ensure_v36_skiplist(); + auto result = m_v36_weights_skiplist->query(start, max_shares, desired_weight); + return {std::move(result.weights), result.total_weight, result.total_donation_weight}; + } + + // -- merged_weights_delta (F2: single source of truth) -- + // Unified per-share merged-mining PPLNS weight delta. Replaces the three + // formerly-divergent variants (the compute_merged_payout_hash inline walk, + // ensure_v36_skiplist, and per-chain ensure_merged_skiplist). Mirrors + // p2pool MergedWeightsSkipList.get_delta (data.py:1864). + // + // chain_id == nullopt : hash / v36 path — no merged-address resolution. + // chain_id has value : per-chain path — Tier 1 explicit merged_addresses, + // Tier 1.5 retroactive miner→merged lookup (normalize + // used only as a lookup PROBE), else the default key. + // + // Pre-V36 shares count toward window sizing but contribute zero weight + // (p2pool returns (1, {}, 0, 0)); total_weight/donation are only set once a + // weight key resolves, so unconvertible scripts never inflate the denominator. + template + chain::WeightsDelta merged_weights_delta(ShareT* obj, std::optional chain_id) + { + chain::WeightsDelta delta; + delta.share_count = 1; + if (obj->m_desired_version < 36) + return delta; + + auto target = chain::bits_to_target(obj->m_bits); + auto att = chain::target_to_average_attempts(target); + auto parent_script = get_share_script(obj); + if (parent_script.empty()) + return delta; + + std::vector weight_key; + // Tier 1 / 1.5 merged-address resolution — only for chain-keyed skiplists + // whose share type carries explicit merged_addresses. + if (chain_id.has_value()) + { + if constexpr (requires { obj->m_merged_addresses; }) + { + // Tier 1: explicit merged_addresses for this chain. + for (const auto& entry : obj->m_merged_addresses) + { + if (entry.m_chain_id == *chain_id) + { + weight_key = make_merged_key(entry.m_script.m_data); + break; + } + } + // Tier 1.5: retroactive lookup of the same miner's explicit merged + // address from their other shares. Try the raw parent script, then + // its normalized P2PKH form (normalize is a lookup PROBE only here). + if (weight_key.empty()) + { + auto table_it = m_miner_merged_addr.find(*chain_id); + if (table_it != m_miner_merged_addr.end()) + { + auto miner_it = table_it->second.find(parent_script); + if (miner_it == table_it->second.end()) + { + auto norm = normalize_script_for_merged(parent_script); + if (!norm.empty() && norm != parent_script) + miner_it = table_it->second.find(norm); + } + if (miner_it != table_it->second.end()) + weight_key = make_merged_key(miner_it->second); + } + } + } + } + + // Default key: RAW parent script, matching p2pool's + // address_key = share.new_script. normalize_script_for_merged stays a + // Tier-1.5 lookup PROBE only (above); keying emission by the normalized + // P2PKH form was the merged-payout accounting divergence (F1, Option A). + if (weight_key.empty()) + weight_key = parent_script; + if (weight_key.empty()) + return delta; + + delta.total_weight = att * 65535; + delta.total_donation_weight = att * static_cast(obj->m_donation); + // [v36-audit C2] INTENTIONAL DIVERGENCE: merged-chain weights are FLAT + // (att-weighted, no time-decay). Only the parent/LTC chain applies decay. + // Mirrors p2pool get_v36_merged_weights (flat + cap, data.py:2657) vs the + // parent decayed-cumulative skiplist. Verified intentional, not a bug. + delta.weights[weight_key] = att * static_cast(65535 - obj->m_donation); + return delta; + } + + // Per-hash wrapper: the boilerplate shared by every merged-weight skiplist + // lambda — bounds-check the raw chain, then dispatch to merged_weights_delta. + chain::WeightsDelta merged_weights_delta_for_hash( + const uint256& hash, std::optional chain_id) + { + chain::WeightsDelta delta; + if (!chain.contains(hash)) return delta; + chain.get_share(hash).invoke([&](auto* obj) { + delta = merged_weights_delta(obj, chain_id); + }); + return delta; + } + + // -- compute_merged_payout_hash -- + // Deterministic hash of V36-only PPLNS weight distribution. + // Committed into V36 shares so peers can verify that the share creator's + // merged mining payouts match the expected distribution. + // + // Format: sorted "addr_hex:weight|...|T:total|D:donation" → SHA256d + // Returns zero uint256 if no V36 shares in window. + // + // Python ref: p2pool/data.py compute_merged_payout_hash() + uint256 compute_merged_payout_hash( + const uint256& prev_share_hash, const uint256& block_target) + { + if (prev_share_hash.IsNull()) + return uint256{}; + + // Use RAW chain — matches p2pool's compute_merged_payout_hash() + // which uses tracker (raw), not tracker.verified. + // Using verified chain caused zero returns when prev_share wasn't + // yet verified → ref_hash mismatch → GENTX-FAIL. + if (!chain.contains(prev_share_hash)) + return uint256{}; + + auto height = chain.get_height(prev_share_hash); + if (height == 0) + return uint256{}; + + // No chain depth guard — p2pool computes merged_payout_hash for ANY + // height > 0 using chain_length = min(height, REAL_CHAIN_LENGTH). + // The previous guard (height < CHAIN_LENGTH → return zero) caused + // ref_hash mismatch because p2pool computed a real hash while c2pool + // returned zero. + + // Unlimited desired_weight — V36 exponential decay handles windowing. + uint288 unlimited_weight; + unlimited_weight.SetHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + auto chain_len = std::min(height, + static_cast(PoolConfig::real_chain_length())); + + // Walk RAW chain (not verified) — matches p2pool's compute_merged_payout_hash + // which uses tracker (raw). Using verified chain causes hash mismatch when + // the verified chain is shorter (during sync or with missing ancestors). + auto raw_prev_fn = [this](const uint256& hash) -> uint256 { + if (!chain.contains(hash)) return uint256{}; + return chain.get_index(hash)->tail; + }; + chain::WeightsSkipList raw_sl( + [this](const uint256& hash) -> chain::WeightsDelta { + // F2: unified merged-weight delta, hash/v36 path (no chain_id). + return merged_weights_delta_for_hash(hash, std::nullopt); + }, + std::move(raw_prev_fn) + ); + auto result = raw_sl.query(prev_share_hash, chain_len, unlimited_weight); + auto weights = std::move(result.weights); + auto total_weight = result.total_weight; + auto donation_weight = result.total_donation_weight; + + if (weights.empty() || total_weight.IsNull()) + return uint256{}; + + // Convert uint288 to decimal string, matching Python's '%d' formatting + auto to_decimal = [](const uint288& val) -> std::string { + if (val.IsNull()) return "0"; + uint288 tmp = val; + std::string result; + while (!tmp.IsNull()) { + uint32_t rem = 0; + for (int i = uint288::WIDTH - 1; i >= 0; --i) { + uint64_t cur = (static_cast(rem) << 32) | tmp.pn[i]; + tmp.pn[i] = static_cast(cur / 10); + rem = static_cast(cur % 10); + } + result.push_back('0' + static_cast(rem)); + } + std::reverse(result.begin(), result.end()); + return result; + }; + + // Convert script bytes to hex string for deterministic serialization. + // V36 consensus: sort and serialize by script hex (not address string). + // Matches p2pool's compute_merged_payout_hash() which uses script.encode('hex'). + auto script_to_hex = [](const std::vector& script) -> std::string { + static const char digits[] = "0123456789abcdef"; + std::string hex; + hex.reserve(script.size() * 2); + for (unsigned char c : script) { + hex.push_back(digits[c >> 4]); + hex.push_back(digits[c & 0xf]); + } + return hex; + }; + + // Deterministic serialization: sorted by script hex (V36 consensus) + // Format: "script_hex1:weight1|script_hex2:weight2|...|T:total|D:donation" + std::map sorted_by_script; + for (const auto& [script, w] : weights) + sorted_by_script[script_to_hex(script)] += w; + + std::string payload; + for (const auto& [script_hex, w] : sorted_by_script) + { + if (!payload.empty()) + payload.push_back('|'); + payload += script_hex; + payload.push_back(':'); + payload += to_decimal(w); + } + // Append total and donation + payload += "|T:"; + payload += to_decimal(total_weight); + payload += "|D:"; + payload += to_decimal(donation_weight); + + // SHA256d (hash256 in p2pool) + auto span = std::span( + reinterpret_cast(payload.data()), payload.size()); + auto hash_result = Hash(span); + + // Per-address merged PPLNS breakdown — log once per new chain height + // for death valley comparison between p2pool and c2pool. + { + static int32_t s_last_log_height = -1; + if (height != s_last_log_height) { + s_last_log_height = height; + LOG_INFO << "[MERGED-PPLNS] height=" << height + << " chain_len=" << chain_len + << " addrs=" << sorted_by_script.size() + << " total_w=" << to_decimal(total_weight) + << " don_w=" << to_decimal(donation_weight); + for (const auto& [script_hex, w] : sorted_by_script) { + double pct = 0; + if (!total_weight.IsNull()) { + pct = (w * 10000 / total_weight).GetLow64() / 100.0; + } + LOG_INFO << "[MERGED-PPLNS] " << script_hex.substr(0, 40) + << " w=" << to_decimal(w) + << " pct=" << std::fixed << std::setprecision(2) << pct << "%"; + } + LOG_INFO << "[MERGED-PPLNS] hash=" << hash_result.GetHex(); + + // DOGE payout weights (chain_id=98, with Tier 1/1.5/2 resolution) + // This is what actually determines DOGE coinbase outputs. + // During death valley, compare address count and keys vs [MERGED-PPLNS]. + constexpr uint32_t DOGE_CHAIN_ID = 98; + uint288 doge_unlimited; + doge_unlimited.SetHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + auto doge_result = get_merged_cumulative_weights( + prev_share_hash, chain_len, doge_unlimited, DOGE_CHAIN_ID); + auto classify_doge = [](const std::vector& s) -> const char* { + if (is_merged_key(s)) return "MERGED"; + if (s.size() == 25 && s[0] == 0x76 && s[1] == 0xa9) return "P2PKH"; + if (s.size() == 23 && s[0] == 0xa9) return "P2SH"; + if (s.size() == 22 && s[0] == 0x00 && s[1] == 0x14) return "P2WPKH-RAW"; + if (s.size() == 34 && s[0] == 0x00 && s[1] == 0x20) return "P2WSH-RAW"; + if (s.size() == 34 && s[0] == 0x51 && s[1] == 0x20) return "P2TR-RAW"; + return "OTHER"; + }; + auto doge_miner_w = doge_result.total_weight - doge_result.total_donation_weight; + LOG_INFO << "[DOGE-PAYOUT] height=" << height + << " addrs=" << doge_result.weights.size() + << " total_w=" << to_decimal(doge_result.total_weight) + << " don_w=" << to_decimal(doge_result.total_donation_weight); + for (const auto& [script, w] : doge_result.weights) { + double pct = 0; + if (!doge_miner_w.IsNull()) + pct = (w * 10000 / doge_miner_w).GetLow64() / 100.0; + LOG_INFO << "[DOGE-PAYOUT] " << classify_doge(script) + << " " << script_to_hex(script).substr(0, 50) + << " w=" << to_decimal(w) + << " pct=" << std::fixed << std::setprecision(2) << pct << "%"; + } + } + } + + return hash_result; + } + + // -- Merged mining: per-chain expected payouts -- + // Given an aux chain's subsidy and chain_id, computes the expected payout + // distribution using merged PPLNS weights. + // Uses INTEGER arithmetic matching p2pool's build_canonical_merged_coinbase + // exactly — no floating point anywhere. + // + // p2pool algorithm: + // grand_total = total_weight (already includes donation_weight) + // donation_amount = coinbase_value * donation_weight // grand_total + // miners_reward = coinbase_value - donation_amount + // accepted_total = sum of convertible miner weights (== total_weight - donation_weight here) + // per_miner = miners_reward * w // accepted_total + // rounding_remainder = miners_reward - sum(per_miner) + // final_donation = donation_amount + rounding_remainder + // [v36-audit C4] NODE-LOCAL / NON-CONSENSUS. Despite the name, this derives + // THIS node's intended DOGE aux-coinbase payout split from PPLNS weights to + // build the block submitted to dogecoind. Sole caller = the MM payout_provider + // (c2pool_refactored.cpp); never invoked from share verification. Peers validate + // the merged commitment via the AuxPoW merkle proof, NOT by re-deriving these + // outputs, so output ordering is node-local (dogecoind accepts any valid order). + std::map, uint64_t> + get_merged_expected_payouts(const uint256& best_share_hash, + const uint256& block_target, + uint64_t subsidy, + uint32_t chain_id, + const std::vector& donation_script, + const std::vector& operator_ltc_script = {}, + const std::vector& operator_merged_script = {}) + { + auto chain_len = std::min(chain.get_height(best_share_hash), + static_cast(PoolConfig::real_chain_length())); + // Unlimited desired_weight — exponential decay handles windowing. + uint288 unlimited_weight; + unlimited_weight.SetHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + + auto [weights, total_weight, donation_weight] = + get_merged_cumulative_weights(best_share_hash, chain_len, unlimited_weight, chain_id); + + std::map, uint64_t> result; + + if (total_weight.IsNull() || subsidy == 0) + return result; + + // Integer division matching p2pool exactly (using uint288 throughout + // to avoid uint64 truncation — total_weight routinely exceeds 2^64): + // donation_amount = subsidy * donation_weight // total_weight + uint288 subsidy288(subsidy); + uint64_t donation_amount = 0; + if (!donation_weight.IsNull()) { + donation_amount = (subsidy288 * donation_weight / total_weight).GetLow64(); + } + + // finder_fee = 0 (CANONICAL_MERGED_FINDER_FEE_PER_MILLE = 0 in p2pool) + uint64_t miners_reward = subsidy - donation_amount; + + // p2pool data.py:220-269: accepted_total_weight is the sum of ONLY + // convertible weights (after filtering unconvertible P2WSH/P2TR). + // NOT total_weight - donation_weight (which includes unconvertible). + // First pass: resolve scripts and compute accepted_total + struct ResolvedEntry { + std::vector script; + uint288 weight; + }; + std::vector resolved; + uint288 accepted_total; + for (const auto& [key, weight] : weights) { + std::vector script; + if (!operator_ltc_script.empty() && !operator_merged_script.empty() && key == operator_ltc_script) { + script = operator_merged_script; + } else { + script = resolve_merged_payout_script(key); + } + if (script.empty()) continue; // Unconvertible (P2WSH, P2TR) — skip + resolved.push_back({std::move(script), weight}); + accepted_total = accepted_total + weight; + } + + // Second pass: distribute miners_reward proportionally (uint288 arithmetic) + uint64_t total_distributed = 0; + uint288 miners_reward288(miners_reward); + for (const auto& entry : resolved) { + uint64_t amount = !accepted_total.IsNull() + ? (miners_reward288 * entry.weight / accepted_total).GetLow64() + : 0; + if (amount > 0) { + result[entry.script] += amount; + total_distributed += amount; + } + } + + // Rounding remainder → donation (integer division truncates) + // Guard against uint64 underflow: total_distributed can exceed miners_reward + // when per-miner rounding accumulates beyond the pool (rare with many miners). + uint64_t remainder = (total_distributed <= miners_reward) ? (miners_reward - total_distributed) : 0; + uint64_t final_donation = donation_amount + remainder; + + // V36 CONSENSUS RULE: Donation must be >= 1 satoshi + if (final_donation < 1 && static_cast(subsidy) > 0 && !result.empty()) { + // Deduct from largest miner (deterministic tiebreak by script) + auto largest = std::max_element(result.begin(), result.end(), + [](const auto& a, const auto& b) { + if (a.second != b.second) return a.second < b.second; + return a.first < b.first; + }); + if (largest != result.end()) { + largest->second -= 1; + final_donation += 1; + } + } + + result[donation_script] = (result.contains(donation_script) ? result[donation_script] : 0ULL) + + static_cast(final_donation); + + return result; + } + + // F10/(b): should_punish_version (the 95%-obsolescence punish) was removed. + // It was non-canonical — canonical p2pool check() has no 95% obsolescence + // rule; the count-based AutoRatchet plus the 60% weighted switch rule + // (share_check step 2) are the only version gates. Keeping it would + // punish/score-down shares canonical accepts, breaking the #81 zero- + // divergence gate. Head-scoring (Phase 4) now punishes only naughty heads. + +private: + std::vector m_stale_callbacks; + + // -- Skip list caches for O(log n) weight queries -- + std::optional m_weights_skiplist; + std::optional m_v36_weights_skiplist; + std::unordered_map m_merged_skiplists; + + // -- Retroactive merged address lookup table -- + // Maps (chain_id) → (parent_script → explicit_merged_script). + // Populated incrementally as V36 shares with explicit merged_addresses + // are added. Used as "Tier 1.5" in the merged skip list lambda: + // when a V36 share has empty merged_addresses (activation-boundary race), + // look up the same miner's explicit address from their other shares. + std::unordered_map, std::vector>> m_miner_merged_addr; + + // Register explicit merged addresses from a share into the lookup table. + // If a NEW miner→address mapping is discovered, invalidates the merged + // skip list for that chain_id so affected shares get recomputed. + template + void try_register_merged_addr(ShareT* share) + { + if constexpr (requires { share->m_merged_addresses; }) + { + if (share->m_desired_version < 36) return; + if (share->m_merged_addresses.empty()) return; + auto parent_script = get_share_script(share); + if (parent_script.empty()) return; + for (const auto& entry : share->m_merged_addresses) + { + if (entry.m_script.m_data.empty()) continue; + auto& lookup = m_miner_merged_addr[entry.m_chain_id]; + bool any_new = false; + // Register under raw script (primary key) + // p2pool v0.14.5: lookup[item.new_script] = script + auto [it, inserted] = lookup.emplace(parent_script, entry.m_script.m_data); + if (inserted) any_new = true; + // Also register under normalized P2PKH form so + // Tier 1.5 catches shares with different script encoding. + // p2pool v0.14.5: lookup[normalized] = script + auto normalized = normalize_script_for_merged(parent_script); + if (!normalized.empty() && normalized != parent_script) { + auto [it2, ins2] = lookup.emplace(normalized, entry.m_script.m_data); + if (ins2) any_new = true; + } + if (any_new) + { + // New mapping — stale skip list entries used auto-convert + // for this miner; recreate to use the explicit address. + m_merged_skiplists.erase(entry.m_chain_id); + auto to_hex_short = [](const std::vector& s, size_t n = 20) { + static const char* H = "0123456789abcdef"; + std::string r; for (size_t i = 0; i < std::min(s.size(), n); ++i) { r += H[s[i]>>4]; r += H[s[i]&0xf]; } + return r; + }; + LOG_INFO << "[MERGED-REG] NEW chain_id=" << entry.m_chain_id + << " parent=" << to_hex_short(parent_script) + << " merged=" << to_hex_short(entry.m_script.m_data) + << " — skiplist invalidated"; + } + } + } + } + + // Previous-share lambda for RAW chain (work templates, general PPLNS) + auto make_previous_fn() + { + return [this](const uint256& hash) -> uint256 { + if (!chain.contains(hash)) return uint256{}; + return chain.get_index(hash)->tail; + }; + } + + // Previous-share lambda for VERIFIED chain (consensus hash computation). + // Ensures compute_merged_payout_hash only walks shares that all peers + // agree on — prevents c2pool's own unverified shares from polluting + // the weight distribution and causing consensus divergence. + auto make_verified_previous_fn() + { + return [this](const uint256& hash) -> uint256 { + if (!verified.contains(hash)) return uint256{}; + return verified.get_index(hash)->tail; + }; + } + + void ensure_weights_skiplist() + { + if (m_weights_skiplist) + return; + m_weights_skiplist.emplace( + [this](const uint256& hash) -> chain::WeightsDelta { + chain::WeightsDelta delta; + if (!chain.contains(hash)) return delta; + delta.share_count = 1; + chain.get_share(hash).invoke([&](auto* obj) { + auto target = chain::bits_to_target(obj->m_bits); + auto att = chain::target_to_average_attempts(target); + delta.total_weight = att * 65535; + delta.total_donation_weight = att * static_cast(obj->m_donation); + auto addr_bytes = get_share_script(obj); + delta.weights[addr_bytes] = att * static_cast(65535 - obj->m_donation); + }); + return delta; + }, + make_previous_fn() + ); + } + + void ensure_v36_skiplist() + { + if (m_v36_weights_skiplist) + return; + m_v36_weights_skiplist.emplace( + [this](const uint256& hash) -> chain::WeightsDelta { + // F2: unified merged-weight delta, hash/v36 path (no chain_id). + return merged_weights_delta_for_hash(hash, std::nullopt); + }, + make_previous_fn() + ); + } + + chain::WeightsSkipList& ensure_merged_skiplist(uint32_t chain_id) + { + auto it = m_merged_skiplists.find(chain_id); + if (it != m_merged_skiplists.end()) + return it->second; + + auto [new_it, _] = m_merged_skiplists.emplace( + chain_id, + chain::WeightsSkipList( + [this, chain_id](const uint256& hash) -> chain::WeightsDelta { + // F2: unified merged-weight delta, per-chain path. + return merged_weights_delta_for_hash(hash, chain_id); + }, + make_previous_fn() + ) + ); + return new_it->second; + } + + void invalidate_weight_caches(const uint256& hash) + { + if (m_weights_skiplist) m_weights_skiplist->forget(hash); + if (m_v36_weights_skiplist) m_v36_weights_skiplist->forget(hash); + for (auto& [_, sl] : m_merged_skiplists) sl.forget(hash); + m_decayed_cache_valid = false; // chain changed — decayed cache stale + } + + // ── Dense PPLNS cache priming ───────────────────────────────────── + // Pre-populate the decayed weights cache from a HeadPPLNS ring buffer. + // Called before each attempt_verify() in think() Phase 2 so that + // generate_share_transaction() → get_v36_decayed_cumulative_weights() + // hits the O(1) cache instead of doing an O(chain_len) walk. + // + // The ring buffer uses the precomputed decay table which is BIT-EXACT + // with the iterative walk (same truncation at each step). This is NOT + // an approximation — it produces identical CumulativeWeights. + void prime_pplns_cache(const uint256& start, int32_t max_shares, + const CumulativeWeights& weights) { + m_decayed_cache_start = start; + m_decayed_cache_shares = max_shares; + // Verification always uses unlimited desired_weight + m_decayed_cache_desired.SetHex( + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + m_decayed_cache_result = weights; + m_decayed_cache_valid = true; + } + + // -- Decayed weights result cache -- + // Avoids recomputing the O(chain_length) walk when the same + // (start, max_shares) is queried multiple times between chain changes + // (e.g. verify_share + get_expected_payouts for the same share). + bool m_decayed_cache_valid{false}; + uint256 m_decayed_cache_start; + int32_t m_decayed_cache_shares{0}; + uint288 m_decayed_cache_desired; + CumulativeWeights m_decayed_cache_result; +}; + +} // namespace dgb