Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 52 additions & 3 deletions src/c2pool/main_dgb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
#include <impl/dgb/coin/mempool_ingest.hpp>
#include <impl/dgb/stratum/work_source.hpp>
#include <impl/dgb/coin/p2p_node.hpp>
#include <impl/dgb/coin/rpc.hpp> // NodeRPC — external-daemon submitblock arm (#82)
#include <impl/dgb/coin/rpc_conf.hpp> // digibyte.conf creds resolution (rpcpassword off argv)
#include <impl/dgb/config_coin.hpp> // dgb::CoinParams::MAINNET_RPC_PORT default

#include <core/filesystem.hpp>
#include <core/stratum_server.hpp>
Expand Down Expand Up @@ -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<std::byte>& 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;

Expand Down Expand Up @@ -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<dgb::coin::NodeRPC> rpc;
if (rpc_conf.armed()) {
rpc = std::make_unique<dgb::coin::NodeRPC>(&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);
Expand Down Expand Up @@ -483,6 +523,8 @@ int main(int argc, char** argv)
std::string coin_daemon; // --coin-daemon HOST:PORT (embedded P2P producer target)
std::vector<std::byte> 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";
Expand Down Expand Up @@ -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);
Expand All @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/impl/dgb/coin/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
98 changes: 98 additions & 0 deletions src/impl/dgb/coin/rpc_conf.hpp
Original file line number Diff line number Diff line change
@@ -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 <cstdint>
#include <fstream>
#include <string>

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<uint16_t>(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<uint16_t>(std::stoi(p));
}

} // namespace coin
} // namespace dgb
87 changes: 87 additions & 0 deletions src/impl/dgb/test/share_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
#include <impl/dgb/share.hpp>
#include <impl/dgb/share_tracker.hpp> // DensePPLNSWindow — V36 decayed-PPLNS SSOT
#include <impl/dgb/params.hpp> // make_coin_params — assembled CoinParams SSOT
#include <impl/dgb/coin/rpc_conf.hpp> // #82 external-daemon RPC creds (digibyte.conf)

#include <cstdio>
#include <fstream>

// Sharechain-identity: the isolation primitives (PREFIX / IDENTIFIER) are the
// per-coin namespacing boundary — they MUST stay byte-exact to the DGB oracle.
Expand Down Expand Up @@ -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");
}
Loading