diff --git a/src/impl/bch/pool_entrypoint.hpp b/src/impl/bch/pool_entrypoint.hpp index 41766af4..b8554c74 100644 --- a/src/impl/bch/pool_entrypoint.hpp +++ b/src/impl/bch/pool_entrypoint.hpp @@ -62,6 +62,7 @@ #include #include +#include #include // HexStr @@ -69,6 +70,7 @@ #include #include +#include // std::getenv (BCH_DEMO_SHARE_BITS demo floor) #include #include #include @@ -167,8 +169,199 @@ inline void standup_pool_run(boost::asio::io_context& ioc, // follow-up slice as pplns_fn/ref_hash_fn. work_source->set_donation_script(PoolConfig::get_donation_script(35)); + // -- ref_hash_fn: peer-verifiable share commitment (G2 conform) -------- + // Without this the local-author coinbase carries NO p2pool OP_RETURN + // ref_hash (build_connection_coinbase gates the OP_RETURN on ref_hash_fn + // being set) and create_local_share runs has_frozen=false -- so every + // share we author is systematically non-peer-verifiable: the recomputed + // ref_hash never equals the stored one (100% verify_share mismatch). + // This lambda produces the ref_hash AND the chain-walked snapshot + // (bits/max_bits from compute_share_target, absheight/abswork/ + // far_share_hash off the prev walk, clipped timestamp) that populates + // snap.frozen_ref; mining_submit threads it back into create_local_share + // (has_frozen=true) so the create-side reconstruction is byte-exact. + // Mirrors main_btc.cpp:920 and the chain-position math of + // create_local_share_v35 (share_check.hpp:2724); BCH is standalone + // SHA256d -- no segwit, no merged dimension. + work_source->set_ref_hash_fn( + [&node](const uint256& prev_share_hash, + const std::vector& scriptSig, + const std::vector& payout_script, + uint64_t subsidy, uint32_t block_bits, uint32_t timestamp) + -> core::stratum::RefHashResult + { + core::stratum::RefHashResult result; + result.share_version = 35; + result.desired_version = 36; + + bch::RefHashParams p; + p.share_version = 35; + p.prev_share = prev_share_hash; + p.coinbase_scriptSig = scriptSig; + p.share_nonce = 0; + p.subsidy = subsidy; + p.donation = 50; // 0.5% finder fee (matches create side) + p.stale_info = 0; + p.desired_version = 36; + p.has_segwit = false; // BCH: never segwit + p.timestamp = timestamp; + + // Pubkey extract mirrors create_local_share_v35 (share_check.hpp + // ~2600): P2PKH / P2SH / P2WPKH, so p.pubkey_hash + p.pubkey_type + // feed the ref-stream identically on both sides. + 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) { + std::memcpy(p.pubkey_hash.begin(), payout_script.data() + 3, 20); + p.pubkey_type = 0; + } else if (payout_script.size() == 23 && payout_script[0] == 0xa9 && + payout_script[1] == 0x14 && payout_script[22] == 0x87) { + std::memcpy(p.pubkey_hash.begin(), payout_script.data() + 2, 20); + p.pubkey_type = 2; + } else if (payout_script.size() == 22 && payout_script[0] == 0x00 && + payout_script[1] == 0x14) { + std::memcpy(p.pubkey_hash.begin(), payout_script.data() + 2, 20); + p.pubkey_type = 1; + } + + auto block_bits_fallback = [&] { + p.bits = block_bits; p.max_bits = block_bits; + }; + + // Read-only chain walk (shared lock; the compute thread holds the + // exclusive lock and never runs on this stratum thread). + { + std::shared_lock lk(node.tracker_mutex()); + auto& tracker = node.tracker(); + + const bool have_prev = !prev_share_hash.IsNull() && + tracker.chain.contains(prev_share_hash); + + // share_version off the prev tip (matches create_ver); cold + // start stays v35 voting v36. + if (have_prev) { + tracker.chain.get(prev_share_hash).share.invoke([&](auto* s) { + using ST = std::remove_pointer_t; + p.share_version = ST::version; + }); + } + + // absheight + clipped timestamp off prev. + if (have_prev) { + tracker.chain.get(prev_share_hash).share.invoke([&](auto* prev) { + p.absheight = prev->m_absheight + 1; + if (p.timestamp <= prev->m_timestamp) + p.timestamp = prev->m_timestamp + 1; + }); + } else { + p.absheight = 1; // genesis + } + + // share_target with the clipped timestamp (same call + // create_local_share_v35 makes). + try { + auto st = tracker.compute_share_target( + prev_share_hash, p.timestamp, + chain::bits_to_target(block_bits)); + p.bits = st.bits; p.max_bits = st.max_bits; + } catch (const std::exception&) { + block_bits_fallback(); + } + // Cold start: compute_share_target's genesis branch yields + // bits==0 while max_bits carries the MAX_TARGET floor. Pin + // p.bits to the floor so the ref_hash, the frozen field, and + // share_bits_ all agree (else recomputed != stored). + if (p.bits == 0 && p.max_bits != 0) + p.bits = p.max_bits; + + // [ISOLATED-NET DEMO / G2] Mirror work_source's + // BCH_DEMO_SHARE_BITS floor here so the ref_hash, the frozen + // field, and the core pool_difficulty gate all read ONE share + // target on a CPU-grind isolated net. Without it ref_hash_fn + // reports compute_share_target's ~diff-1 floor -- unclearable + // by a CPU grinder -- so no submission is ever promoted to a + // STORED share and recomputed==stored can't be observed. + // OFF unless the env var is set; never active on normal or + // mainnet runs. BCH-local (fenced to the BCH tree). + if (const char* e = std::getenv("BCH_DEMO_SHARE_BITS"); e && *e) { + uint32_t demo = static_cast(std::strtoul(e, nullptr, 16)); + if (demo) { p.bits = demo; p.max_bits = demo; } + } + + // abswork = prev_abswork + attempts(this share's bits). + { + auto att = chain::target_to_average_attempts( + chain::bits_to_target(p.bits)); + if (have_prev) { + uint128 prev_abswork; + tracker.chain.get(prev_share_hash).share.invoke( + [&](auto* prev) { prev_abswork = prev->m_abswork; }); + p.abswork = prev_abswork + uint128(att.GetLow64()); + } else { + p.abswork = uint128(att.GetLow64()); + } + } + + // far_share_hash: 99th ancestor, mirroring create_local_share + // _v35's get_height_and_last rule exactly. + if (have_prev) { + auto [prev_height, last] = + tracker.chain.get_height_and_last(prev_share_hash); + if (last.IsNull() && prev_height < 99) { + p.far_share_hash = uint256(); + } else { + try { + p.far_share_hash = + tracker.chain.get_nth_parent_key(prev_share_hash, 99); + } catch (const std::exception&) { + // Bootstrap short chain: degrade to far=None instead of + // throwing out of ref_hash_fn (which would zero frozen_ref + // and force the create-side into its own unguarded walk). + p.far_share_hash = uint256(); + } + } + } else { + p.far_share_hash = uint256(); + } + } + + // Mirror the walked values into the result for snap.frozen_ref. + result.share_version = p.share_version; + result.desired_version = p.desired_version; + result.bits = p.bits; + result.max_bits = p.max_bits; + result.timestamp = p.timestamp; + result.absheight = p.absheight; + result.abswork = p.abswork; + result.far_share_hash = p.far_share_hash; + + try { + auto [rh, nn] = bch::compute_ref_hash_for_work(p); + result.ref_hash = rh; + result.last_txout_nonce = nn; + } catch (const std::exception& e) { + LOG_WARNING << "[BCH-STRATUM] compute_ref_hash_for_work threw: " + << e.what() << " -- coinbase will lack OP_RETURN"; + } + return result; + }); + + LOG_INFO << "[BCH-STRATUM] ref_hash_fn wired (walks share tracker for" + << " share_target/absheight/abswork/far_share; emits p2pool" + << " OP_RETURN ref_hash + freezes the snapshot for byte-exact" + << " create-side reconstruction)."; + + // Local-share -> stratum work-refresh bridge (BCH-side wiring; the refresh + // mechanism itself lives unchanged in core StratumServer::notify_all(), + // which re-polls best_share_hash_fn and pushes a clean mining.notify with + // the NEW sharechain tip as prev_share). Without ringing it after each + // authored share, every miner keeps grinding the job frozen at pool start + // (prev=genesis), so shares bootstrap off 0000... as orphan siblings and + // the chain never links past height 1. + auto stratum_notify = std::make_shared>(); + work_source->set_create_share_fn( - [&node](const std::vector& full_coinbase, + [&node, stratum_notify](const std::vector& full_coinbase, const std::vector& header_80b, const core::stratum::JobSnapshot& job, const std::vector& payout_script) -> uint256 @@ -192,8 +385,45 @@ inline void standup_pool_run(boost::asio::io_context& ioc, min_header.m_bits = read_le32(header_80b.data() + 72); min_header.m_nonce = read_le32(header_80b.data() + 76); - BaseScript coinbase_bs(std::vector( - full_coinbase.begin(), full_coinbase.end())); + // full_coinbase is the fully serialized coinbase TRANSACTION, but a + // share's m_coinbase field must be ONLY the coinbase INPUT scriptSig + // (BIP34 height + pool marker) -- share_init_verify enforces the + // consensus 2..100 byte bound. Feeding the whole tx tripped + // "bad coinbase size" and left every G2 share unverified. Extract the + // input scriptSig here; the full tx still rides via actual_coinbase_bytes + // for gentx byte-parity (extranonce lives in an OP_RETURN output, not + // the scriptSig, so this slice is complete on its own). + std::vector coinbase_scriptSig; + { + size_t p = 0; + bool ok = true; + auto need = [&](size_t n) { return p + n <= full_coinbase.size(); }; + auto rd_varint = [&](uint64_t& out) -> bool { + if (!need(1)) return false; + uint8_t ch = full_coinbase[p++]; + if (ch < 253) { out = ch; return true; } + size_t n = (ch == 253) ? 2 : (ch == 254) ? 4 : 8; + if (!need(n)) return false; + out = 0; + for (size_t i = 0; i < n; ++i) + out |= uint64_t(full_coinbase[p++]) << (8 * i); + return true; + }; + uint64_t vin = 0, slen = 0; + if (need(4)) p += 4; else ok = false; // version + ok = ok && rd_varint(vin) && vin >= 1; // tx_in count + if (ok && need(36)) p += 36; else ok = false; // prevout (32+4) + ok = ok && rd_varint(slen); // scriptSig length + if (ok && need(slen)) { + coinbase_scriptSig.assign(full_coinbase.begin() + p, + full_coinbase.begin() + p + slen); + } else { + LOG_WARNING << "[BCH-CREATE-SHARE] cannot parse coinbase scriptSig from " + << full_coinbase.size() << "B tx -- share skipped"; + return uint256::ZERO; + } + } + BaseScript coinbase_bs(coinbase_scriptSig); // Stratum branches are hex of LE-internal bytes -> ParseHex+memcpy // (SetHex would byte-reverse and break the merkle root the miner used). @@ -217,8 +447,15 @@ inline void standup_pool_run(boost::asio::io_context& ioc, // Author at the current tip's version (35 on an empty chain), voting // desired=36. Same-version authoring never trips the 60%-by-work // accept gate; the ratchet-driven upgrade to v36 is a follow-up slice. + // Author at the frozen share_version the ref_hash was computed + // under (job.frozen_ref.share_version) so the create-side ref + // reconstruction is byte-exact; fall back to the prev-tip version + // (cold start: v35 voting v36) when ref_hash_fn produced no frozen + // data (bits == 0). int64_t create_ver = 35; - if (!job.prev_share_hash.IsNull() && + if (job.frozen_ref.bits != 0) { + create_ver = job.frozen_ref.share_version; + } else if (!job.prev_share_hash.IsNull() && node.tracker().chain.contains(job.prev_share_hash)) { node.tracker().chain.get_share(job.prev_share_hash).invoke( [&](auto* s) { @@ -244,15 +481,15 @@ inline void standup_pool_run(boost::asio::io_context& ioc, /* witness_root */ uint256(), /* override_max_bits */ job.share_max_bits, // pin share target to /* override_bits */ job.share_bits, // what the miner was issued - /* frozen_absheight */ 0, - /* frozen_abswork */ uint128(), - /* frozen_far_share_hash */ uint256(), - /* frozen_timestamp */ 0, - /* frozen_merged_payout */ uint256(), - /* has_frozen */ false, - /* frozen_merkle_branches*/ {}, - /* frozen_witness_root */ uint256(), - /* frozen_merged_cb_info */ {}, + /* frozen_absheight */ job.frozen_ref.absheight, + /* frozen_abswork */ job.frozen_ref.abswork, + /* frozen_far_share_hash */ job.frozen_ref.far_share_hash, + /* frozen_timestamp */ job.frozen_ref.timestamp, + /* frozen_merged_payout */ job.frozen_ref.merged_payout_hash, + /* has_frozen */ (job.frozen_ref.bits != 0), + /* frozen_merkle_branches*/ job.frozen_ref.frozen_merkle_branches, + /* frozen_witness_root */ uint256(), // BCH: never segwit + /* frozen_merged_cb_info */ {}, // BCH: standalone, no merged /* share_version */ create_ver, /* desired_version */ 36); } catch (const std::exception& e) { @@ -266,6 +503,9 @@ inline void standup_pool_run(boost::asio::io_context& ioc, if (!share_hash.IsNull()) { node.broadcast_share(share_hash); node.notify_local_share(share_hash); + // Ring the stratum work-refresh so the next mining.notify carries + // the new sharechain tip as prev_share (chain links past height 1). + if (*stratum_notify) (*stratum_notify)(); LOG_INFO << "[BCH-CREATE-SHARE] OK + broadcast v" << create_ver << " hash=" << share_hash.GetHex().substr(0, 16); } @@ -281,6 +521,8 @@ inline void standup_pool_run(boost::asio::io_context& ioc, stratum_server = std::make_unique( ioc, stratum_addr, stratum_port, work_source); if (stratum_server->start()) { + // Wire the local-share bridge to the now-constructed stratum server. + *stratum_notify = [srv = stratum_server.get()]() { srv->notify_all(); }; LOG_INFO << "[BCH-POOL] stratum listening on " << stratum_addr << ":" << stratum_port << " (BCHWorkSource: SHA256d, no-segwit," << " CashTokens transparent-carry; hit block routes the" diff --git a/src/impl/bch/share_check.hpp b/src/impl/bch/share_check.hpp index db414901..6399c13c 100644 --- a/src/impl/bch/share_check.hpp +++ b/src/impl/bch/share_check.hpp @@ -2744,7 +2744,14 @@ uint256 create_local_share( // Chain is complete and shorter than 99 → None (zero) share.m_far_share_hash = uint256(); } else { - share.m_far_share_hash = tracker.chain.get_nth_parent_key(prev_share, 99); + try { + share.m_far_share_hash = tracker.chain.get_nth_parent_key(prev_share, 99); + } catch (const std::exception&) { + // Bootstrap: chain shorter than the 99-deep lookback even though + // get_height_and_last reported a non-null last → far=None, mirroring + // the guarded _v35 author and p2pool's get_nth_parent_hash(prev,99). + share.m_far_share_hash = uint256(); + } } } else { // Genesis: p2pool always does (prev_absheight + 1), (prev_abswork + aps)