Skip to content

Commit 47598a0

Browse files
authored
Merge pull request #553 from frstrtr/bch/g2-work-source
bch(g2): coinbase-author KAT + slice-d pool-serve hot path
2 parents 5b12d50 + a121045 commit 47598a0

11 files changed

Lines changed: 592 additions & 75 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ jobs:
343343
bch_genesis_conformance_test bch_abla_growth_soak_test \
344344
bch_coinbase_kat_segwit_predicate_test \
345345
bch_coinbase_kat_bytevector_test \
346+
bch_coinbase_author_kat_test \
346347
bch_cashtokens_transparency_test \
347348
bch_block_connector_test bch_block_ordering_test \
348349
bch_utxo_connect_test bch_reorg_connect_test bch_reorg_rerequest_test \
@@ -436,6 +437,7 @@ jobs:
436437
bch_genesis_conformance_test bch_abla_growth_soak_test \
437438
bch_coinbase_kat_segwit_predicate_test \
438439
bch_coinbase_kat_bytevector_test \
440+
bch_coinbase_author_kat_test \
439441
bch_cashtokens_transparency_test \
440442
bch_block_connector_test bch_block_ordering_test \
441443
bch_utxo_connect_test bch_reorg_connect_test bch_reorg_rerequest_test \

scripts/gen_g2_oracle.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Oracle golden generator for BCH G2 coinbase-author KAT.
2+
# Transcribes VERBATIM the v36 output-assembly from p2pool-merged-v36
3+
# p2pool/data.py:920-1085 (generate_transaction, v36_active branch):
4+
# amounts = subsidy*weight//total_weight ;
5+
# total_donation = subsidy - sum(amounts) ;
6+
# if total_donation < 1 and subsidy>0: largest by (amount,script) -=1 ; recompute
7+
# amounts[DON] += total_donation
8+
# dests = sorted(scripts \ {DON}, key=(amounts[s], s))[-4000:]
9+
# payouts = [{value:amounts[s], script:s} for s in dests if amounts[s]] + [DON last]
10+
import struct
11+
12+
COMBINED_DONATION_SCRIPT = bytes.fromhex('a9148c6272621d89e8fa526dd86acff60c7136be8e8587')
13+
def p2pkh(b): return bytes.fromhex('76a914') + bytes([b])*20 + bytes.fromhex('88ac')
14+
A=p2pkh(0xaa); B=p2pkh(0xbb); C=p2pkh(0xcc); D=p2pkh(0xdd)
15+
16+
def assemble_v36(weights, donation_weight, subsidy):
17+
total_weight = sum(weights.values()) + donation_weight
18+
amounts = dict((s, subsidy*w//total_weight) for s,w in weights.items())
19+
total_donation = subsidy - sum(amounts.values())
20+
if total_donation < 1 and subsidy > 0:
21+
largest = max(amounts, key=lambda k:(amounts[k], k))
22+
amounts[largest] -= 1
23+
total_donation = subsidy - sum(amounts.values())
24+
amounts[COMBINED_DONATION_SCRIPT] = amounts.get(COMBINED_DONATION_SCRIPT,0) + total_donation
25+
excluded = {COMBINED_DONATION_SCRIPT}
26+
dests = sorted([s for s in amounts if s not in excluded], key=lambda s:(amounts[s], s))[-4000:]
27+
payouts = [(s, amounts[s]) for s in dests if amounts[s]]
28+
payouts.append((COMBINED_DONATION_SCRIPT, amounts[COMBINED_DONATION_SCRIPT]))
29+
return payouts
30+
31+
def assemble_with_finderfee(weights, finder, donation_weight, subsidy):
32+
# NEGATIVE control: pre-v36 math (199/200 haircut) + subsidy//200 finder fee.
33+
total_weight = sum(weights.values()) + donation_weight
34+
amounts = dict((s, subsidy*(199*w)//(200*total_weight)) for s,w in weights.items())
35+
amounts[finder] = amounts.get(finder,0) + subsidy//200
36+
total_donation = subsidy - sum(amounts.values())
37+
if total_donation < 1 and subsidy > 0:
38+
largest = max(amounts, key=lambda k:(amounts[k], k))
39+
amounts[largest] -= 1
40+
total_donation = subsidy - sum(amounts.values())
41+
amounts[COMBINED_DONATION_SCRIPT] = amounts.get(COMBINED_DONATION_SCRIPT,0) + total_donation
42+
dests = sorted([s for s in amounts if s != COMBINED_DONATION_SCRIPT], key=lambda s:(amounts[s], s))[-4000:]
43+
payouts = [(s, amounts[s]) for s in dests if amounts[s]]
44+
payouts.append((COMBINED_DONATION_SCRIPT, amounts[COMBINED_DONATION_SCRIPT]))
45+
return payouts
46+
47+
def varint(n):
48+
if n < 0xfd: return bytes([n])
49+
if n <= 0xffff: return b'\xfd'+struct.pack('<H',n)
50+
if n <= 0xffffffff: return b'\xfe'+struct.pack('<I',n)
51+
return b'\xff'+struct.pack('<Q',n)
52+
53+
def serialize(payouts):
54+
out = b''
55+
for script, value in payouts:
56+
out += struct.pack('<Q', value) + varint(len(script)) + script
57+
return out
58+
59+
SUB = 1_000_000_000
60+
cases = {
61+
'CASE1_ordering_tie': assemble_v36({A:10,B:20,C:20,D:50}, 0, SUB),
62+
'CASE2_donation_forced_last': assemble_v36({A:40,B:20}, 40, SUB),
63+
'CASE3_v36_no_finderfee': assemble_v36({A:60,B:40}, 0, SUB),
64+
'CASE3_NEG_with_finderfee': assemble_with_finderfee({A:60,B:40}, A, 0, SUB),
65+
}
66+
names = {A.hex():'A',B.hex():'B',C.hex():'C',D.hex():'D',COMBINED_DONATION_SCRIPT.hex():'DON'}
67+
for name, p in cases.items():
68+
print('=== %s ===' % name)
69+
for s,v in p: print(' %-4s value=%d' % (names.get(s.hex(),'?'), v))
70+
print(' OUTSECTION_HEX=%s' % serialize(p).hex())

src/c2pool/main_bch.cpp

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
#include <impl/bch/coin/regtest_block.hpp>
3535
#include <impl/bch/coin/rpc.hpp>
3636
#include <impl/bch/coin/node.hpp>
37+
#include <impl/bch/pool_entrypoint.hpp>
38+
#include <btclibs/util/strencodings.h> // ParseHexBytes (sharechain prefix)
3739

3840
#include <core/core_util.hpp>
3941

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

589+
590+
// --pool: PRODUCTION pool run-loop -- the first non-harness c2pool-bch
591+
// entrypoint. Stands up the BCH pool node + embedded coin daemon on one shared
592+
// io_context via bch::standup_pool_run, with the won-block sink bound (dual
593+
// path: embedded P2P primary + BCHN submitblock fallback) and, when --stratum
594+
// is given, the miner-facing BCHWorkSource + core::StratumServer listening so a
595+
// genuine share-author coinbase is what gets assembled and broadcast. The
596+
// --ibd path is a read-only evidence harness; THIS path is the real node.
597+
//
598+
// Config is built WITHOUT a YAML load (matches run_ibd): no pool.yaml/coin.yaml
599+
// is required for the slice. The two embedded-daemon wire fields (P2P magic +
600+
// BCHN peer) and the sharechain PREFIX (bucket-1 isolation primitive, never
601+
// standardized) are set by hand from BCH chainparams. The external BCHN-RPC
602+
// fallback inside EmbeddedDaemon::run() is retained (external_fallback law).
603+
//
604+
// p2pool-merged-v36 surface: NONE -- run-loop bring-up + block dispatch, not
605+
// share/PPLNS/coinbase/PoW bytes. PER-COIN ISOLATION: src/impl/bch only.
606+
int run_pool(const std::string& peer_host, uint16_t peer_port, bool testnet,
607+
bool regtest, uint32_t anchor_height,
608+
const std::string& stratum_addr, uint16_t stratum_port)
609+
{
610+
boost::asio::io_context ioc;
611+
612+
bch::PoolConfig::is_testnet = testnet;
613+
614+
bch::Config config("bch");
615+
// Skip Config::init() (no on-disk pool.yaml/coin.yaml); set only the fields
616+
// the run-loop touches, from BCH chainparams.
617+
config.coin()->m_testnet = testnet || regtest;
618+
config.coin()->m_symbol = "BCH";
619+
config.coin()->m_p2p.address = NetService(peer_host, peer_port);
620+
// BCH P2P network magic (pchMessageStart, BCHN chainparams.cpp): mainnet
621+
// e3e1f3e8, testnet3 f4e5f3f4, regtest dab5bffa. Wrong magic == BCHN drops
622+
// the peer with EOF right after connect.
623+
config.coin()->m_p2p.prefix = regtest
624+
? std::vector<std::byte>{ std::byte{0xda}, std::byte{0xb5}, std::byte{0xbf}, std::byte{0xfa} }
625+
: (testnet
626+
? std::vector<std::byte>{ std::byte{0xf4}, std::byte{0xe5}, std::byte{0xf3}, std::byte{0xf4} }
627+
: std::vector<std::byte>{ std::byte{0xe3}, std::byte{0xe1}, std::byte{0xf3}, std::byte{0xe8} });
628+
// Sharechain identity: BCH p2pool PREFIX namespaces the sharechain P2P
629+
// framing. standup_pool_run's Node reads pool()->m_prefix.
630+
config.pool()->m_prefix = ParseHexBytes(bch::PoolConfig::prefix_hex());
631+
632+
std::cout
633+
<< "[pool] c2pool-bch pool run-loop"
634+
<< (regtest ? " (regtest)" : (testnet ? " (testnet)" : " (mainnet)"))
635+
<< " -- BCHN peer " << peer_host << ":" << peer_port
636+
<< ", cold-start anchor=" << anchor_height;
637+
if (stratum_port)
638+
std::cout << ", stratum " << stratum_addr << ":" << stratum_port;
639+
else
640+
std::cout << ", stratum DISABLED (no --stratum)";
641+
std::cout << "\n";
642+
643+
try {
644+
bch::standup_pool_run(ioc, config, anchor_height,
645+
stratum_addr, stratum_port, testnet || regtest, regtest);
646+
} catch (const std::exception& e) {
647+
std::cout << "[pool] FATAL: " << e.what() << "\n";
648+
return 1;
649+
}
650+
return 0;
651+
}
652+
586653
} // namespace
587654

588655
int main(int argc, char** argv)
@@ -592,6 +659,11 @@ int main(int argc, char** argv)
592659
bool want_leg_c = false;
593660
bool want_leg_c_p2p = false;
594661
bool want_with_peer_verify = false;
662+
bool want_pool = false;
663+
bool regtest = false;
664+
std::string stratum_addr = "0.0.0.0";
665+
uint16_t stratum_port = 0; // 0 disables stratum; --stratum sets it
666+
uint32_t anchor_height = 0; // cold-start ABLA floor anchor
595667
uint16_t leg_c_p2p_port = 18444; // BCHN regtest P2P default
596668
std::string rpc_conf;
597669
bool testnet = false;
@@ -609,6 +681,20 @@ int main(int argc, char** argv)
609681
if (std::strcmp(argv[i], "--help") == 0) want_help = true;
610682
if (std::strcmp(argv[i], "--ibd") == 0) want_ibd = true;
611683
if (std::strcmp(argv[i], "--with-peer-verify") == 0) want_with_peer_verify = true;
684+
if (std::strcmp(argv[i], "--pool") == 0) want_pool = true;
685+
if (std::strcmp(argv[i], "--regtest") == 0) { regtest = true; testnet = true; port = 18444; }
686+
if (std::strcmp(argv[i], "--anchor") == 0 && i + 1 < argc)
687+
anchor_height = static_cast<uint32_t>(std::stoul(argv[++i]));
688+
if (std::strcmp(argv[i], "--stratum") == 0 && i + 1 < argc) {
689+
std::string sp = argv[++i];
690+
const auto c = sp.rfind(char(58)); // ASCII colon
691+
if (c != std::string::npos) {
692+
stratum_addr = sp.substr(0, c);
693+
stratum_port = static_cast<uint16_t>(std::stoul(sp.substr(c + 1)));
694+
} else {
695+
stratum_port = static_cast<uint16_t>(std::stoul(sp));
696+
}
697+
}
612698
if (std::strcmp(argv[i], "--leg-c-capture") == 0) want_leg_c = true;
613699
if (std::strcmp(argv[i], "--leg-c-capture-p2p") == 0) want_leg_c_p2p = true;
614700
if (std::strcmp(argv[i], "--p2p-port") == 0 && i + 1 < argc)
@@ -651,6 +737,9 @@ int main(int argc, char** argv)
651737
return run_leg_c_capture(rpc_conf);
652738
}
653739

740+
if (want_pool)
741+
return run_pool(host, port, testnet, regtest, anchor_height, stratum_addr, stratum_port);
742+
654743
if (want_with_peer_verify)
655744
return run_with_peer_verify(host, port, testnet, max_seconds);
656745

src/impl/bch/coin/asert.hpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,31 @@ inline ASERTParams asert_testnet3() {
7474
};
7575
}
7676

77+
inline uint256 bch_pow_limit_regtest() {
78+
// BCH regtest powLimit (BCHN CRegTestParams). NOTE: top byte 0x7f does NOT
79+
// satisfy CalculateASERT's 32-leading-zero-bits invariant -- which is why
80+
// regtest runs with fPowNoRetargeting (ASERT is never invoked on regtest).
81+
uint256 v;
82+
v.SetHex("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
83+
return v;
84+
}
85+
86+
/// BCH regtest difficulty params (BCHN CRegTestParams). fPowNoRetargeting=true,
87+
/// so the required target is fixed at the powLimit nBits (0x207fffff) for every
88+
/// block -- ASERT is bypassed (header_chain honours BCHChainParams::no_retargeting).
89+
/// The anchor here is nominal (genesis) and never feeds CalculateASERT.
90+
/// allow_min_difficulty mirrors BCHN regtest. (FINDING3: regtest must be its own
91+
/// net so --pool --regtest serves diff-1 0x207fffff, not the testnet anchor.)
92+
inline ASERTParams asert_regtest() {
93+
return ASERTParams{
94+
ASERTAnchor{0, 0x207fffff, 1296688602}, // genesis nBits/time (nominal)
95+
2 * 24 * 60 * 60,
96+
BCH_TARGET_SPACING,
97+
true,
98+
bch_pow_limit_regtest(),
99+
};
100+
}
101+
77102
inline ASERTParams asert_testnet4() {
78103
return ASERTParams{
79104
ASERTAnchor{16844, 0x1d00ffff, 1605451779},

src/impl/bch/coin/embedded_daemon.hpp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,12 @@ class EmbeddedDaemon {
8181
/// Cold-start ctor: floor-anchored ABLA at `anchor_height` (safe default
8282
/// when no VM300 BCHN pin is available yet). `context`/`config` outlive the
8383
/// daemon (owned by the binary entrypoint, same contract as coin::Node).
84-
EmbeddedDaemon(auto* context, config_t* config, uint32_t anchor_height)
84+
EmbeddedDaemon(auto* context, config_t* config, uint32_t anchor_height,
85+
bool is_regtest = false)
8586
: m_config(config),
86-
m_chain(config->m_testnet ? BCHChainParams::testnet()
87-
: BCHChainParams::mainnet()),
87+
m_chain(is_regtest ? BCHChainParams::regtest()
88+
: (config->m_testnet ? BCHChainParams::testnet()
89+
: BCHChainParams::mainnet())),
8890
m_pool(),
8991
m_embedded(m_chain, m_pool, config->m_testnet),
9092
m_node(context, config),

src/impl/bch/coin/header_chain.hpp

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ struct BCHChainParams {
150150
uint256 pow_limit; // == asert.pow_limit (mirror for check_pow)
151151
uint256 genesis_hash; // SHA256d genesis block hash (identification)
152152
bool allow_min_difficulty{false}; // == asert.allow_min_difficulty (testnet)
153+
bool no_retargeting{false}; // == BCHN fPowNoRetargeting (regtest)
153154

154155
// Fast-start checkpoint: skip syncing from genesis, start from a recent height.
155156
// The header chain seeds this checkpoint as if it were the genesis block.
@@ -196,6 +197,25 @@ struct BCHChainParams {
196197
p.fast_start_checkpoint = Checkpoint{0, p.genesis_hash};
197198
return p;
198199
}
200+
201+
/// BCH regtest params (P2P port 18444). BCHN CRegTestParams:
202+
/// fPowNoRetargeting + fPowAllowMinDifficultyBlocks -- difficulty is fixed at
203+
/// the genesis nBits 0x207fffff (powLimit), so a single CPU can mine it.
204+
/// Genesis is the shared Satoshi regtest genesis. Seeds the cold-start tip so
205+
/// served work nbits == 0x207fffff BEFORE the first P2P regtest block arrives
206+
/// (FINDING3: --pool --regtest previously collapsed to testnet params and
207+
/// served unwinnable testnet difficulty). GBT-from-local-regtest-BCHN
208+
/// (regtest_block::build_from_gbt) is a deferred follow-on slice.
209+
static BCHChainParams regtest() {
210+
BCHChainParams p;
211+
p.asert = asert_regtest();
212+
p.pow_limit = p.asert.pow_limit;
213+
p.allow_min_difficulty = p.asert.allow_min_difficulty;
214+
p.no_retargeting = true;
215+
p.genesis_hash.SetHex("0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206");
216+
p.fast_start_checkpoint = Checkpoint{0, p.genesis_hash};
217+
return p;
218+
}
199219
};
200220

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

728+
// Regtest (BCHN fPowNoRetargeting): difficulty never adjusts -- every
729+
// block carries the powLimit nBits (0x207fffff). ASERT is bypassed here:
730+
// CalculateASERT would assert on the regtest powLimit (top byte 0x7f
731+
// violates its 32-leading-zero-bits invariant). The bits/target are
732+
// already gated against pow_limit by check_pow at the accept site, so
733+
// accept. (FINDING3.)
734+
if (m_params.no_retargeting) return true;
735+
708736
// Get tip (the block we're building on).
709737
auto prev_opt = lookup_header_internal(header.m_previous_block);
710738
if (!prev_opt) return false;

0 commit comments

Comments
 (0)