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
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ jobs:
bch_genesis_conformance_test bch_abla_growth_soak_test \
bch_coinbase_kat_segwit_predicate_test \
bch_coinbase_kat_bytevector_test \
bch_coinbase_author_kat_test \
bch_cashtokens_transparency_test \
bch_block_connector_test bch_block_ordering_test \
bch_utxo_connect_test bch_reorg_connect_test bch_reorg_rerequest_test \
Expand Down Expand Up @@ -436,6 +437,7 @@ jobs:
bch_genesis_conformance_test bch_abla_growth_soak_test \
bch_coinbase_kat_segwit_predicate_test \
bch_coinbase_kat_bytevector_test \
bch_coinbase_author_kat_test \
bch_cashtokens_transparency_test \
bch_block_connector_test bch_block_ordering_test \
bch_utxo_connect_test bch_reorg_connect_test bch_reorg_rerequest_test \
Expand Down
70 changes: 70 additions & 0 deletions scripts/gen_g2_oracle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Oracle golden generator for BCH G2 coinbase-author KAT.
# Transcribes VERBATIM the v36 output-assembly from p2pool-merged-v36
# p2pool/data.py:920-1085 (generate_transaction, v36_active branch):
# amounts = subsidy*weight//total_weight ;
# total_donation = subsidy - sum(amounts) ;
# if total_donation < 1 and subsidy>0: largest by (amount,script) -=1 ; recompute
# amounts[DON] += total_donation
# dests = sorted(scripts \ {DON}, key=(amounts[s], s))[-4000:]
# payouts = [{value:amounts[s], script:s} for s in dests if amounts[s]] + [DON last]
import struct

COMBINED_DONATION_SCRIPT = bytes.fromhex('a9148c6272621d89e8fa526dd86acff60c7136be8e8587')
def p2pkh(b): return bytes.fromhex('76a914') + bytes([b])*20 + bytes.fromhex('88ac')
A=p2pkh(0xaa); B=p2pkh(0xbb); C=p2pkh(0xcc); D=p2pkh(0xdd)

