diff --git a/src/impl/bch/pool_entrypoint.hpp b/src/impl/bch/pool_entrypoint.hpp index 0a9c6f7a..41766af4 100644 --- a/src/impl/bch/pool_entrypoint.hpp +++ b/src/impl/bch/pool_entrypoint.hpp @@ -53,6 +53,12 @@ #include "pool_standup.hpp" #include "coin/embedded_daemon.hpp" #include "stratum/work_source.hpp" +#include "config_pool.hpp" // bch::PoolConfig::get_donation_script +#include "share_check.hpp" // bch::create_local_share (transitive via node.hpp) +#include "share_types.hpp" // bch::StaleInfo +#include "coin/block.hpp" // bch::coin::SmallBlockHeaderType + +#include // BaseScript #include #include @@ -62,6 +68,9 @@ #include #include +#include +#include +#include #include #include #include @@ -132,6 +141,141 @@ inline void standup_pool_run(boost::asio::io_context& ioc, return r.any(); }); + // ── Sharechain WRITE path: local-share author wiring ───────────────── + // Without these callbacks BCHWorkSource accepts miner submissions that + // clear the sharechain target but DROPS them ("accepted (no-tracker)", + // work_source.cpp:686) because create_share_fn_ is null -- c2pool-bch runs + // as a bare stratum proxy and Stored shares stay stuck at 0. This block + // closes that gap (mirrors main_btc.cpp:863/1118), wiring the three + // callbacks the local-author path needs: + // - best_share_hash_fn : new-job prev_share = our sharechain tip + // - donation_script : version-gated coinbase residual recipient + // - create_share_fn : mining_submit -> bch::create_local_share -> + // tracker.add -> broadcast_share + notify_local_share + // BCH is a STANDALONE SHA256d parent: no segwit, no merged/aux dimension + // (merged_addrs = {}, segwit_active=false, witness_* empty). ref_hash_fn / + // pplns_fn are a FOLLOW-UP slice -- they gate peer-verifiable ref_hash + + // PPLNS payout distribution, NOT local Stored accumulation; until wired, + // build_connection_coinbase falls back to a single-output coinbase and + // create_local_share computes its own share target via + // tracker.compute_share_target (has_frozen=false). + work_source->set_best_share_hash_fn( + [&node]() -> uint256 { return node.best_share_hash(); }); + + // Initial donation matches the cold-start create version (35 -> P2PK). A + // ratchet-driven refresh to the COMBINED P2SH on v36 activation is the same + // follow-up slice as pplns_fn/ref_hash_fn. + work_source->set_donation_script(PoolConfig::get_donation_script(35)); + + work_source->set_create_share_fn( + [&node](const std::vector& full_coinbase, + const std::vector& header_80b, + const core::stratum::JobSnapshot& job, + const std::vector& payout_script) -> uint256 + { + if (header_80b.size() != 80) { + LOG_WARNING << "[BCH-CREATE-SHARE] bad header size=" << header_80b.size(); + return uint256::ZERO; + } + + // Parse the 80-byte BCH block header -> SmallBlockHeaderType. The + // merkle_root (bytes 36..67) is reconstructible from coinbase + + // frozen branches, so it is not stored in min_header. + auto read_le32 = [](const uint8_t* p) -> uint32_t { + return uint32_t(p[0]) | (uint32_t(p[1]) << 8) + | (uint32_t(p[2]) << 16) | (uint32_t(p[3]) << 24); + }; + coin::SmallBlockHeaderType min_header; + min_header.m_version = read_le32(header_80b.data() + 0); + std::memcpy(min_header.m_previous_block.data(), header_80b.data() + 4, 32); + min_header.m_timestamp = read_le32(header_80b.data() + 68); + 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())); + + // Stratum branches are hex of LE-internal bytes -> ParseHex+memcpy + // (SetHex would byte-reverse and break the merkle root the miner used). + std::vector merkle_branches; + merkle_branches.reserve(job.merkle_branches.size()); + for (const auto& bhex : job.merkle_branches) { + uint256 b; + auto bb = ParseHex(bhex); + if (bb.size() == 32) std::memcpy(b.begin(), bb.data(), 32); + merkle_branches.push_back(b); + } + + // Exclusive tracker lock (non-blocking): defer to the next submit if + // the compute thread is mid-think rather than freezing the io_context. + std::unique_lock lk(node.tracker_mutex(), std::try_to_lock); + if (!lk.owns_lock()) { + LOG_INFO << "[BCH-CREATE-SHARE] tracker busy -- share deferred"; + return uint256::ZERO; + } + + // 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. + int64_t create_ver = 35; + 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) { + using ST = std::remove_pointer_t; + create_ver = ST::version; + }); + } + + uint256 share_hash; + try { + share_hash = bch::create_local_share( + node.tracker(), min_header, coinbase_bs, + /* subsidy */ job.subsidy, + /* prev_share */ job.prev_share_hash, + merkle_branches, payout_script, + /* donation bps */ 50, + /* merged_addrs */ {}, + /* stale_info */ StaleInfo::none, + /* segwit_active */ false, // BCH: no segwit + /* witness_commitment */ {}, + /* message_data */ {}, + /* actual_coinbase_bytes */ full_coinbase, + /* 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 */ {}, + /* share_version */ create_ver, + /* desired_version */ 36); + } catch (const std::exception& e) { + LOG_WARNING << "[BCH-CREATE-SHARE] threw: " << e.what(); + return uint256::ZERO; + } + + // Drop the exclusive lock BEFORE broadcast/notify (they take their + // own locks / post to the io_context). + lk.unlock(); + if (!share_hash.IsNull()) { + node.broadcast_share(share_hash); + node.notify_local_share(share_hash); + LOG_INFO << "[BCH-CREATE-SHARE] OK + broadcast v" << create_ver + << " hash=" << share_hash.GetHex().substr(0, 16); + } + return share_hash; + }); + + LOG_INFO << "[BCH-POOL] sharechain WRITE path wired (mining_submit ->" + << " create_local_share -> broadcast_share + notify_local_share);" + << " ref_hash/pplns are a follow-up slice."; + std::unique_ptr stratum_server; if (stratum_port != 0) { stratum_server = std::make_unique( diff --git a/src/impl/bch/share_check.hpp b/src/impl/bch/share_check.hpp index f1e168df..db414901 100644 --- a/src/impl/bch/share_check.hpp +++ b/src/impl/bch/share_check.hpp @@ -2202,6 +2202,18 @@ uint256 create_local_share_v35( prev_share, share.m_timestamp, desired_target); share.m_max_bits = share_max_bits; share.m_bits = share_bits; + + // -- Share-target pin (independent of has_frozen) -- + // Pin m_bits/m_max_bits to the target the miner was actually ISSUED and + // hashed against (caller passes job.share_bits/share_max_bits) BEFORE + // abswork is derived from m_bits below. Without this, create_local_share + // re-derives the target via compute_share_target, which on cold-start / + // isolated-net (and under the BCH_DEMO_SHARE_BITS floor) differs from the + // target the stratum gate accepted -- so the internal PoW recheck rejects + // an otherwise-valid local share and Stored stays 0. The has_frozen block + // below re-applies the same values, so this is a no-op on that path. + if (override_max_bits) share.m_max_bits = override_max_bits; + if (override_bits) share.m_bits = override_bits; share.m_nonce = 0; // V35: address as string (VarStr), convert from payout_script @@ -2635,6 +2647,18 @@ uint256 create_local_share( share.m_max_bits = share_max_bits; share.m_bits = share_bits; + // -- Share-target pin (independent of has_frozen) -- + // Pin m_bits/m_max_bits to the target the miner was actually ISSUED and + // hashed against (caller passes job.share_bits/share_max_bits) BEFORE + // abswork is derived from m_bits below. Without this, create_local_share + // re-derives the target via compute_share_target, which on cold-start / + // isolated-net (and under the BCH_DEMO_SHARE_BITS floor) differs from the + // target the stratum gate accepted -- so the internal PoW recheck rejects + // an otherwise-valid local share and Stored stays 0. The has_frozen block + // below re-applies the same values, so this is a no-op on that path. + if (override_max_bits) share.m_max_bits = override_max_bits; + if (override_bits) share.m_bits = override_bits; + share.m_nonce = 0; // share commitment nonce (not block nonce) share.m_merged_addresses = merged_addrs; diff --git a/src/impl/bch/share_tracker.hpp b/src/impl/bch/share_tracker.hpp index cb5f6b2a..9ad13a62 100644 --- a/src/impl/bch/share_tracker.hpp +++ b/src/impl/bch/share_tracker.hpp @@ -407,6 +407,20 @@ class ShareTracker // attempt_verify() promotes it into `verified`. add() merely buffers it in // the raw `chain`. BCH is a SHA256d standalone parent: no merged-mining, so // (unlike btc) there is no try_register_merged_addr() leg here. + // -- Add share to the main chain (raw-pointer form) -- + // Mirrors btc::ShareTracker::add(ShareT*) (SSOT) so create_local_share() + // can hand off a freshly heap-allocated concrete share (PaddingBugfixShare / + // MergedMiningShare) without the caller wrapping it into the ShareType + // variant. This overload was missing on BCH -- create_local_share is the + // first caller, so its instantiation surfaced the gap. BCH is a SHA256d + // standalone parent: no try_register_merged_addr leg (see add(ShareType)). + template + void add(ShareT* share) + { + if (!chain.contains(share->m_hash)) + chain.add(share); + } + void add(ShareType share) { auto h = share.hash(); diff --git a/src/impl/bch/stratum/work_source.cpp b/src/impl/bch/stratum/work_source.cpp index 27fa8870..7592cce0 100644 --- a/src/impl/bch/stratum/work_source.cpp +++ b/src/impl/bch/stratum/work_source.cpp @@ -31,11 +31,13 @@ #include #include // merkle_hash_pair (CTOR SHA256d) #include // build_template + rpc::WorkData +#include // PoolConfig::max_target (G2 cold-start floor) #include #include // HexStr #include +#include // std::getenv (BCH_DEMO_BLOCK_BITS isolated-net pin) #include #include @@ -207,6 +209,55 @@ BCHWorkSource::cached_template() const auto built = bch::coin::TemplateBuilder::build_template(chain_, mempool_, is_testnet_); if (!built) return nullptr; // chain has no tip yet + // [ISOLATED-NET DEMO / G2] Env-gated block-target pin (single source). + // On a genesis-difficulty isolated net the substrate hands GBT bits = + // powLimit (regtest 0x207fffff), so every pseudoshare trivially clears + // block and the p2pool sharechain counter cannot move distinct from + // block-founds. When BCH_DEMO_BLOCK_BITS is set, pin the template block + // bits to a fixed harder compact target here -- on the mutable build + // result before it is frozen into the shared cache -- so the coinbase, + // the header nBits, and core stratum_server gbt_block_nbits all read one + // consistent value; the bulk of accepted shares then land as STORED + // shares and block-founds stay rare. OFF unless the env var is set; + // never active on normal or mainnet runs. BCH-local (no shared-core edit). + if (const char* e = std::getenv("BCH_DEMO_BLOCK_BITS"); e && *e) + built->m_data["bits"] = std::string(e); + + // [G2 cold-start] Seed the pool share-target floor on the WORK-GEN path. + // build_connection_coinbase() also seeds share_bits_ from ref_hash_fn's + // genesis max_bits, but that path runs only AFTER a share is created -- + // which core::StratumServer gates on pool_difficulty>0, itself derived + // from share_bits_. On an empty sharechain share_bits_==0 => pool + // difficulty==0 => the is_pool_share gate never fires => no share is + // created => share_bits_ can never advance: a cold-start deadlock the + // share-create seed cannot break because it sits behind the very gate it + // needs to open. cached_template() runs on every work poll, BEFORE and + // INDEPENDENT of share creation, so seeding the floor here bootstraps + // pool_difficulty and lets the first real submission cross the gate. + // Idempotent (fires only while unseeded); uses the exact floor + // compute_share_target's genesis branch emits for max_bits. + if (share_bits_.load(std::memory_order_relaxed) == 0) { + // [ISOLATED-NET DEMO / G2] Symmetric to BCH_DEMO_BLOCK_BITS above: on a + // genesis-difficulty isolated net PoolConfig::max_target() (the p2pool + // network share floor, ~diff-1 / 0x1d00ffff) is far too hard for a CPU + // grinder to clear at a useful rate, so the sharechain STORED counter + // never advances independently of block-founds even after the + // cold-start deadlock fix. BCH_DEMO_SHARE_BITS pins this cold-start + // share floor to a CPU-clearable compact target (e.g. regtest powLimit + // 0x207fffff) so a grinder promotes real STORED shares while + // BCH_DEMO_BLOCK_BITS keeps block-founds rare -- proving 0->N sharechain + // accumulation without ASIC hashrate. OFF unless set; never active on + // normal or mainnet runs. BCH-local (no shared-core edit). + uint32_t floor_bits; + if (const char* e = std::getenv("BCH_DEMO_SHARE_BITS"); e && *e) + floor_bits = static_cast(std::strtoul(e, nullptr, 16)); + else + floor_bits = + chain::target_to_bits_upper_bound(bch::PoolConfig::max_target()); + share_bits_.store(floor_bits, std::memory_order_relaxed); + share_max_bits_.store(floor_bits, std::memory_order_relaxed); + } + auto sp = std::make_shared(std::move(*built)); std::lock_guard lk(template_mutex_); template_cache_ = sp; @@ -271,6 +322,12 @@ std::pair BCHWorkSource::get_coinbase_parts() const // -- IWorkSource: setters (callback wiring from main_bch.cpp) ------------------ +void BCHWorkSource::set_best_share_hash_fn(std::function fn) +{ + std::lock_guard lk(best_share_mutex_); + best_share_hash_fn_ = std::move(fn); +} + void BCHWorkSource::set_pplns_fn(PplnsFn fn) { std::lock_guard lk(callback_mutex_); @@ -402,6 +459,18 @@ core::stratum::CoinbaseResult BCHWorkSource::build_connection_coinbase( if (rh_result.bits != 0) { share_bits_.store(rh_result.bits, std::memory_order_relaxed); share_max_bits_.store(rh_result.max_bits, std::memory_order_relaxed); + } else if (share_bits_.load(std::memory_order_relaxed) == 0 && + rh_result.max_bits != 0) { + // Cold-start seed (empty sharechain): compute_share_target's + // genesis branch yields bits==0 while max_bits carries the + // MAX_TARGET floor (easiest share target). Without a nonzero + // share_bits_, StratumServer derives pool_difficulty==0, its + // is_pool_share gate never fires, no share is ever created, and + // share_bits_ can never advance past 0 -> cold-start deadlock. + // Seed both atomics to the max_bits floor so the first real + // submission clears the gate and bootstraps the sharechain. + share_bits_.store(rh_result.max_bits, std::memory_order_relaxed); + share_max_bits_.store(rh_result.max_bits, std::memory_order_relaxed); } } catch (const std::exception& e) { LOG_WARNING << "[BCH-STRATUM] ref_hash_fn threw: " << e.what()