Skip to content

Commit d1e2e6e

Browse files
authored
Merge pull request #269 from frstrtr/dgb/82-noderpc-arm-conf-creds
dgb(#82): arm external-daemon submitblock RPC leg from digibyte.conf creds
2 parents 294fb30 + 1f58458 commit d1e2e6e

4 files changed

Lines changed: 238 additions & 3 deletions

File tree

src/c2pool/main_dgb.cpp

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838
#include <impl/dgb/coin/mempool_ingest.hpp>
3939
#include <impl/dgb/stratum/work_source.hpp>
4040
#include <impl/dgb/coin/p2p_node.hpp>
41+
#include <impl/dgb/coin/rpc.hpp> // NodeRPC — external-daemon submitblock arm (#82)
42+
#include <impl/dgb/coin/rpc_conf.hpp> // digibyte.conf creds resolution (rpcpassword off argv)
43+
#include <impl/dgb/config_coin.hpp> // dgb::CoinParams::MAINNET_RPC_PORT default
4144

4245
#include <core/filesystem.hpp>
4346
#include <core/stratum_server.hpp>
@@ -135,7 +138,9 @@ int run_node(const core::CoinParams& params, bool testnet,
135138
const std::string& stratum_addr, uint16_t stratum_port,
136139
const std::string& coin_daemon,
137140
const std::vector<std::byte>& coin_magic,
138-
const uint256& coin_genesis)
141+
const uint256& coin_genesis,
142+
const std::string& rpc_endpoint,
143+
const std::string& rpc_conf_path)
139144
{
140145
io::io_context ioc;
141146

@@ -238,7 +243,42 @@ int run_node(const core::CoinParams& params, bool testnet,
238243
// LOUDLY (the #163 seam guard: no silent drop, INDEPENDENT of the
239244
// embedded source). Point a real NodeRPC at external digibyted here to
240245
// light the submit sink up.
241-
dgb::coin::CoinNode coin_node(/*embedded=*/&embedded_coin, /*rpc=*/nullptr);
246+
// ── #82 external-daemon submitblock arm (RPC leg of the dual-path
247+
// broadcaster) ── Creds come from digibyte.conf (default
248+
// ~/.digibyte/digibyte.conf, overridable with --coin-rpc-auth PATH) so the
249+
// rpcpassword NEVER touches argv; --coin-rpc HOST:PORT overrides only the
250+
// endpoint. When no creds resolve (no daemon provisioned) the arm stays
251+
// UNARMED (rpc=nullptr) and submit_block_hex returns false LOUDLY (the #163
252+
// CoinNode seam guard) — byte-identical to today's daemon-less default
253+
// build, so --run still works without a digibyted. NodeRPC is declared
254+
// BEFORE coin_node so it OUTLIVES the tracker callback that captures it.
255+
dgb::coin::RpcConf rpc_conf;
256+
{
257+
std::string conf_path = rpc_conf_path;
258+
if (conf_path.empty()) {
259+
const char* home = std::getenv("HOME");
260+
conf_path = std::string(home ? home : ".") + "/.digibyte/digibyte.conf";
261+
}
262+
if (dgb::coin::load_rpc_conf(conf_path, rpc_conf)) {
263+
if (rpc_conf.port == 0)
264+
rpc_conf.port = testnet ? dgb::CoinParams::TESTNET_RPC_PORT
265+
: dgb::CoinParams::MAINNET_RPC_PORT;
266+
dgb::coin::apply_endpoint_override(rpc_endpoint, rpc_conf);
267+
}
268+
}
269+
std::unique_ptr<dgb::coin::NodeRPC> rpc;
270+
if (rpc_conf.armed()) {
271+
rpc = std::make_unique<dgb::coin::NodeRPC>(&ioc, /*coin=*/nullptr, testnet);
272+
rpc->connect(NetService(rpc_conf.host, rpc_conf.port), rpc_conf.userpass());
273+
std::cout << "[DGB] external-daemon submit arm ARMED: NodeRPC -> "
274+
<< rpc_conf.host << ":" << rpc_conf.port
275+
<< " (creds from digibyte.conf)" << std::endl;
276+
} else {
277+
std::cout << "[DGB] external-daemon submit arm UNARMED "
278+
"(no digibyte.conf creds; embedded-only submit path)" << std::endl;
279+
}
280+
281+
dgb::coin::CoinNode coin_node(/*embedded=*/&embedded_coin, /*rpc=*/rpc.get());
242282

243283
dgb::Node p2p_node(&ioc, &config);
244284
p2p_node.set_target_outbound_peers(4);
@@ -483,6 +523,8 @@ int main(int argc, char** argv)
483523
std::string coin_daemon; // --coin-daemon HOST:PORT (embedded P2P producer target)
484524
std::vector<std::byte> coin_magic; // --coin-magic HEX (network pchMessageStart)
485525
uint256 coin_genesis; // --coin-genesis HASH (initial getheaders locator base)
526+
std::string rpc_endpoint; // --coin-rpc HOST:PORT (external digibyted submit arm)
527+
std::string rpc_conf_path; // --coin-rpc-auth PATH to digibyte.conf (creds source)
486528
for (int i = 1; i < argc; ++i) {
487529
if (std::strcmp(argv[i], "--version") == 0) {
488530
std::cout << "c2pool-dgb " << C2POOL_VERSION << "\n";
@@ -511,6 +553,12 @@ int main(int argc, char** argv)
511553
if (std::strcmp(argv[i], "--coin-genesis") == 0 && i + 1 < argc) {
512554
coin_genesis = uint256S(argv[++i]); // genesis hash for initial getheaders
513555
}
556+
if (std::strcmp(argv[i], "--coin-rpc") == 0 && i + 1 < argc) {
557+
rpc_endpoint = argv[++i]; // HOST:PORT endpoint override (no secret)
558+
}
559+
if (std::strcmp(argv[i], "--coin-rpc-auth") == 0 && i + 1 < argc) {
560+
rpc_conf_path = argv[++i]; // path to digibyte.conf (rpcpassword stays in-file)
561+
}
514562
}
515563

516564
const core::CoinParams params = dgb::make_coin_params(/*testnet=*/false);
@@ -522,7 +570,8 @@ int main(int argc, char** argv)
522570
// --run: stand up the run-loop (io_context + sharechain peer + stratum).
523571
if (want_run)
524572
return run_node(params, /*testnet=*/false, stratum_addr, stratum_port,
525-
coin_daemon, coin_magic, coin_genesis);
573+
coin_daemon, coin_magic, coin_genesis,
574+
rpc_endpoint, rpc_conf_path);
526575

527576
// --selftest, or a bare invocation: drive the live score path so the
528577
// binary exercises real consensus code, then exit cleanly.

src/impl/dgb/coin/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
set(dgb_coin_impl
99
rpc_data.hpp
1010
rpc.hpp rpc.cpp
11+
rpc_conf.hpp
1112
rpc_request.hpp
1213
softfork_check.hpp
1314
p2p_connection.hpp p2p_connection.cpp

src/impl/dgb/coin/rpc_conf.hpp

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#pragma once
2+
3+
// ---------------------------------------------------------------------------
4+
// dgb::coin::rpc_conf -- digibyte.conf credential resolution for the #82
5+
// external-daemon submitblock arm (the RPC leg of the dual-path broadcaster).
6+
//
7+
// Credential rule (operator self-provision posture, 2026-06-19): the
8+
// rpcpassword NEVER reaches the process table. --coin-rpc carries only
9+
// HOST:PORT; --coin-rpc-auth carries a FILE PATH to the conf (default
10+
// ~/.digibyte/digibyte.conf); rpcuser/rpcpassword are read from that file and
11+
// are never echoed. Mirrors main_bch.cpp load_rpc_conf, lifted into
12+
// src/impl/dgb/ as the fenced conf-parse helper (integrator approval
13+
// 2026-06-20, msgid 1660 follow-up 3): "digibyte.conf as primary creds source
14+
// with optional --coin-rpc / --coin-rpc-auth overrides keeping rpcpassword off
15+
// the process table ... Fence (main_dgb.cpp + conf-parse helper in
16+
// src/impl/dgb/, zero shared/core touch)".
17+
//
18+
// Header-only and dependency-light by design (no config_coin.hpp pull): the
19+
// net-default RPC port is supplied by the caller, so dgb_rpc_conf_test can
20+
// exercise the parser without dragging the coin config in.
21+
// ---------------------------------------------------------------------------
22+
23+
#include <cstdint>
24+
#include <fstream>
25+
#include <string>
26+
27+
namespace dgb
28+
{
29+
namespace coin
30+
{
31+
32+
// Resolved external-daemon RPC endpoint + credentials. `armed()` is the single
33+
// gate main_dgb consults before constructing a NodeRPC: no creds (or no port)
34+
// => the submit arm stays UNARMED and submit_block_hex returns false LOUDLY
35+
// (the #163 CoinNode seam guard), identical to the daemon-less default build.
36+
struct RpcConf
37+
{
38+
std::string host = "127.0.0.1";
39+
uint16_t port = 0; // 0 => caller fills the per-net default
40+
std::string user;
41+
std::string pass;
42+
43+
bool armed() const { return !user.empty() && !pass.empty() && port != 0; }
44+
std::string userpass() const { return user + ":" + pass; }
45+
};
46+
47+
namespace conf_detail
48+
{
49+
inline std::string trim(const std::string& s)
50+
{
51+
const char* ws = " \t\r\n";
52+
const auto b = s.find_first_not_of(ws);
53+
if (b == std::string::npos) return {};
54+
const auto e = s.find_last_not_of(ws);
55+
return s.substr(b, e - b + 1);
56+
}
57+
} // namespace conf_detail
58+
59+
// Parse rpcuser/rpcpassword/rpcport/rpcconnect from a digibyte.conf-style file
60+
// (also accepts the c2pool dgb_rpc_user/dgb_rpc_password aliases). '#' begins a
61+
// comment. Returns true ONLY when BOTH user and password were found; the
62+
// password stays in-file and is never logged.
63+
inline bool load_rpc_conf(const std::string& path, RpcConf& out)
64+
{
65+
std::ifstream f(path);
66+
if (!f) return false;
67+
std::string line;
68+
while (std::getline(f, line)) {
69+
const auto h = line.find('#');
70+
if (h != std::string::npos) line = line.substr(0, h);
71+
const auto eq = line.find('=');
72+
if (eq == std::string::npos) continue;
73+
const std::string key = conf_detail::trim(line.substr(0, eq));
74+
const std::string val = conf_detail::trim(line.substr(eq + 1));
75+
if (val.empty()) continue;
76+
if (key == "rpcuser" || key == "dgb_rpc_user") out.user = val;
77+
else if (key == "rpcpassword" || key == "dgb_rpc_password") out.pass = val;
78+
else if (key == "rpcport") out.port = static_cast<uint16_t>(std::stoi(val));
79+
else if (key == "rpcconnect") out.host = val;
80+
}
81+
return !out.user.empty() && !out.pass.empty();
82+
}
83+
84+
// Apply a "--coin-rpc HOST:PORT" endpoint override. Endpoint only -- carries no
85+
// secret, so it is safe on the process table. A bare "HOST" leaves the port at
86+
// whatever the conf/default supplied; an empty argument is a no-op.
87+
inline void apply_endpoint_override(const std::string& hostport, RpcConf& out)
88+
{
89+
if (hostport.empty()) return;
90+
const auto colon = hostport.rfind(':');
91+
if (colon == std::string::npos) { out.host = hostport; return; }
92+
out.host = hostport.substr(0, colon);
93+
const std::string p = hostport.substr(colon + 1);
94+
if (!p.empty()) out.port = static_cast<uint16_t>(std::stoi(p));
95+
}
96+
97+
} // namespace coin
98+
} // namespace dgb

src/impl/dgb/test/share_test.cpp

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
#include <impl/dgb/share.hpp>
1717
#include <impl/dgb/share_tracker.hpp> // DensePPLNSWindow — V36 decayed-PPLNS SSOT
1818
#include <impl/dgb/params.hpp> // make_coin_params — assembled CoinParams SSOT
19+
#include <impl/dgb/coin/rpc_conf.hpp> // #82 external-daemon RPC creds (digibyte.conf)
20+
21+
#include <cstdio>
22+
#include <fstream>
1923

2024
// Sharechain-identity: the isolation primitives (PREFIX / IDENTIFIER) are the
2125
// per-coin namespacing boundary — they MUST stay byte-exact to the DGB oracle.
@@ -220,3 +224,86 @@ TEST(DGB_share_test, OracleAddressAndRelayParams)
220224
EXPECT_EQ(test.address_p2sh_version2, 0x00); // disabled on testnet too
221225
EXPECT_EQ(test.bech32_hrp, "dgbt");
222226
}
227+
228+
229+
// ---------------------------------------------------------------------------
230+
// #82 external-daemon submit arm — digibyte.conf credential resolution.
231+
// Guards dgb::coin::load_rpc_conf / apply_endpoint_override / RpcConf::armed():
232+
// the RPC leg of the dual-path broadcaster is armed ONLY when both creds AND a
233+
// port resolve, and the rpcpassword is sourced from-file (never argv). The
234+
// endpoint override carries no secret. Header-only helper, so this lives in the
235+
// already-allowlisted dgb_share_test target (no new gtest binary; avoids #143).
236+
namespace {
237+
std::string write_tmp_conf(const char* tag, const std::string& body)
238+
{
239+
const std::string path = std::string(::testing::TempDir()) + "/dgb_rpc_conf_" + tag + ".conf";
240+
std::ofstream(path) << body;
241+
return path;
242+
}
243+
}
244+
245+
TEST(DGB_rpc_conf, ParsesUserPassPortAndConnect)
246+
{
247+
const std::string path = write_tmp_conf("full",
248+
"# digibyte.conf\n"
249+
"rpcuser=dgbuser\n"
250+
"rpcpassword=s3cr3t=with=eq\n" // value may contain '=' after the first
251+
"rpcport=14024\n"
252+
"rpcconnect=10.0.0.7\n");
253+
dgb::coin::RpcConf c;
254+
EXPECT_TRUE(dgb::coin::load_rpc_conf(path, c));
255+
EXPECT_EQ(c.user, "dgbuser");
256+
EXPECT_EQ(c.pass, "s3cr3t=with=eq");
257+
EXPECT_EQ(c.port, 14024u);
258+
EXPECT_EQ(c.host, "10.0.0.7");
259+
EXPECT_TRUE(c.armed());
260+
EXPECT_EQ(c.userpass(), "dgbuser:s3cr3t=with=eq");
261+
std::remove(path.c_str());
262+
}
263+
264+
TEST(DGB_rpc_conf, AcceptsC2poolAliasesAndComments)
265+
{
266+
const std::string path = write_tmp_conf("alias",
267+
"dgb_rpc_user = aliasuser # inline comment stripped\n"
268+
"dgb_rpc_password = aliaspass\n");
269+
dgb::coin::RpcConf c;
270+
EXPECT_TRUE(dgb::coin::load_rpc_conf(path, c));
271+
EXPECT_EQ(c.user, "aliasuser");
272+
EXPECT_EQ(c.pass, "aliaspass");
273+
EXPECT_EQ(c.port, 0u); // no rpcport -> caller fills net default
274+
EXPECT_FALSE(c.armed()); // armed() requires a non-zero port
275+
std::remove(path.c_str());
276+
}
277+
278+
TEST(DGB_rpc_conf, MissingPasswordIsNotArmedAndLoadFails)
279+
{
280+
const std::string path = write_tmp_conf("nopass", "rpcuser=lonely\n");
281+
dgb::coin::RpcConf c;
282+
EXPECT_FALSE(dgb::coin::load_rpc_conf(path, c)); // both user+pass required
283+
EXPECT_FALSE(c.armed());
284+
std::remove(path.c_str());
285+
}
286+
287+
TEST(DGB_rpc_conf, MissingFileReturnsFalse)
288+
{
289+
dgb::coin::RpcConf c;
290+
EXPECT_FALSE(dgb::coin::load_rpc_conf("/nonexistent/dgb/digibyte.conf", c));
291+
EXPECT_FALSE(c.armed());
292+
}
293+
294+
TEST(DGB_rpc_conf, EndpointOverrideCarriesNoSecret)
295+
{
296+
dgb::coin::RpcConf c;
297+
c.user = "u"; c.pass = "p"; c.port = 14024; c.host = "127.0.0.1";
298+
dgb::coin::apply_endpoint_override("192.168.1.50:15001", c);
299+
EXPECT_EQ(c.host, "192.168.1.50");
300+
EXPECT_EQ(c.port, 15001u);
301+
// host-only override keeps the resolved port
302+
dgb::coin::apply_endpoint_override("example.host", c);
303+
EXPECT_EQ(c.host, "example.host");
304+
EXPECT_EQ(c.port, 15001u);
305+
// empty override is a no-op; creds untouched (still off the process table)
306+
dgb::coin::apply_endpoint_override("", c);
307+
EXPECT_EQ(c.host, "example.host");
308+
EXPECT_EQ(c.userpass(), "u:p");
309+
}

0 commit comments

Comments
 (0)