Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions src/impl/bch/pool_entrypoint.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <core/pack_types.hpp> // BaseScript

#include <core/log.hpp>
#include <core/stratum_server.hpp>
Expand All @@ -62,6 +68,9 @@
#include <boost/asio/io_context.hpp>

#include <cstdint>
#include <cstring>
#include <shared_mutex>
#include <type_traits>
#include <memory>
#include <stdexcept>
#include <string>
Expand Down Expand Up @@ -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<unsigned char>& full_coinbase,
const std::vector<uint8_t>& header_80b,
const core::stratum::JobSnapshot& job,
const std::vector<unsigned char>& 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<unsigned char>(
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<uint256> 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<std::shared_mutex> 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<decltype(s)>;
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<core::StratumServer> stratum_server;
if (stratum_port != 0) {
stratum_server = std::make_unique<core::StratumServer>(
Expand Down
24 changes: 24 additions & 0 deletions src/impl/bch/share_check.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;

Expand Down
14 changes: 14 additions & 0 deletions src/impl/bch/share_tracker.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <typename ShareT>
void add(ShareT* share)
{
if (!chain.contains(share->m_hash))
chain.add(share);
}

void add(ShareType share)
{
auto h = share.hash();
Expand Down
69 changes: 69 additions & 0 deletions src/impl/bch/stratum/work_source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@
#include <impl/bch/coin/mempool.hpp>
#include <impl/bch/coin/merkle.hpp> // merkle_hash_pair (CTOR SHA256d)
#include <impl/bch/coin/template_builder.hpp> // build_template + rpc::WorkData
#include <impl/bch/config_pool.hpp> // PoolConfig::max_target (G2 cold-start floor)

#include <core/log.hpp>
#include <btclibs/util/strencodings.h> // HexStr

#include <ctime>
#include <cstdlib> // std::getenv (BCH_DEMO_BLOCK_BITS isolated-net pin)
#include <span>
#include <utility>

Expand Down Expand Up @@ -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<uint32_t>(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<const bch::coin::rpc::WorkData>(std::move(*built));
std::lock_guard<std::mutex> lk(template_mutex_);
template_cache_ = sp;
Expand Down Expand Up @@ -271,6 +322,12 @@ std::pair<std::string, std::string> BCHWorkSource::get_coinbase_parts() const

// -- IWorkSource: setters (callback wiring from main_bch.cpp) ------------------

void BCHWorkSource::set_best_share_hash_fn(std::function<uint256()> fn)
{
std::lock_guard<std::mutex> lk(best_share_mutex_);
best_share_hash_fn_ = std::move(fn);
}

void BCHWorkSource::set_pplns_fn(PplnsFn fn)
{
std::lock_guard<std::mutex> lk(callback_mutex_);
Expand Down Expand Up @@ -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()
Expand Down
Loading