Skip to content

Commit 21f4c82

Browse files
authored
Merge pull request #506 from frstrtr/btc/regtest-mode
feat(btc): add --regtest net mode to c2pool-btc daemon
2 parents b886efb + cd02911 commit 21f4c82

6 files changed

Lines changed: 133 additions & 15 deletions

File tree

src/c2pool/main_btc.cpp

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -70,16 +70,18 @@ namespace io = boost::asio;
7070
static void print_usage()
7171
{
7272
std::cerr <<
73-
"Usage: c2pool-btc [--testnet | --testnet4] --bitcoind HOST:PORT\n"
73+
"Usage: c2pool-btc [--testnet | --testnet4 | --regtest] --bitcoind HOST:PORT\n"
7474
" [--p2pool HOST:PORT]\n"
7575
"\n"
7676
" --testnet BTC testnet3 chain (genesis 000000000933ea01...)\n"
7777
" --testnet4 BTC testnet4 chain (genesis 00000000da84f2ba...)\n"
78+
" --regtest BTC regtest chain (genesis 0f9188f13cb7b2c7...)\n"
7879
" default: mainnet\n"
7980
" --bitcoind H:P bitcoind P2P endpoint host:port\n"
8081
" e.g. 127.0.0.1:8333 (mainnet)\n"
8182
" 127.0.0.1:18333 (testnet3)\n"
8283
" 127.0.0.1:48333 (testnet4)\n"
84+
" 127.0.0.1:18443 (regtest)\n"
8385
" --p2pool H:P BTC p2pool peer (jtoomim/SPB v35 + protocol 3502)\n"
8486
" e.g. p2p-spb.xyz:9333\n"
8587
" --stratum [H:]P stratum TCP listener for miners (B4-stratum)\n"
@@ -101,10 +103,11 @@ static void print_usage()
101103

102104
/// BTC wire-protocol magic bytes per network (pchMessageStart).
103105
/// Source: ref/bitcoin/src/kernel/chainparams.cpp.
104-
static std::vector<std::byte> btc_magic_bytes(bool testnet, bool testnet4)
106+
static std::vector<std::byte> btc_magic_bytes(bool testnet, bool testnet4, bool regtest)
105107
{
106108
std::string hex;
107-
if (testnet4) hex = "1c163f28"; // testnet4 (line 335-338)
109+
if (regtest) hex = "fabfb5da"; // regtest (CRegTestParams)
110+
else if (testnet4) hex = "1c163f28"; // testnet4 (line 335-338)
108111
else if (testnet) hex = "0b110907"; // testnet3 (line 235-238)
109112
else hex = "f9beb4d9"; // mainnet (line 117-120)
110113
return ParseHexBytes(hex);
@@ -116,6 +119,7 @@ int main(int argc, char* argv[])
116119

117120
bool testnet = false;
118121
bool testnet4 = false;
122+
bool regtest = false;
119123
std::string bitcoind_host;
120124
uint16_t bitcoind_port = 0;
121125
std::string p2pool_host;
@@ -143,6 +147,10 @@ int main(int argc, char* argv[])
143147
testnet = true;
144148
testnet4 = true;
145149
}
150+
else if (arg == "--regtest")
151+
{
152+
regtest = true;
153+
}
146154
else if (arg == "--bitcoind" && i + 1 < argc)
147155
{
148156
std::string ep = argv[++i];
@@ -223,14 +231,17 @@ int main(int argc, char* argv[])
223231
std::cerr << "[BTC] warning: --prefix ignored without --network-id\n";
224232
btc::PoolConfig::set_network_id(network_id_hex, prefix_hex);
225233

226-
auto chain_params = testnet4
227-
? btc::coin::BTCChainParams::testnet4()
228-
: (testnet ? btc::coin::BTCChainParams::testnet()
229-
: btc::coin::BTCChainParams::mainnet());
234+
auto chain_params = regtest
235+
? btc::coin::BTCChainParams::regtest()
236+
: (testnet4
237+
? btc::coin::BTCChainParams::testnet4()
238+
: (testnet ? btc::coin::BTCChainParams::testnet()
239+
: btc::coin::BTCChainParams::mainnet()));
230240

231-
const std::string net_subdir = testnet4 ? "bitcoin_testnet4"
241+
const std::string net_subdir = regtest ? "bitcoin_regtest"
242+
: (testnet4 ? "bitcoin_testnet4"
232243
: (testnet ? "bitcoin_testnet"
233-
: "bitcoin");
244+
: "bitcoin"));
234245

235246
const std::filesystem::path net_dir = core::filesystem::config_path() / net_subdir;
236247
std::error_code ec;
@@ -240,7 +251,7 @@ int main(int argc, char* argv[])
240251
const std::string utxo_db_path = (net_dir / "utxo_view_db").string();
241252

242253
LOG_INFO << "[BTC] c2pool-btc starting — net="
243-
<< (testnet4 ? "testnet4" : (testnet ? "testnet3" : "mainnet"));
254+
<< (regtest ? "regtest" : (testnet4 ? "testnet4" : (testnet ? "testnet3" : "mainnet")));
244255
LOG_INFO << "[BTC] HeaderChain DB: " << chain_db_path;
245256
LOG_INFO << "[BTC] UTXO DB: " << utxo_db_path;
246257
LOG_INFO << "[BTC] Genesis: " << chain_params.genesis_hash.GetHex();
@@ -346,9 +357,10 @@ int main(int argc, char* argv[])
346357
btc::Config config(net_subdir);
347358
// Skip Config::init() — it would try to load pool.yaml + coin.yaml
348359
// from disk; for B2-net smoke we set fields directly from chainparams.
349-
config.coin()->m_p2p.prefix = btc_magic_bytes(testnet, testnet4);
360+
config.coin()->m_p2p.prefix = btc_magic_bytes(testnet, testnet4, regtest);
350361
config.coin()->m_p2p.address = NetService(bitcoind_host, bitcoind_port);
351-
config.coin()->m_testnet = testnet;
362+
config.coin()->m_testnet = testnet || regtest;
363+
config.coin()->m_regtest = regtest;
352364
config.coin()->m_symbol = "BTC";
353365

354366
btc::coin::Node<btc::Config> coin_node(&ioc, &config);
@@ -668,6 +680,14 @@ int main(int argc, char* argv[])
668680
<< p2pool_host << ":" << p2pool_port
669681
<< " (addrs.json reset to enforce exclusive target)";
670682
}
683+
else if (regtest)
684+
{
685+
// Isolated regtest standup: NEVER dial the public mainnet p2pool seeds
686+
// (DEFAULT_BOOTSTRAP_HOSTS). Leaving the addr store empty keeps the
687+
// sharechain solo/local so a won block is never relayed to real peers.
688+
// Supply --p2pool HOST:PORT to dial an explicit isolated tuned-net peer.
689+
LOG_INFO << "[BTC] Sharechain bootstrap: regtest — 0 public seeds (isolated)";
690+
}
671691
else
672692
{
673693
// Default seed list (PoolConfig::DEFAULT_BOOTSTRAP_HOSTS, port 9333).

src/impl/btc/coin/header_chain.hpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,24 @@ struct BTCChainParams {
212212
return p;
213213
}
214214

215+
/// BTC regtest params (p2p port 18444) — local block-production / bitaxe
216+
/// testbed. powLimit + genesis + fPowNoRetargeting per Bitcoin Core
217+
/// CRegTestParams (ref/bitcoin/src/kernel/chainparams.cpp). Min-diff +
218+
/// no-retarget so a single rig (or generatetoaddress) produces blocks now.
219+
static BTCChainParams regtest() {
220+
BTCChainParams p;
221+
p.target_timespan = MAINNET_TARGET_TIMESPAN;
222+
p.target_spacing = MAINNET_TARGET_SPACING;
223+
p.allow_min_difficulty = true;
224+
p.no_retargeting = true;
225+
// Regtest powLimit (CRegTestParams): nBits 0x207fffff.
226+
p.pow_limit.SetHex("7fffff0000000000000000000000000000000000000000000000000000000000");
227+
// Regtest genesis (Bitcoin Core CRegTestParams).
228+
p.genesis_hash.SetHex("0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206");
229+
p.fast_start_checkpoint = Checkpoint{0, p.genesis_hash};
230+
return p;
231+
}
232+
215233
int64_t difficulty_adjustment_interval() const {
216234
return target_timespan / target_spacing;
217235
}

src/impl/btc/config_coin.hpp

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include <core/config.hpp>
44
#include <core/fileconfig.hpp>
55
#include <core/netaddress.hpp>
6+
#include <string>
67

78
#include <yaml-cpp/yaml.h>
89

@@ -68,7 +69,23 @@ template<> struct convert<btc::config::RPCData>
6869

6970
namespace btc
7071
{
71-
72+
73+
// Sharechain LevelDB + P2P-listen namespace isolation.
74+
//
75+
// regtest MUST be evaluated FIRST: main_btc resets CoinConfig::m_testnet to
76+
// false under --regtest (it drives only the parent chainparams), so a
77+
// testnet-only switch would resolve to "bitcoin" = MAINNET and silently join
78+
// the production p2pool sharechain -- the .121 standup incident of
79+
// 2026-06-26, where a won regtest block would have relayed to real peers.
80+
// Pure free function so the isolation invariant is lockable without standing
81+
// up a node. Locked by regtest_sharechain_isolation_test.cpp.
82+
inline std::string sharechain_net_name(bool regtest, bool testnet)
83+
{
84+
if (regtest) return "bitcoin_regtest";
85+
if (testnet) return "bitcoin_testnet";
86+
return "bitcoin";
87+
}
88+
7289
class CoinConfig : protected core::Fileconfig
7390
{
7491

@@ -90,6 +107,7 @@ class CoinConfig : protected core::Fileconfig
90107
std::string m_symbol;
91108
int m_share_period{};
92109
bool m_testnet {false};
110+
bool m_regtest {false}; // --regtest: isolated sharechain net namespace (bitcoin_regtest)
93111
// std::string coin_prefix; //TODO: const unsigned char*? + int identifier lenght
94112
// int32_t block_period;
95113
// std::string p2p_address;

src/impl/btc/node.hpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,14 @@ class NodeImpl : public pool::BaseNode<btc::Config, btc::ShareChain, btc::Peer>
175175
m_chain = &m_tracker.chain;
176176

177177
// Open LevelDB storage and load any persisted shares
178-
std::string net_name = config->m_testnet ? "bitcoin_testnet" : "bitcoin";
178+
// Sharechain LevelDB + listen namespace must isolate by net so a
179+
// regtest standup never shares a dir/namespace with mainnet shares.
180+
// (regtest checked FIRST: under --regtest CoinConfig::m_testnet is
181+
// reset to false in main_btc, so a testnet-only switch would resolve
182+
// to "bitcoin" = MAINNET and silently join the prod sharechain.)
183+
// Isolation invariant extracted to a pure helper (locked by
184+
// regtest_sharechain_isolation_test.cpp); regtest checked first.
185+
std::string net_name = sharechain_net_name(config->m_regtest, config->m_testnet);
179186
m_storage = std::make_unique<c2pool::storage::SharechainStorage>(net_name);
180187
load_persisted_shares();
181188

src/impl/btc/test/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
if (BUILD_TESTING AND GTest_FOUND)
22
# btc twin of ltc share_test — uniquely named to avoid the CMP0002 target
33
# collision with src/impl/ltc/test (both subdirs build in the same tree).
4-
add_executable(btc_share_test share_test.cpp f11_donation_invariance_test.cpp f10b_version_punish_removal_test.cpp g01_share_format_parity_test.cpp auto_ratchet_sim_test.cpp block_broadcast_guard_test.cpp)
4+
add_executable(btc_share_test share_test.cpp f11_donation_invariance_test.cpp f10b_version_punish_removal_test.cpp g01_share_format_parity_test.cpp auto_ratchet_sim_test.cpp block_broadcast_guard_test.cpp regtest_sharechain_isolation_test.cpp)
55
target_link_libraries(btc_share_test PRIVATE
66
GTest::gtest_main GTest::gtest
77
core btc
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// ---------------------------------------------------------------------------
2+
// btc::sharechain_net_name isolation KATs.
3+
//
4+
// Regression guard for the .121 standup incident (2026-06-26): under --regtest
5+
// main_btc resets CoinConfig::m_testnet to false (it drives only the parent
6+
// chainparams), so the sharechain LevelDB + P2P-listen namespace -- which had
7+
// resolved off m_testnet ALONE -- silently namespaced to "bitcoin" = MAINNET
8+
// and joined the production p2pool sharechain. A won regtest block would then
9+
// have relayed an on_block_found share to real mainnet peers (a prod touch on
10+
// an isolated VM). The fix evaluates regtest FIRST in a pure helper; these
11+
// KATs lock that ordering so the silent-mainnet-join can never recur.
12+
//
13+
// Rides the already-allowlisted btc_share_test executable -- no build.yml
14+
// --target allowlist change is needed and there is no NOT_BUILT sentinel risk.
15+
// p2pool-merged-v36 surface: NONE (transport namespace, not consensus).
16+
// ---------------------------------------------------------------------------
17+
18+
#include <gtest/gtest.h>
19+
20+
#include "../config_coin.hpp"
21+
22+
// The incident itself: regtest=true with testnet=false (main_btc resets it).
23+
// This MUST isolate to bitcoin_regtest; the pre-fix code returned "bitcoin".
24+
TEST(RegtestSharechainIsolation, RegtestWithTestnetResetDoesNotJoinMainnet)
25+
{
26+
EXPECT_EQ(btc::sharechain_net_name(/*regtest=*/true, /*testnet=*/false),
27+
"bitcoin_regtest");
28+
// Red-able: if regtest were not checked first, this would be "bitcoin".
29+
EXPECT_NE(btc::sharechain_net_name(true, false), "bitcoin");
30+
}
31+
32+
// regtest takes precedence even if a testnet flag is somehow also set.
33+
TEST(RegtestSharechainIsolation, RegtestWinsOverTestnet)
34+
{
35+
EXPECT_EQ(btc::sharechain_net_name(true, true), "bitcoin_regtest");
36+
}
37+
38+
// The two non-regtest paths are unchanged (no regression for prod/testnet).
39+
TEST(RegtestSharechainIsolation, TestnetAndMainnetUnchanged)
40+
{
41+
EXPECT_EQ(btc::sharechain_net_name(false, true), "bitcoin_testnet");
42+
EXPECT_EQ(btc::sharechain_net_name(false, false), "bitcoin");
43+
}
44+
45+
// The three namespaces are pairwise distinct -- no two nets can ever share a
46+
// sharechain LevelDB dir / listen namespace.
47+
TEST(RegtestSharechainIsolation, AllThreeNamespacesDistinct)
48+
{
49+
const auto rt = btc::sharechain_net_name(true, false);
50+
const auto tn = btc::sharechain_net_name(false, true);
51+
const auto mn = btc::sharechain_net_name(false, false);
52+
EXPECT_NE(rt, tn);
53+
EXPECT_NE(rt, mn);
54+
EXPECT_NE(tn, mn);
55+
}

0 commit comments

Comments
 (0)