Skip to content

Commit 3e73364

Browse files
authored
btc: arm submitblock RPC backup (ARM B) of the dual-path broadcaster (#744) (#787)
* btc: arm submitblock RPC backup leg (ARM B) of the dual-path broadcaster (#744) The embedded coin-net P2P relay (submit_block_for_connect -> submit_block_p2p_raw) is the always-primary daemonless broadcast arm and was already live-wired. The submitblock RPC backup (ARM B) existed in btc::coin::Node but m_rpc was never constructed in the run path (init_rpc was dead code that also forced getwork), so a won block could not reach a locally-run bitcoind -- the #744 gap. Add an opt-in submitblock backup mirroring the DGB reference (#82): - btc/coin/rpc_conf.hpp: header-only bitcoin.conf credential parser (twin of dgb/coin/rpc_conf.hpp). rpcpassword stays off the process table; --coin-rpc carries HOST:PORT only, --coin-rpc-auth points at the conf file. - btc::coin::Node::arm_submit_rpc(): constructs+connects NodeRPC for submitblock ONLY (no getwork side effect), plus has_rpc(). - main_btc: load creds (default ~/.bitcoin/bitcoin.conf), arm when armed()==true, otherwise stay UNARMED (daemonless default, byte-identical to before). - rpc_conf_test.cpp: 5 KATs (armed/unarmed/missing-file/aliases/endpoint-override). Broadcast-config path only -- no PoW hash, share format, coinbase commitment, or PPLNS math is touched (consensus-neutral). BTC-fenced; no shared/core edit. Builds clean (c2pool-btc + btc_share_test); KATs pass. * btc: harden ARM B submitblock backup — genesis, parser, throw-guard, socket deadline (#744/#787) Arming ARM B (m_rpc) made four previously-dead defects live on the reward path. Review (review) findings, all fixed: B1 (blocker) — NodeRPC::check() probed the LITECOIN genesis hash (copied from the LTC impl, a #759-class cross-coin drift). On BTC mainnet the is_main_chain && !has_block gate always failed -> permanent 15s reconnect loop, ARM B degraded on the one net where a lost block is real money. New btc/coin/genesis.hpp + btc_genesis_hash(IS_TESTNET); check() now probes the per-net BTC genesis (mirrors the DGB reference). genesis_check_test.cpp pins it (regression guard vs the LTC hash). B2 (blocker) — unguarded std::stoi in rpc_conf.hpp aborted the node at startup on a malformed rpcport (e.g. rpcport=abc) in the DEFAULT ~/.bitcoin/bitcoin.conf, crashing operators who never opted in. New conf_detail::parse_port (digits-only, range-checked, never throws) -> junk degrades to UNARMED. 3 new KATs. M1 (major) — a daemon unreachable at submit raised JsonRpcException out of submit_block_hex; broadcast_block_for_connect had no throw guards (unlike the core SSOT), so the exception destroyed ARM A's already-succeeded relay -> false 'reached NEITHER -- lost subsidy' alarm for a block that WAS relayed. Both legs now guarded. 3 new throwing-leg KATs. M2 (major) — no socket deadline on the beast tcp_stream shared with stratum on one single-threaded ioc; a hung bitcoind froze the whole node mid-won-block. Added apply_socket_timeouts() (SO_SND/RCVTIMEO, mirrors DASH #781), armed after each (re)connect. Also softened the arm_submit_rpc comment (check() does issue read-only probes). Broadcast/RPC-transport path only -- no PoW, share, coinbase-commitment, or PPLNS math touched (consensus-neutral). BTC-fenced. Builds clean (c2pool-btc + btc_share_test); 20 KATs pass.
1 parent a437a5c commit 3e73364

11 files changed

Lines changed: 553 additions & 6 deletions

src/c2pool/main_btc.cpp

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include <impl/btc/coin/transaction.hpp>
3232
#include <impl/btc/config.hpp>
3333
#include <impl/btc/config_pool.hpp>
34+
#include <impl/btc/coin/rpc_conf.hpp> // bitcoin.conf creds for the submitblock RPC backup (ARM B, #82/#744)
3435
#include <impl/btc/node.hpp>
3536
#include <impl/btc/auto_ratchet.hpp> // AutoRatchet V35->V36 forward-version-voting
3637
#include <impl/btc/share_check.hpp> // RefHashParams + compute_ref_hash_for_work
@@ -102,7 +103,13 @@ static void print_usage()
102103
" --prefix HEX c2pool sharechain PREFIX (hex, <=8 bytes), an\n"
103104
" INDEPENDENT per-network constant (no algebraic tie to\n"
104105
" IDENTIFIER). Supply with --network-id to join a custom\n"
105-
" p2pool chain; omit to use the compiled default prefix.\n";
106+
" p2pool chain; omit to use the compiled default prefix.\n"
107+
" --coin-rpc H:P submitblock RPC backup (ARM B) endpoint override.\n"
108+
" Endpoint only (no secret); creds come from bitcoin.conf.\n"
109+
" --coin-rpc-auth PATH bitcoin.conf-style file with rpcuser/rpcpassword\n"
110+
" for the submitblock backup (default ~/.bitcoin/\n"
111+
" bitcoin.conf; keeps rpcpassword off the process table).\n"
112+
" Omit both to run daemonless (embedded P2P relay only).\n";
106113
}
107114

108115
/// BTC wire-protocol magic bytes per network (pchMessageStart).
@@ -133,6 +140,8 @@ int main(int argc, char* argv[])
133140
uint16_t sharechain_port = 0; // 0 = default P2P_PORT (9333); --sharechain-port overrides (opt-in isolation)
134141
std::string network_id_hex; // --network-id: c2pool IDENTIFIER override (empty = public net)
135142
std::string prefix_hex; // --prefix: c2pool PREFIX override (empty = compiled default)
143+
std::string rpc_endpoint; // --coin-rpc HOST:PORT: submitblock backup endpoint override (no secret)
144+
std::string rpc_conf_path; // --coin-rpc-auth PATH: bitcoin.conf creds (default ~/.bitcoin/bitcoin.conf)
136145

137146
for (int i = 1; i < argc; ++i)
138147
{
@@ -221,6 +230,20 @@ int main(int argc, char* argv[])
221230
{
222231
prefix_hex = argv[++i];
223232
}
233+
else if (arg == "--coin-rpc" && i + 1 < argc)
234+
{
235+
// --coin-rpc HOST:PORT — endpoint override for the submitblock RPC
236+
// backup (ARM B). Endpoint ONLY (no secret) so it is safe on argv;
237+
// rpcuser/rpcpassword come from bitcoin.conf (--coin-rpc-auth).
238+
rpc_endpoint = argv[++i];
239+
}
240+
else if (arg == "--coin-rpc-auth" && i + 1 < argc)
241+
{
242+
// --coin-rpc-auth PATH — bitcoin.conf-style file carrying
243+
// rpcuser/rpcpassword for the submitblock RPC backup. Keeps the
244+
// rpcpassword OFF the process table (default ~/.bitcoin/bitcoin.conf).
245+
rpc_conf_path = argv[++i];
246+
}
224247
else
225248
{
226249
std::cerr << "unknown arg: " << arg << "\n";
@@ -379,6 +402,47 @@ int main(int argc, char* argv[])
379402

380403
btc::coin::Node<btc::Config> coin_node(&ioc, &config);
381404

405+
// ── #82/#744 submitblock RPC BACKUP arm (ARM B of the dual-path
406+
// broadcaster) ── The embedded coin-net P2P relay (submit_block_for_connect
407+
// -> submit_block_p2p_raw) is the always-primary daemonless path; this arms
408+
// the OPT-IN submitblock backup so a won block ALSO reaches a locally-run
409+
// bitcoind if the operator provisions one. Creds come from bitcoin.conf
410+
// (default ~/.bitcoin/bitcoin.conf, overridable with --coin-rpc-auth PATH)
411+
// so the rpcpassword NEVER touches argv; --coin-rpc HOST:PORT overrides only
412+
// the endpoint. No creds => arm stays UNARMED (has_rpc()==false) and
413+
// submit_block_hex returns false LOUDLY -- byte-identical to the daemonless
414+
// default. Mirrors main_dgb's NodeRPC arming (the #82 reference).
415+
{
416+
btc::coin::RpcConf rpc_conf;
417+
std::string conf_path = rpc_conf_path;
418+
if (conf_path.empty()) {
419+
const char* home = std::getenv("HOME");
420+
conf_path = std::string(home ? home : ".") + "/.bitcoin/bitcoin.conf";
421+
}
422+
if (btc::coin::load_rpc_conf(conf_path, rpc_conf)) {
423+
if (rpc_conf.port == 0) {
424+
// Bitcoin Core RPC defaults: mainnet 8332, testnet3 18332,
425+
// testnet4 48332, regtest 18443.
426+
rpc_conf.port = regtest ? 18443
427+
: (testnet4 ? 48332
428+
: (testnet ? 18332 : 8332));
429+
}
430+
btc::coin::apply_endpoint_override(rpc_endpoint, rpc_conf);
431+
} else {
432+
// No conf creds; --coin-rpc alone still lets an operator point the
433+
// endpoint, but without user/pass the arm cannot fire.
434+
btc::coin::apply_endpoint_override(rpc_endpoint, rpc_conf);
435+
}
436+
if (rpc_conf.armed()) {
437+
coin_node.arm_submit_rpc(NetService(rpc_conf.host, rpc_conf.port),
438+
rpc_conf.userpass());
439+
} else {
440+
LOG_INFO << "[BTC] submitblock RPC backup UNARMED "
441+
"(no bitcoin.conf creds; embedded P2P relay is the only "
442+
"broadcast arm)";
443+
}
444+
}
445+
382446
// Constants for getheaders driver: protocol version sent in the message
383447
// (matches what we advertised in version handshake — see B1 coin/p2p_node.hpp).
384448
constexpr uint32_t BTC_PROTOCOL_VERSION = 70016;

src/impl/btc/coin/block_broadcast.hpp

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,19 @@ inline bool broadcast_block_for_connect(
5252
const std::function<bool()>& relay_p2p,
5353
const std::function<bool()>& submit_rpc)
5454
{
55-
bool relayed = relay_p2p ? relay_p2p() : false; // best-effort fast propagation
56-
bool connected = submit_rpc ? submit_rpc() : false; // ALWAYS - connect-authoritative
55+
// BOTH legs guarded (mirrors core::broadcast_block_with_fallback): a throwing
56+
// sink must NEVER unwind out of the won-block path and destroy the OTHER
57+
// leg's result. In particular a throwing submit_rpc -- e.g. a daemon
58+
// unreachable at submit time raises JsonRpcException out of submit_block_hex
59+
// -- must NOT erase an already-succeeded P2P relay, else the caller falsely
60+
// reports "reached NEITHER -- lost subsidy" for a block that WAS relayed to
61+
// peers. This hole was newly reachable once ARM B (m_rpc) was armed (#744 M1).
62+
bool relayed = false;
63+
try { relayed = relay_p2p ? relay_p2p() : false; } // best-effort fast propagation
64+
catch (...) { relayed = false; }
65+
bool connected = false;
66+
try { connected = submit_rpc ? submit_rpc() : false; } // ALWAYS - connect-authoritative
67+
catch (...) { connected = false; }
5768
return relayed || connected;
5869
}
5970

src/impl/btc/coin/genesis.hpp

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
#pragma once
3+
4+
// ---------------------------------------------------------------------------
5+
// btc::coin genesis-hash SSOT for NodeRPC::check().
6+
//
7+
// check() probes getblockheader(<genesis>) to confirm the external daemon is a
8+
// real bitcoind on the SELECTED network (a wrong-coin / wrong-net daemon does
9+
// not answer for this hash). Historically check() hard-coded the *Litecoin*
10+
// genesis (copied verbatim from the LTC impl and never corrected), so on BTC
11+
// MAINNET the `is_main_chain && !has_block` gate always failed -> a permanent
12+
// 15s reconnect loop and a degraded ARM B on the one network where a lost block
13+
// is real money (#744/#787 B1, a #759-class cross-coin copy-paste drift).
14+
//
15+
// Per-net values sourced from bitcoin/src/kernel/chainparams.cpp and mirrored in
16+
// header_chain.hpp. Kept as a per-coin ISOLATION primitive (bucket-1): pinned
17+
// here so a standalone TU can guard against drift WITHOUT linking the beast
18+
// transport, exactly as dgb/coin/rpc_request.hpp does for DGB.
19+
// ---------------------------------------------------------------------------
20+
21+
namespace btc
22+
{
23+
namespace coin
24+
{
25+
26+
inline constexpr const char* BTC_GENESIS_MAIN =
27+
"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f";
28+
// testnet3 genesis. NodeRPC carries a single testnet bool; the check() gate only
29+
// bites on `is_main_chain` (mainnet), so testnet3/testnet4/regtest all bypass it
30+
// regardless (is_main_chain==false) -- the testnet value is the correct probe
31+
// for a standard testnet3 daemon and harmless for the other test nets.
32+
inline constexpr const char* BTC_GENESIS_TEST =
33+
"000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943";
34+
35+
// The genesis hash NodeRPC::check() probes for the selected network.
36+
inline const char* btc_genesis_hash(bool testnet)
37+
{
38+
return testnet ? BTC_GENESIS_TEST : BTC_GENESIS_MAIN;
39+
}
40+
41+
} // namespace coin
42+
} // namespace btc

src/impl/btc/coin/node.hpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,29 @@ class Node : public btc::interfaces::Node
101101
m_p2p->enable_mempool_request();
102102
}
103103

104+
/// Arm the submitblock RPC BACKUP leg (ARM B of the dual-path broadcaster)
105+
/// WITHOUT the getwork side effect init_rpc() carries. connect() runs the
106+
/// read-only liveness/softfork probe check() (getblockheader + getblockchaininfo
107+
/// + getnetworkinfo, results discarded by c2pool's own template path) but
108+
/// never getwork, so an external bitcoind we drive purely for submitblock
109+
/// arms cleanly and the embedded/daemonless template path is unperturbed.
110+
/// OPT-IN: main_btc calls
111+
/// this only when bitcoin.conf creds resolve (rpcpassword stays off the
112+
/// process table); otherwise m_rpc stays null, has_rpc()==false, and
113+
/// submit_block_hex returns false LOUDLY -- byte-identical to the daemonless
114+
/// default. Mirrors main_dgb's NodeRPC arming (the #82 reference). The
115+
/// embedded P2P relay (ARM A) remains the always-primary daemonless path.
116+
void arm_submit_rpc(const NetService& addr, const std::string& userpass)
117+
{
118+
m_rpc = std::make_unique<NodeRPC>(m_context, this, m_config->coin()->m_testnet);
119+
m_rpc->connect(addr, userpass);
120+
LOG_INFO << "[BTC] submitblock RPC backup ARMED: NodeRPC -> "
121+
<< addr.to_string() << " (creds from bitcoin.conf)";
122+
}
123+
124+
/// True once arm_submit_rpc has bound the submitblock backup leg.
125+
bool has_rpc() const { return m_rpc != nullptr; }
126+
104127
/// Submit a block via P2P directly (faster propagation than RPC).
105128
void submit_block_p2p(BlockType& block)
106129
{

src/impl/btc/coin/rpc.cpp

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
// SPDX-License-Identifier: AGPL-3.0-or-later
22
#include "rpc.hpp"
33

4+
#ifndef _WIN32
5+
#include <sys/socket.h> // setsockopt SO_SND/RCVTIMEO (Send() deadline, #744/#787 M2)
6+
#include <sys/time.h> // struct timeval
7+
#endif
8+
49
#include <impl/btc/config_pool.hpp>
510
#include <impl/btc/coin/softfork_check.hpp>
11+
#include <impl/btc/coin/genesis.hpp> // btc_genesis_hash — per-net check() probe (#744/#787 B1)
612

713
#include <core/log.hpp>
814
#include <core/hash.hpp>
@@ -65,6 +71,11 @@ void NodeRPC::connect(NetService address, std::string userpass)
6571
LOG_ERROR << "CoindRPC error when try connect: [" << ec.message() << "].";
6672
} else
6773
{
74+
// #744/#787 M2: arm the Send() socket deadline on the
75+
// freshly-connected socket BEFORE check() issues any
76+
// blocking RPC, so a daemon that connects but never
77+
// responds cannot wedge the ioc here either.
78+
apply_socket_timeouts();
6879
try
6980
{
7081
if (check())
@@ -128,9 +139,38 @@ void NodeRPC::sync_reconnect()
128139
LOG_WARNING << "CoindRPC sync_reconnect connect failed: " << ec.message();
129140
return;
130141
}
142+
apply_socket_timeouts(); // #744/#787 M2: re-arm the Send() deadline on the fresh socket
131143
LOG_INFO << "CoindRPC reconnected (sync)";
132144
}
133145

146+
void NodeRPC::apply_socket_timeouts()
147+
{
148+
// Force the socket back to BLOCKING mode, then set kernel send/receive
149+
// timeouts. async_connect leaves asio's non_blocking flag set; under it a
150+
// synchronous recv returns EAGAIN immediately and SO_RCVTIMEO is a no-op. In
151+
// blocking mode the sync http::write/http::read inside Send() do true
152+
// blocking send()/recv() which the kernel bounds by SO_SND/RCVTIMEO, so a
153+
// wedged bitcoind returns an error after RPC_IO_TIMEOUT_SECONDS instead of
154+
// hanging the whole ioc. (beast tcp_stream::expires_after governs only ASYNC
155+
// ops; Send() drives the sync path, so the socket-option deadline is the
156+
// correct lever -- same reasoning as the DASH #781 impl.) POSIX-only; on
157+
// Windows this is a no-op (pre-PR behaviour: no deadline). #744/#787 M2.
158+
#ifndef _WIN32
159+
if (!m_stream.socket().is_open())
160+
return;
161+
boost::system::error_code ec;
162+
m_stream.socket().non_blocking(false, ec); // guarantee SO_*TIMEO applies
163+
if (ec)
164+
LOG_WARNING << "CoindRPC: could not set blocking mode: " << ec.message();
165+
struct timeval tv;
166+
tv.tv_sec = RPC_IO_TIMEOUT_SECONDS;
167+
tv.tv_usec = 0;
168+
const int fd = m_stream.socket().native_handle();
169+
::setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
170+
::setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
171+
#endif
172+
}
173+
134174
std::string NodeRPC::Send(const std::string &request)
135175
{
136176
// Retry once after synchronous reconnect on write/read failure
@@ -195,7 +235,9 @@ nlohmann::json NodeRPC::CallAPIMethod(const std::string& method, const jsonrpccx
195235

196236
bool NodeRPC::check()
197237
{
198-
bool has_block = check_blockheader(uint256S("12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2"));
238+
// #744/#787 B1: probe the per-net BTC genesis (was the LITECOIN genesis,
239+
// copied from the LTC impl -- broke the mainnet handshake gate below).
240+
bool has_block = check_blockheader(uint256S(btc_genesis_hash(IS_TESTNET)));
199241
bool is_main_chain = getblockchaininfo()["chain"].get<std::string>() == "main";
200242
nlohmann::json blockchaininfo;
201243

src/impl/btc/coin/rpc.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ class NodeRPC : public jsonrpccxx::IClientConnector
4949
bool m_connected = false;
5050
std::unique_ptr<core::Timer> m_reconnect_timer;
5151

52+
// #744/#787 M2: a hung bitcoind (accepts TCP, never responds) must not freeze
53+
// the whole single-threaded ioc (stratum + sharechain P2P + header sync all
54+
// run on it) mid-won-block. apply_socket_timeouts() forces the socket to
55+
// blocking mode and sets kernel SO_SND/RCVTIMEO so Send()'s sync write/read
56+
// return an error after RPC_IO_TIMEOUT_SECONDS instead of hanging forever.
57+
// Mirrors the DASH #781 pattern. Called after each (re)connect.
58+
static constexpr int RPC_IO_TIMEOUT_SECONDS = 30;
59+
void apply_socket_timeouts();
60+
5261
std::string Send(const std::string &request) override;
5362
nlohmann::json CallAPIMethod(const std::string& method, const jsonrpccxx::positional_parameter& params = {});
5463

0 commit comments

Comments
 (0)