diff --git a/src/c2pool/main_dgb.cpp b/src/c2pool/main_dgb.cpp index 5acacfcb4..081fc52df 100644 --- a/src/c2pool/main_dgb.cpp +++ b/src/c2pool/main_dgb.cpp @@ -38,6 +38,9 @@ #include #include #include +#include // NodeRPC — external-daemon submitblock arm (#82) +#include // digibyte.conf creds resolution (rpcpassword off argv) +#include // dgb::CoinParams::MAINNET_RPC_PORT default #include #include @@ -135,7 +138,9 @@ int run_node(const core::CoinParams& params, bool testnet, const std::string& stratum_addr, uint16_t stratum_port, const std::string& coin_daemon, const std::vector& coin_magic, - const uint256& coin_genesis) + const uint256& coin_genesis, + const std::string& rpc_endpoint, + const std::string& rpc_conf_path) { io::io_context ioc; @@ -238,7 +243,42 @@ int run_node(const core::CoinParams& params, bool testnet, // LOUDLY (the #163 seam guard: no silent drop, INDEPENDENT of the // embedded source). Point a real NodeRPC at external digibyted here to // light the submit sink up. - dgb::coin::CoinNode coin_node(/*embedded=*/&embedded_coin, /*rpc=*/nullptr); + // ── #82 external-daemon submitblock arm (RPC leg of the dual-path + // broadcaster) ── Creds come from digibyte.conf (default + // ~/.digibyte/digibyte.conf, overridable with --coin-rpc-auth PATH) so the + // rpcpassword NEVER touches argv; --coin-rpc HOST:PORT overrides only the + // endpoint. When no creds resolve (no daemon provisioned) the arm stays + // UNARMED (rpc=nullptr) and submit_block_hex returns false LOUDLY (the #163 + // CoinNode seam guard) — byte-identical to today's daemon-less default + // build, so --run still works without a digibyted. NodeRPC is declared + // BEFORE coin_node so it OUTLIVES the tracker callback that captures it. + dgb::coin::RpcConf rpc_conf; + { + std::string conf_path = rpc_conf_path; + if (conf_path.empty()) { + const char* home = std::getenv("HOME"); + conf_path = std::string(home ? home : ".") + "/.digibyte/digibyte.conf"; + } + if (dgb::coin::load_rpc_conf(conf_path, rpc_conf)) { + if (rpc_conf.port == 0) + rpc_conf.port = testnet ? dgb::CoinParams::TESTNET_RPC_PORT + : dgb::CoinParams::MAINNET_RPC_PORT; + dgb::coin::apply_endpoint_override(rpc_endpoint, rpc_conf); + } + } + std::unique_ptr rpc; + if (rpc_conf.armed()) { + rpc = std::make_unique(&ioc, /*coin=*/nullptr, testnet); + rpc->connect(NetService(rpc_conf.host, rpc_conf.port), rpc_conf.userpass()); + std::cout << "[DGB] external-daemon submit arm ARMED: NodeRPC -> " + << rpc_conf.host << ":" << rpc_conf.port + << " (creds from digibyte.conf)" << std::endl; + } else { + std::cout << "[DGB] external-daemon submit arm UNARMED " + "(no digibyte.conf creds; embedded-only submit path)" << std::endl; + } + + dgb::coin::CoinNode coin_node(/*embedded=*/&embedded_coin, /*rpc=*/rpc.get()); dgb::Node p2p_node(&ioc, &config); p2p_node.set_target_outbound_peers(4); @@ -483,6 +523,8 @@ int main(int argc, char** argv) std::string coin_daemon; // --coin-daemon HOST:PORT (embedded P2P producer target) std::vector coin_magic; // --coin-magic HEX (network pchMessageStart) uint256 coin_genesis; // --coin-genesis HASH (initial getheaders locator base) + std::string rpc_endpoint; // --coin-rpc HOST:PORT (external digibyted submit arm) + std::string rpc_conf_path; // --coin-rpc-auth PATH to digibyte.conf (creds source) for (int i = 1; i < argc; ++i) { if (std::strcmp(argv[i], "--version") == 0) { std::cout << "c2pool-dgb " << C2POOL_VERSION << "\n"; @@ -511,6 +553,12 @@ int main(int argc, char** argv) if (std::strcmp(argv[i], "--coin-genesis") == 0 && i + 1 < argc) { coin_genesis = uint256S(argv[++i]); // genesis hash for initial getheaders } + if (std::strcmp(argv[i], "--coin-rpc") == 0 && i + 1 < argc) { + rpc_endpoint = argv[++i]; // HOST:PORT endpoint override (no secret) + } + if (std::strcmp(argv[i], "--coin-rpc-auth") == 0 && i + 1 < argc) { + rpc_conf_path = argv[++i]; // path to digibyte.conf (rpcpassword stays in-file) + } } const core::CoinParams params = dgb::make_coin_params(/*testnet=*/false); @@ -522,7 +570,8 @@ int main(int argc, char** argv) // --run: stand up the run-loop (io_context + sharechain peer + stratum). if (want_run) return run_node(params, /*testnet=*/false, stratum_addr, stratum_port, - coin_daemon, coin_magic, coin_genesis); + coin_daemon, coin_magic, coin_genesis, + rpc_endpoint, rpc_conf_path); // --selftest, or a bare invocation: drive the live score path so the // binary exercises real consensus code, then exit cleanly. diff --git a/src/impl/dgb/coin/CMakeLists.txt b/src/impl/dgb/coin/CMakeLists.txt index 57701b8de..c554c2b03 100644 --- a/src/impl/dgb/coin/CMakeLists.txt +++ b/src/impl/dgb/coin/CMakeLists.txt @@ -8,6 +8,7 @@ set(dgb_coin_impl rpc_data.hpp rpc.hpp rpc.cpp + rpc_conf.hpp rpc_request.hpp softfork_check.hpp p2p_connection.hpp p2p_connection.cpp diff --git a/src/impl/dgb/coin/rpc_conf.hpp b/src/impl/dgb/coin/rpc_conf.hpp new file mode 100644 index 000000000..68cf346b3 --- /dev/null +++ b/src/impl/dgb/coin/rpc_conf.hpp @@ -0,0 +1,98 @@ +#pragma once + +// --------------------------------------------------------------------------- +// dgb::coin::rpc_conf -- digibyte.conf credential resolution for the #82 +// external-daemon submitblock arm (the RPC leg of the dual-path broadcaster). +// +// Credential rule (operator self-provision posture, 2026-06-19): the +// rpcpassword NEVER reaches the process table. --coin-rpc carries only +// HOST:PORT; --coin-rpc-auth carries a FILE PATH to the conf (default +// ~/.digibyte/digibyte.conf); rpcuser/rpcpassword are read from that file and +// are never echoed. Mirrors main_bch.cpp load_rpc_conf, lifted into +// src/impl/dgb/ as the fenced conf-parse helper (integrator approval +// 2026-06-20, msgid 1660 follow-up 3): "digibyte.conf as primary creds source +// with optional --coin-rpc / --coin-rpc-auth overrides keeping rpcpassword off +// the process table ... Fence (main_dgb.cpp + conf-parse helper in +// src/impl/dgb/, zero shared/core touch)". +// +// Header-only and dependency-light by design (no config_coin.hpp pull): the +// net-default RPC port is supplied by the caller, so dgb_rpc_conf_test can +// exercise the parser without dragging the coin config in. +// --------------------------------------------------------------------------- + +#include +#include +#include + +namespace dgb +{ +namespace coin +{ + +// Resolved external-daemon RPC endpoint + credentials. `armed()` is the single +// gate main_dgb consults before constructing a NodeRPC: no creds (or no port) +// => the submit arm stays UNARMED and submit_block_hex returns false LOUDLY +// (the #163 CoinNode seam guard), identical to the daemon-less default build. +struct RpcConf +{ + std::string host = "127.0.0.1"; + uint16_t port = 0; // 0 => caller fills the per-net default + std::string user; + std::string pass; + + bool armed() const { return !user.empty() && !pass.empty() && port != 0; } + std::string userpass() const { return user + ":" + pass; } +}; + +namespace conf_detail +{ +inline std::string trim(const std::string& s) +{ + const char* ws = " \t\r\n"; + const auto b = s.find_first_not_of(ws); + if (b == std::string::npos) return {}; + const auto e = s.find_last_not_of(ws); + return s.substr(b, e - b + 1); +} +} // namespace conf_detail + +// Parse rpcuser/rpcpassword/rpcport/rpcconnect from a digibyte.conf-style file +// (also accepts the c2pool dgb_rpc_user/dgb_rpc_password aliases). '#' begins a +// comment. Returns true ONLY when BOTH user and password were found; the +// password stays in-file and is never logged. +inline bool load_rpc_conf(const std::string& path, RpcConf& out) +{ + std::ifstream f(path); + if (!f) return false; + std::string line; + while (std::getline(f, line)) { + const auto h = line.find('#'); + if (h != std::string::npos) line = line.substr(0, h); + const auto eq = line.find('='); + if (eq == std::string::npos) continue; + const std::string key = conf_detail::trim(line.substr(0, eq)); + const std::string val = conf_detail::trim(line.substr(eq + 1)); + if (val.empty()) continue; + if (key == "rpcuser" || key == "dgb_rpc_user") out.user = val; + else if (key == "rpcpassword" || key == "dgb_rpc_password") out.pass = val; + else if (key == "rpcport") out.port = static_cast(std::stoi(val)); + else if (key == "rpcconnect") out.host = val; + } + return !out.user.empty() && !out.pass.empty(); +} + +// Apply a "--coin-rpc HOST:PORT" endpoint override. Endpoint only -- carries no +// secret, so it is safe on the process table. A bare "HOST" leaves the port at +// whatever the conf/default supplied; an empty argument is a no-op. +inline void apply_endpoint_override(const std::string& hostport, RpcConf& out) +{ + if (hostport.empty()) return; + const auto colon = hostport.rfind(':'); + if (colon == std::string::npos) { out.host = hostport; return; } + out.host = hostport.substr(0, colon); + const std::string p = hostport.substr(colon + 1); + if (!p.empty()) out.port = static_cast(std::stoi(p)); +} + +} // namespace coin +} // namespace dgb diff --git a/src/impl/dgb/test/share_test.cpp b/src/impl/dgb/test/share_test.cpp index be4810ff4..e5c7e7b00 100644 --- a/src/impl/dgb/test/share_test.cpp +++ b/src/impl/dgb/test/share_test.cpp @@ -16,6 +16,10 @@ #include #include // DensePPLNSWindow — V36 decayed-PPLNS SSOT #include // make_coin_params — assembled CoinParams SSOT +#include // #82 external-daemon RPC creds (digibyte.conf) + +#include +#include // Sharechain-identity: the isolation primitives (PREFIX / IDENTIFIER) are the // per-coin namespacing boundary — they MUST stay byte-exact to the DGB oracle. @@ -220,3 +224,86 @@ TEST(DGB_share_test, OracleAddressAndRelayParams) EXPECT_EQ(test.address_p2sh_version2, 0x00); // disabled on testnet too EXPECT_EQ(test.bech32_hrp, "dgbt"); } + + +// --------------------------------------------------------------------------- +// #82 external-daemon submit arm — digibyte.conf credential resolution. +// Guards dgb::coin::load_rpc_conf / apply_endpoint_override / RpcConf::armed(): +// the RPC leg of the dual-path broadcaster is armed ONLY when both creds AND a +// port resolve, and the rpcpassword is sourced from-file (never argv). The +// endpoint override carries no secret. Header-only helper, so this lives in the +// already-allowlisted dgb_share_test target (no new gtest binary; avoids #143). +namespace { +std::string write_tmp_conf(const char* tag, const std::string& body) +{ + const std::string path = std::string(::testing::TempDir()) + "/dgb_rpc_conf_" + tag + ".conf"; + std::ofstream(path) << body; + return path; +} +} + +TEST(DGB_rpc_conf, ParsesUserPassPortAndConnect) +{ + const std::string path = write_tmp_conf("full", + "# digibyte.conf\n" + "rpcuser=dgbuser\n" + "rpcpassword=s3cr3t=with=eq\n" // value may contain '=' after the first + "rpcport=14024\n" + "rpcconnect=10.0.0.7\n"); + dgb::coin::RpcConf c; + EXPECT_TRUE(dgb::coin::load_rpc_conf(path, c)); + EXPECT_EQ(c.user, "dgbuser"); + EXPECT_EQ(c.pass, "s3cr3t=with=eq"); + EXPECT_EQ(c.port, 14024u); + EXPECT_EQ(c.host, "10.0.0.7"); + EXPECT_TRUE(c.armed()); + EXPECT_EQ(c.userpass(), "dgbuser:s3cr3t=with=eq"); + std::remove(path.c_str()); +} + +TEST(DGB_rpc_conf, AcceptsC2poolAliasesAndComments) +{ + const std::string path = write_tmp_conf("alias", + "dgb_rpc_user = aliasuser # inline comment stripped\n" + "dgb_rpc_password = aliaspass\n"); + dgb::coin::RpcConf c; + EXPECT_TRUE(dgb::coin::load_rpc_conf(path, c)); + EXPECT_EQ(c.user, "aliasuser"); + EXPECT_EQ(c.pass, "aliaspass"); + EXPECT_EQ(c.port, 0u); // no rpcport -> caller fills net default + EXPECT_FALSE(c.armed()); // armed() requires a non-zero port + std::remove(path.c_str()); +} + +TEST(DGB_rpc_conf, MissingPasswordIsNotArmedAndLoadFails) +{ + const std::string path = write_tmp_conf("nopass", "rpcuser=lonely\n"); + dgb::coin::RpcConf c; + EXPECT_FALSE(dgb::coin::load_rpc_conf(path, c)); // both user+pass required + EXPECT_FALSE(c.armed()); + std::remove(path.c_str()); +} + +TEST(DGB_rpc_conf, MissingFileReturnsFalse) +{ + dgb::coin::RpcConf c; + EXPECT_FALSE(dgb::coin::load_rpc_conf("/nonexistent/dgb/digibyte.conf", c)); + EXPECT_FALSE(c.armed()); +} + +TEST(DGB_rpc_conf, EndpointOverrideCarriesNoSecret) +{ + dgb::coin::RpcConf c; + c.user = "u"; c.pass = "p"; c.port = 14024; c.host = "127.0.0.1"; + dgb::coin::apply_endpoint_override("192.168.1.50:15001", c); + EXPECT_EQ(c.host, "192.168.1.50"); + EXPECT_EQ(c.port, 15001u); + // host-only override keeps the resolved port + dgb::coin::apply_endpoint_override("example.host", c); + EXPECT_EQ(c.host, "example.host"); + EXPECT_EQ(c.port, 15001u); + // empty override is a no-op; creds untouched (still off the process table) + dgb::coin::apply_endpoint_override("", c); + EXPECT_EQ(c.host, "example.host"); + EXPECT_EQ(c.userpass(), "u:p"); +}