def assemble_v36(weights, donation_weight, subsidy):
total_weight = sum(weights.values()) + donation_weight
amounts = dict((s, subsidy*w//total_weight) for s,w in weights.items())
total_donation = subsidy - sum(amounts.values())
if total_donation < 1 and subsidy > 0:
largest = max(amounts, key=lambda k:(amounts[k], k))
amounts[largest] -= 1
total_donation = subsidy - sum(amounts.values())
amounts[COMBINED_DONATION_SCRIPT] = amounts.get(COMBINED_DONATION_SCRIPT,0) + total_donation
excluded = {COMBINED_DONATION_SCRIPT}
dests = sorted([s for s in amounts if s not in excluded], key=lambda s:(amounts[s], s))[-4000:]
payouts = [(s, amounts[s]) for s in dests if amounts[s]]
payouts.append((COMBINED_DONATION_SCRIPT, amounts[COMBINED_DONATION_SCRIPT]))
return payouts

def assemble_with_finderfee(weights, finder, donation_weight, subsidy):
# NEGATIVE control: pre-v36 math (199/200 haircut) + subsidy//200 finder fee.
total_weight = sum(weights.values()) + donation_weight
amounts = dict((s, subsidy*(199*w)//(200*total_weight)) for s,w in weights.items())
amounts[finder] = amounts.get(finder,0) + subsidy//200
total_donation = subsidy - sum(amounts.values())
if total_donation < 1 and subsidy > 0:
largest = max(amounts, key=lambda k:(amounts[k], k))
amounts[largest] -= 1
total_donation = subsidy - sum(amounts.values())
amounts[COMBINED_DONATION_SCRIPT] = amounts.get(COMBINED_DONATION_SCRIPT,0) + total_donation
dests = sorted([s for s in amounts if s != COMBINED_DONATION_SCRIPT], key=lambda s:(amounts[s], s))[-4000:]
payouts = [(s, amounts[s]) for s in dests if amounts[s]]
payouts.append((COMBINED_DONATION_SCRIPT, amounts[COMBINED_DONATION_SCRIPT]))
return payouts

def varint(n):
if n < 0xfd: return bytes([n])
if n <= 0xffff: return b'\xfd'+struct.pack('<H',n)
if n <= 0xffffffff: return b'\xfe'+struct.pack('<I',n)
return b'\xff'+struct.pack('<Q',n)

def serialize(payouts):
out = b''
for script, value in payouts:
out += struct.pack('<Q', value) + varint(len(script)) + script
return out

SUB = 1_000_000_000
cases = {
'CASE1_ordering_tie': assemble_v36({A:10,B:20,C:20,D:50}, 0, SUB),
'CASE2_donation_forced_last': assemble_v36({A:40,B:20}, 40, SUB),
'CASE3_v36_no_finderfee': assemble_v36({A:60,B:40}, 0, SUB),
'CASE3_NEG_with_finderfee': assemble_with_finderfee({A:60,B:40}, A, 0, SUB),
}
names = {A.hex():'A',B.hex():'B',C.hex():'C',D.hex():'D',COMBINED_DONATION_SCRIPT.hex():'DON'}
for name, p in cases.items():
print('=== %s ===' % name)
for s,v in p: print(' %-4s value=%d' % (names.get(s.hex(),'?'), v))
print(' OUTSECTION_HEX=%s' % serialize(p).hex())
91 changes: 90 additions & 1 deletion src/c2pool/main_bch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
#include <impl/bch/coin/regtest_block.hpp>
#include <impl/bch/coin/rpc.hpp>
#include <impl/bch/coin/node.hpp>
#include <impl/bch/pool_entrypoint.hpp>
#include <btclibs/util/strencodings.h> // ParseHexBytes (sharechain prefix)

#include <core/core_util.hpp>

Expand Down Expand Up @@ -71,7 +73,8 @@ void print_banner(const char* argv0)
<< " " << argv0 << " --ibd [--testnet] [--near-tip] [--auto-kick] [--peer HOST:PORT] [--max-seconds N]\n"
<< " " << argv0 << " --with-peer-verify [--testnet] [--peer HOST:PORT] [--max-seconds N]\n"
<< " " << argv0 << " --leg-c-capture [--rpc-conf PATH]\n"
<< " " << argv0 << " --leg-c-capture-p2p [--rpc-conf PATH] [--p2p-port N]\n\n"
<< " " << argv0 << " --leg-c-capture-p2p [--rpc-conf PATH] [--p2p-port N]\n"
<< " " << argv0 << " --pool [--testnet|--regtest] [--stratum [HOST:]PORT] [--peer HOST:PORT] [--anchor N]\n\n"
<< "Status: M5 pool/sharechain + embedded-daemon assembly live.\n"
<< " The embedded daemon (coin/embedded_daemon.hpp) is the primary\n"
<< " work source; external BCHN-RPC stays as the fallback.\n"
Expand Down Expand Up @@ -583,6 +586,70 @@ int run_leg_c_capture_p2p(const std::string& conf_path, uint16_t p2p_port)
return confirmed ? 0 : 1;
}


// --pool: PRODUCTION pool run-loop -- the first non-harness c2pool-bch
// entrypoint. Stands up the BCH pool node + embedded coin daemon on one shared
// io_context via bch::standup_pool_run, with the won-block sink bound (dual
// path: embedded P2P primary + BCHN submitblock fallback) and, when --stratum
// is given, the miner-facing BCHWorkSource + core::StratumServer listening so a
// genuine share-author coinbase is what gets assembled and broadcast. The
// --ibd path is a read-only evidence harness; THIS path is the real node.
//
// Config is built WITHOUT a YAML load (matches run_ibd): no pool.yaml/coin.yaml
// is required for the slice. The two embedded-daemon wire fields (P2P magic +
// BCHN peer) and the sharechain PREFIX (bucket-1 isolation primitive, never
// standardized) are set by hand from BCH chainparams. The external BCHN-RPC
// fallback inside EmbeddedDaemon::run() is retained (external_fallback law).
//
// p2pool-merged-v36 surface: NONE -- run-loop bring-up + block dispatch, not
// share/PPLNS/coinbase/PoW bytes. PER-COIN ISOLATION: src/impl/bch only.
int run_pool(const std::string& peer_host, uint16_t peer_port, bool testnet,
bool regtest, uint32_t anchor_height,
const std::string& stratum_addr, uint16_t stratum_port)
{
boost::asio::io_context ioc;

bch::PoolConfig::is_testnet = testnet;

bch::Config config("bch");
// Skip Config::init() (no on-disk pool.yaml/coin.yaml); set only the fields
// the run-loop touches, from BCH chainparams.
config.coin()->m_testnet = testnet || regtest;
config.coin()->m_symbol = "BCH";
config.coin()->m_p2p.address = NetService(peer_host, peer_port);
// BCH P2P network magic (pchMessageStart, BCHN chainparams.cpp): mainnet
// e3e1f3e8, testnet3 f4e5f3f4, regtest dab5bffa. Wrong magic == BCHN drops
// the peer with EOF right after connect.
config.coin()->m_p2p.prefix = regtest
? std::vector<std::byte>{ std::byte{0xda}, std::byte{0xb5}, std::byte{0xbf}, std::byte{0xfa} }
: (testnet
? std::vector<std::byte>{ std::byte{0xf4}, std::byte{0xe5}, std::byte{0xf3}, std::byte{0xf4} }
: std::vector<std::byte>{ std::byte{0xe3}, std::byte{0xe1}, std::byte{0xf3}, std::byte{0xe8} });
// Sharechain identity: BCH p2pool PREFIX namespaces the sharechain P2P
// framing. standup_pool_run's Node reads pool()->m_prefix.
config.pool()->m_prefix = ParseHexBytes(bch::PoolConfig::prefix_hex());

std::cout
<< "[pool] c2pool-bch pool run-loop"
<< (regtest ? " (regtest)" : (testnet ? " (testnet)" : " (mainnet)"))
<< " -- BCHN peer " << peer_host << ":" << peer_port
<< ", cold-start anchor=" << anchor_height;
if (stratum_port)
std::cout << ", stratum " << stratum_addr << ":" << stratum_port;
else
std::cout << ", stratum DISABLED (no --stratum)";
std::cout << "\n";

try {
bch::standup_pool_run(ioc, config, anchor_height,
stratum_addr, stratum_port, testnet || regtest, regtest);
} catch (const std::exception& e) {
std::cout << "[pool] FATAL: " << e.what() << "\n";
return 1;
}
return 0;
}

} // namespace

int main(int argc, char** argv)
Expand All @@ -592,6 +659,11 @@ int main(int argc, char** argv)
bool want_leg_c = false;
bool want_leg_c_p2p = false;
bool want_with_peer_verify = false;
bool want_pool = false;
bool regtest = false;
std::string stratum_addr = "0.0.0.0";
uint16_t stratum_port = 0; // 0 disables stratum; --stratum sets it
uint32_t anchor_height = 0; // cold-start ABLA floor anchor
uint16_t leg_c_p2p_port = 18444; // BCHN regtest P2P default
std::string rpc_conf;
bool testnet = false;
Expand All @@ -609,6 +681,20 @@ int main(int argc, char** argv)
if (std::strcmp(argv[i], "--help") == 0) want_help = true;
if (std::strcmp(argv[i], "--ibd") == 0) want_ibd = true;
if (std::strcmp(argv[i], "--with-peer-verify") == 0) want_with_peer_verify = true;
if (std::strcmp(argv[i], "--pool") == 0) want_pool = true;
if (std::strcmp(argv[i], "--regtest") == 0) { regtest = true; testnet = true; port = 18444; }
if (std::strcmp(argv[i], "--anchor") == 0 && i + 1 < argc)
anchor_height = static_cast<uint32_t>(std::stoul(argv[++i]));
if (std::strcmp(argv[i], "--stratum") == 0 && i + 1 < argc) {
std::string sp = argv[++i];
const auto c = sp.rfind(char(58)); // ASCII colon
if (c != std::string::npos) {
stratum_addr = sp.substr(0, c);
stratum_port = static_cast<uint16_t>(std::stoul(sp.substr(c + 1)));
} else {
stratum_port = static_cast<uint16_t>(std::stoul(sp));
}
}
if (std::strcmp(argv[i], "--leg-c-capture") == 0) want_leg_c = true;
if (std::strcmp(argv[i], "--leg-c-capture-p2p") == 0) want_leg_c_p2p = true;
if (std::strcmp(argv[i], "--p2p-port") == 0 && i + 1 < argc)
Expand Down Expand Up @@ -651,6 +737,9 @@ int main(int argc, char** argv)
return run_leg_c_capture(rpc_conf);
}

if (want_pool)
return run_pool(host, port, testnet, regtest, anchor_height, stratum_addr, stratum_port);

if (want_with_peer_verify)
return run_with_peer_verify(host, port, testnet, max_seconds);

Expand Down
25 changes: 25 additions & 0 deletions src/impl/bch/coin/asert.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,31 @@ inline ASERTParams asert_testnet3() {
};
}

