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
8 changes: 5 additions & 3 deletions src/impl/bch/coin/embedded_daemon.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,12 @@ 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),
Expand Down
28 changes: 28 additions & 0 deletions src/impl/bch/coin/header_chain.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ struct BCHChainParams {
uint256 pow_limit; // == asert.pow_limit (mirror for check_pow)
uint256 genesis_hash; // SHA256d genesis block hash (identification)
bool allow_min_difficulty{false}; // == asert.allow_min_difficulty (testnet)
bool no_retargeting{false}; // == BCHN fPowNoRetargeting (regtest)

// Fast-start checkpoint: skip syncing from genesis, start from a recent height.
// The header chain seeds this checkpoint as if it were the genesis block.
Expand Down Expand Up @@ -196,6 +197,25 @@ struct BCHChainParams {
p.fast_start_checkpoint = Checkpoint{0, p.genesis_hash};
return p;
}

/// BCH regtest params (P2P port 18444). BCHN CRegTestParams:
/// fPowNoRetargeting + fPowAllowMinDifficultyBlocks -- difficulty is fixed at
/// the genesis nBits 0x207fffff (powLimit), so a single CPU can mine it.
/// Genesis is the shared Satoshi regtest genesis. Seeds the cold-start tip so
/// served work nbits == 0x207fffff BEFORE the first P2P regtest block arrives
/// (FINDING3: --pool --regtest previously collapsed to testnet params and
/// served unwinnable testnet difficulty). GBT-from-local-regtest-BCHN
/// (regtest_block::build_from_gbt) is a deferred follow-on slice.
static BCHChainParams regtest() {
BCHChainParams p;
p.asert = asert_regtest();
p.pow_limit = p.asert.pow_limit;
p.allow_min_difficulty = p.asert.allow_min_difficulty;
p.no_retargeting = true;
p.genesis_hash.SetHex("0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206");
p.fast_start_checkpoint = Checkpoint{0, p.genesis_hash};
return p;
}
};

// ─── PoW Functions ──────────────────────────────────────────────────────────
Expand Down Expand Up @@ -705,6 +725,14 @@ class HeaderChain {
bool validate_difficulty(const BlockHeaderType& header, uint32_t new_height) {
if (new_height < 2) return true; // genesis + first block

// Regtest (BCHN fPowNoRetargeting): difficulty never adjusts -- every
// block carries the powLimit nBits (0x207fffff). ASERT is bypassed here:
// CalculateASERT would assert on the regtest powLimit (top byte 0x7f
// violates its 32-leading-zero-bits invariant). The bits/target are
// already gated against pow_limit by check_pow at the accept site, so
// accept. (FINDING3.)
if (m_params.no_retargeting) return true;

// Get tip (the block we're building on).
auto prev_opt = lookup_header_internal(header.m_previous_block);
if (!prev_opt) return false;
Expand Down
Loading
Loading