inline uint256 bch_pow_limit_regtest() {
// BCH regtest powLimit (BCHN CRegTestParams). NOTE: top byte 0x7f does NOT
// satisfy CalculateASERT's 32-leading-zero-bits invariant -- which is why
// regtest runs with fPowNoRetargeting (ASERT is never invoked on regtest).
uint256 v;
v.SetHex("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
return v;
}

/// BCH regtest difficulty params (BCHN CRegTestParams). fPowNoRetargeting=true,
/// so the required target is fixed at the powLimit nBits (0x207fffff) for every
/// block -- ASERT is bypassed (header_chain honours BCHChainParams::no_retargeting).
/// The anchor here is nominal (genesis) and never feeds CalculateASERT.
/// allow_min_difficulty mirrors BCHN regtest. (FINDING3: regtest must be its own
/// net so --pool --regtest serves diff-1 0x207fffff, not the testnet anchor.)
inline ASERTParams asert_regtest() {
return ASERTParams{
ASERTAnchor{0, 0x207fffff, 1296688602}, // genesis nBits/time (nominal)
2 * 24 * 60 * 60,
BCH_TARGET_SPACING,
true,
bch_pow_limit_regtest(),
};
}

inline ASERTParams asert_testnet4() {
return ASERTParams{
ASERTAnchor{16844, 0x1d00ffff, 1605451779},
Expand Down
64 changes: 60 additions & 4 deletions src/impl/bch/coin/embedded_daemon.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,17 @@ class EmbeddedDaemon {
/// Cold-start ctor: floor-anchored ABLA at `anchor_height` (safe default
/// when no VM300 BCHN pin is available yet). `context`/`config` outlive the
/// daemon (owned by the binary entrypoint, same contract as coin::Node).
EmbeddedDaemon(auto* context, config_t* config, uint32_t anchor_height)
EmbeddedDaemon(auto* context, config_t* config, uint32_t anchor_height,
bool is_regtest = false)
: m_config(config),
m_chain(config->m_testnet ? BCHChainParams::testnet()
: BCHChainParams::mainnet()),
m_chain(is_regtest ? BCHChainParams::regtest()
: (config->m_testnet ? BCHChainParams::testnet()
: BCHChainParams::mainnet())),
m_pool(),
m_embedded(m_chain, m_pool, config->m_testnet),
m_node(context, config),
m_abla(config->m_testnet, anchor_height, m_chain),
m_connector(m_chain, m_pool) {}
m_connector(m_chain, m_pool) { m_regtest = is_regtest; }

EmbeddedDaemon(const EmbeddedDaemon&) = delete;
EmbeddedDaemon& operator=(const EmbeddedDaemon&) = delete;
Expand All @@ -105,12 +107,58 @@ class EmbeddedDaemon {
wire_chain_ingest(); // new_headers --> HeaderChain (advances synced height)
pin_cold_start_anchor(); // operator-APPROVED VM300 anchor (decisions@ 2026-06-18); floor-equivalent
const bool p2p_up = maybe_start_p2p(); // arm won-block P2P relay leg + connector re-request sink (gated on configured peer)
sync_mempool_from_rpc(); // RPC-fallback: BCHN loose-mempool txs never reach embedded P2P -> fill m_pool so populated templates (nTx>1) are built
LOG_INFO << "[EMB-BCH] embedded daemon up: embedded-primary work source,"
<< " external BCHN-RPC fallback retained, ABLA loop closed"
<< " (cold-start anchor pinned @" << BchnAnchorRecord::height << "),"
<< " P2P relay leg " << (p2p_up ? "LIVE" : "OFF (no peer configured -> RPC-only)") << ".";
}

/// Populate the embedded mempool from the external BCHN GBT (RPC-fallback
/// path). On regtest -- and any RPC-primary deploy -- loose transactions
/// live in BCHN's mempool and never reach the embedded P2P relay, so the
/// template builder selects from an empty m_pool and every won block is
/// coinbase-only (nTx=1). BCHN GBT already returns the CTOR-ordered,
/// fee-annotated, consensus-valid tx set -- the SAME set NodeRPC::getwork()
/// consumes for the external-RPC work path -- so we hand those to m_pool and
/// build_template selects them unchanged. No-op when no external RPC is bound
/// (a pure embedded-P2P deploy keeps its own relayed mempool).
/// KNOWN LIMITATION: one-shot at run()-startup; per-tip/periodic refresh is a
/// tracked production-standup follow-up (fenced, non-blocking for G3a).
/// p2pool-merged-v36 surface: NONE -- local mempool population only.
std::size_t sync_mempool_from_rpc() {
auto* r = m_node.rpc();
if (!r) return 0; // pure embedded-P2P deploy -> no RPC fill
std::size_t added = 0;
try {
auto wd = r->getwork(); // GBT-derived; m_txs unpacked, witness-free
const auto& jtx = (wd.m_data.is_object() && wd.m_data.contains("transactions")
&& wd.m_data["transactions"].is_array())
? wd.m_data["transactions"]
: nlohmann::json::array();
for (std::size_t i = 0; i < wd.m_txs.size(); ++i) {
MutableTransaction mtx(wd.m_txs[i]);
bool ok = false;
if (i < jtx.size() && jtx[i].is_object()
&& jtx[i].contains("fee") && jtx[i]["fee"].is_number()) {
// Authoritative per-tx fee from BCHN GBT -> fee_known=true so
// get_sorted_txs_with_fees() selects it into the template.
ok = m_pool.add_tx_with_known_fee(
mtx, static_cast<uint64_t>(jtx[i]["fee"].template get<int64_t>()));
} else {
ok = m_pool.add_tx(mtx); // no fee field -> UTXO/feeless fallback
}
if (ok) ++added;
}
LOG_INFO << "[EMB-BCH] mempool RPC-sync: +" << added
<< " txs from BCHN GBT (m_pool size=" << m_pool.size() << ")";
} catch (const std::exception& e) {
LOG_WARNING << "[EMB-BCH] mempool RPC-sync skipped (RPC fallback): "
<< e.what();
}
return added;
}

/// READ-ONLY IBD evidence harness entry (drives the --ibd run-loop in
/// main_bch.cpp). Brings up ONLY the network-free chain + the P2P front-end
/// pointed at a single BCHN peer (VM300 bchn-bch .110:8333), with the
Expand Down Expand Up @@ -367,6 +415,13 @@ class EmbeddedDaemon {
/// p2pool-merged-v36 surface (ABLA is BCH embedded-internal).
void pin_cold_start_anchor() {
using Rec = BchnAnchorRecord;
if (m_regtest) {
// Regtest has no mainnet/testnet BCHN anchor; the @955700 record is
// meaningless here. ABLA stays at its floor anchor; the chain syncs
// from the regtest genesis. Skipping the pin keeps regtest correct.
LOG_INFO << "[EMB-BCH] cold-start anchor SKIPPED (regtest: floor ABLA, genesis-rooted chain).";
return;
}
apply_bchn_anchor(Rec::height, Rec::state(m_config->m_testnet));
const uint64_t pinned = m_abla.tracker().budget_for_tip(Rec::height);
if (Rec::is_floor())
Expand Down Expand Up @@ -494,6 +549,7 @@ class EmbeddedDaemon {
}

private:
bool m_regtest = false; // regtest 3rd-net: skip mainnet/testnet anchor + drive RPC mempool sync
config_t* m_config; // not owned (binary entrypoint owns it)
HeaderChain m_chain; // before m_embedded + m_abla: their refs bind here
Mempool m_pool; // before m_embedded
Expand Down
Loading
Loading