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
84 changes: 67 additions & 17 deletions src/c2pool/main_dash.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
#include <impl/dash/coin/zmq_tip_notify.hpp> // dash::coin::TipHashDedup / ZmqHashblockSubscriber — dashd ZMQ hashblock INSTANT tip-notify (opt-in, hardening on the #770 poll)
#include <impl/dash/coin/coin_p2p_magic.hpp> // dash::coin::select_coin_p2p_magic — E5 --coin-p2p-magic override (regtest ARM A dial)
#include <impl/dash/coin/node_coin_state.hpp> // dash::coin::NodeCoinState (embedded work bundle)
#include <impl/dash/coin/arm_resolution.hpp> // dash::coin::resolve_embedded_arm (#738 arm decision, one place)
#include <impl/dash/coin/dkg_window.hpp> // dash::coin::is_dkg_commitment_window (BLOCKER-1 guard)
#include <impl/dash/coin/dkg_commitments.hpp> // E1: build_daemonless_qc_plan (serve DKG windows daemonlessly)
#include <impl/dash/coin/vendor/bls_verify.hpp> // E1 Phase-L: make_commitment_bls_verifier (real qc verify seam)
Expand Down Expand Up @@ -218,7 +219,7 @@ void print_banner(const char* argv0)
<< " [--stratum [HOST:]PORT] [--coin-p2p-connect HOST:PORT]... [--coin-p2p-discover]\n"
<< " [--web-port PORT] [--web-host ADDR] [--dashboard-dir PATH]\n"
<< " [--external-ip ADDR]\n"
<< " [--embedded-utxo]\n"
<< " [--embedded-utxo] [--embedded-mainnet]\n"
<< " [--give-author PCT] [-f|--fee PCT] [--node-owner-address ADDR]\n"
<< " [--redistribute pplns|fee|boost|donate]\n"
<< " [--coin-zmq-hashblock tcp://HOST:PORT]\n"
Expand All @@ -242,6 +243,15 @@ void print_banner(const char* argv0)
<< " clean_jobs notify on the fallback arm; absent/unreachable => the 3 s\n"
<< " getbestblockhash poll is the active path (requires dashd\n"
<< " zmqpubhashblock=tcp://HOST:PORT). No consensus effect.\n"
<< " --embedded-mainnet (DEFAULT OFF) is the single opt-in for the\n"
<< " DAEMONLESS embedded template arm on MAINNET. It lifts the arm gate\n"
<< " AND arms the coin-state feed that populates it (seed-based peer\n"
<< " discovery unless --coin-p2p-connect names peers), so one flag takes\n"
<< " the arm end to end. WITHOUT it a mainnet node ALWAYS serves the\n"
<< " reward-safe dashd-RPC fallback: --coin-p2p-connect/--coin-p2p-discover\n"
<< " are transport only and NEVER move the arm. Even when armed, every\n"
<< " per-template gate (SML fresh at tip, non-superblock, credit-pool seed\n"
<< " height, bestCL, MN-payee cursor, DKG plan) fails closed to dashd.\n"
<< " --coin-p2p-magic HEX overrides the embedded coin-P2P wire magic\n"
<< " (default mainnet bf0c6bbd / testnet cee2caff; regtest fcc1b7dc).\n"
<< " --regtest-force-won-block (regtest E5 harness, fail-closed) drives\n"
Expand Down Expand Up @@ -1221,11 +1231,42 @@ int run_node(bool testnet, const std::string& rpc_endpoint,
std::cout << "[run] web dashboard disabled (--web-port 0)\n";
}

// ── E1: OPT-IN embedded coin-network P2P dial (--coin-p2p-connect) ────
// GUARANTEE: with no --coin-p2p-connect on argv, coin_p2p stays null and
// this block is a no-op — the run path is unchanged and the mining-hotel
// prod posture (NodeCoinState unpopulated -> dashd-RPC fallback) is
// untouched. The coin-network wire MAGIC (dashd pchMessageStart: mainnet
// ── #738: resolve WHICH template arm this invocation takes, ONCE ──────
// Before this, taking the embedded arm needed TWO conditions that no single
// flag satisfied: the work-source gate (--embedded-mainnet, half 1) AND a
// live coin-state feed (--coin-p2p-connect/--coin-p2p-discover, half 2,
// which is what constructs the maintainer that flips populated()). The
// embedded opt-in armed only half 1, so NodeCoinState was never fed and the
// arm could not be taken from any documented invocation. resolve_embedded_
// arm() closes that with a ONE-WAY implication: the embedded opt-in implies
// its own feed. The converse is deliberately absent — a transport flag NEVER
// moves the arm (the hotel incident where --coin-p2p-connect activated an
// unguarded embedded arm on a live production node). Pinned by
// test_dash_stratum_work_source's DashRunArmResolution suite.
const dash::coin::ArmResolution run_arm =
dash::coin::resolve_embedded_arm(dash::coin::ArmInputs{
testnet, embedded_mainnet,
!coin_p2p_targets.empty(), coin_p2p_discover });
// Discovery is turned on for a pinned-peer-less embedded opt-in: daemonless
// needs somewhere to sync from. Operator-named transport is never overridden.
const bool coin_p2p_discover_eff = coin_p2p_discover || run_arm.discover_implied;
std::cout << "[run] template arm=" << dash::coin::arm_name(run_arm.arm)
<< " — " << dash::coin::arm_reason_text(run_arm.reason)
<< "\n[run] (embedded_arm_enabled="
<< (run_arm.embedded_arm_enabled ? "yes" : "no")
<< " coin_state_feed=" << (run_arm.coin_feed_armed ? "armed" : "off")
<< (run_arm.discover_implied ? " discovery=implied-by-opt-in" : "")
<< ")\n";

// ── E1: OPT-IN embedded coin-network P2P dial ─────────────────────────
// GUARANTEE: this block is a no-op unless the invocation asked for a
// coin-state feed — an explicit --coin-p2p-connect / --coin-p2p-discover,
// or the --embedded-mainnet opt-in that implies one (#738). With NONE of
// those on argv, coin_p2p stays null, the run path is unchanged and the
// mining-hotel prod posture (NodeCoinState unpopulated -> dashd-RPC
// fallback) is untouched. Arming the feed alone still does NOT move the
// arm: without --embedded-mainnet, work_source keeps serving the dashd
// fallback no matter what this block populates. The coin-network wire MAGIC (dashd pchMessageStart: mainnet
// bf0c6bbd / testnet cee2caff, same constants as the slice-1 launcher
// dispatch) is DISTINCT from the sharechain PREFIX set above — different
// layers, never conflated. The client rides the SAME ioc as the
Expand All @@ -1241,7 +1282,7 @@ int run_node(bool testnet, const std::string& rpc_endpoint,
// Refresh timer declared LAST -> destroyed FIRST: it stops (and its lambda
// stops capturing coin_p2p / coin_peer_mgr) before either is torn down.
std::unique_ptr<core::Timer> coin_dial_refresh_timer;
if (!coin_p2p_targets.empty() || coin_p2p_discover) {
if (run_arm.coin_feed_armed) {
config.coin()->m_testnet = testnet;
// Coin-network wire magic (dashd pchMessageStart). Default: mainnet
// bf0c6bbd / testnet cee2caff. A dev regtest dashd uses a DISTINCT magic
Expand All @@ -1258,7 +1299,7 @@ int run_node(bool testnet, const std::string& rpc_endpoint,
coin_p2p = std::make_unique<dash::coin::p2p::CoinClient<dash::Config>>(
&ioc, &coin_state, &config, "COIN-P2P");

if (coin_p2p_discover) {
if (coin_p2p_discover_eff) {
// ── Network-standalone arm: seed-discovered, SCORED, group-diverse
// peer set INDEPENDENT of the local dashd. The pinned local dashd
// (--coin-p2p-connect front target, if any) is registered as the
Expand All @@ -1280,6 +1321,10 @@ int run_node(bool testnet, const std::string& rpc_endpoint,
}
coin_peer_mgr->set_dns_seeds(dash::coin::dash_dns_seeds(testnet));
coin_peer_mgr->set_fixed_seeds(dash::coin::dash_fixed_seeds(testnet));
// HTTP peer fallback: when DNS + fixed seeds are exhausted, query
// known c2pool nodes via GET /api/coin_peers for peer addresses —
// the third and last bootstrap tier. Mirrors main_ltc.cpp:1875.
coin_peer_mgr->set_http_peer_seeds({{"voidbind.com", 8080}});

// ── ACTIVE daemon-disjointness (connect+discover / oracle-shadow) ──
// When a local dashd RPC is armed, feed its getpeerinfo so the
Expand Down Expand Up @@ -1345,8 +1390,11 @@ int run_node(bool testnet, const std::string& rpc_endpoint,
cp->update_dial_targets(std::move(refreshed));
});

std::cout << "[run] coin-network P2P DISCOVERY armed (--coin-p2p-discover): "
"DASH-isolated scored/diverse peer set, pinned="
std::cout << "[run] coin-network P2P DISCOVERY armed ("
<< (run_arm.discover_implied
? "implied by --embedded-mainnet"
: "--coin-p2p-discover")
<< "): DASH-isolated scored/diverse peer set, pinned="
<< pinned_str << " magic=" << net_magic_hex
<< " proto=70230 dns_seeds=" << dash::coin::dash_dns_seeds(testnet).size()
<< " fixed_seeds=" << dash::coin::dash_fixed_seeds(testnet).size()
Expand Down Expand Up @@ -1378,8 +1426,10 @@ int run_node(bool testnet, const std::string& rpc_endpoint,
// holds a non-owning const ref to it). It is declared here, in run_node's
// scope, BEFORE work_source; the explicit stratum_server.reset() after
// ioc.run() tears the acceptor down while node_coin_state is still alive.
// Default (unpopulated) bundle: populated()==false, so get_work() takes the
// retained dashd-fallback arm -- correct + documented for the 4a standup.
// Constructed UNPOPULATED: populated()==false, so get_work() takes the
// retained dashd-fallback arm until a coin-state feed publishes a tip. With
// no feed armed (run_arm.coin_feed_armed == false) nothing ever publishes
// one, and the fallback arm is the whole run.
//
// E2b (#738) UTXO/fee lane: utxo_lane is declared BEFORE node_coin_state
// deliberately -- attach() hands the lane's UTXOViewCache pointer to the
Expand Down Expand Up @@ -2599,7 +2649,7 @@ int run_node(bool testnet, const std::string& rpc_endpoint,
// flag) yields nullopt and the arm fails closed to the dashd fallback,
// exactly the old refusal. The emit gate re-derives this same plan and
// hard-rejects any template whose type-6 set drifts from it.
if (testnet || embedded_mainnet) {
if (run_arm.embedded_arm_enabled) {
const auto qc_net = testnet ? dash::coin::LlmqNetwork::Testnet
: dash::coin::LlmqNetwork::Mainnet;
// Phase-L sourcing leg: collect REAL relayed DKG commitments off
Expand Down Expand Up @@ -2757,14 +2807,14 @@ int run_node(bool testnet, const std::string& rpc_endpoint,
// Only meaningful when the embedded arm actually serves (testnet or
// --embedded-mainnet); harmless otherwise (the arm is off, work_source
// never consults viability).
node_coin_state.set_require_fresh_bestcl(testnet || embedded_mainnet);
node_coin_state.set_require_fresh_bestcl(run_arm.embedded_arm_enabled);

// SOAK FIX (bad-cbtx-assetlocked-amount): the DIP-0027 credit-pool seed
// rides a separate on_mnlistdiff step and can lag one block while the SML
// hash is already at the tip; the accrual then commits a stale
// creditPoolBalance. Refuse the embedded arm unless the credit-pool seed
// is current AT the tip, same discipline as the SML axis.
node_coin_state.set_require_fresh_credit_pool(testnet || embedded_mainnet);
node_coin_state.set_require_fresh_credit_pool(run_arm.embedded_arm_enabled);

// E4 re-soak fix (bad-cb-payee at 1519827): the projected masternode
// payee is only dashd-exact when the payee queue has folded EVERY
Expand All @@ -2780,7 +2830,7 @@ int run_node(bool testnet, const std::string& rpc_endpoint,
// MnStateMachine's forward-contiguity guard + the maintainer's gap
// fail-closed path (wipe + authoritative protx re-seed) close the
// gap itself.
node_coin_state.set_require_fresh_mn_payee(testnet || embedded_mainnet);
node_coin_state.set_require_fresh_mn_payee(run_arm.embedded_arm_enabled);

// H-6: SML/quorum apply and bestCL adoption move ASYNCHRONOUSLY to the
// header tip. When they advance (catching the SML up to a moved tip, or
Expand Down Expand Up @@ -2946,7 +2996,7 @@ int run_node(bool testnet, const std::string& rpc_endpoint,
// getheaders off our current locator + a mempool prime.
coin_p2p->set_on_handshake_complete(
[cp = coin_p2p.get(), hc = header_chain.get(), sml_base,
discover = coin_p2p_discover]() {
discover = coin_p2p_discover_eff]() {
LOG_INFO << "[EMB-DASH] handshake complete -> initial sync:"
" getheaders + mempool + mnlistdiff(cold)"
<< (discover ? " + getaddr (peer crawl)" : "");
Expand Down
157 changes: 157 additions & 0 deletions src/impl/dash/coin/arm_resolution.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#pragma once

/// #738 — which template arm a `--run` invocation resolves to, as ONE pure,
/// testable function.
///
/// THE DEFECT THIS CLOSES
/// ---------------------
/// Taking the embedded (daemonless) template arm needs TWO independent
/// conditions, and before this header NO SINGLE FLAG satisfied both:
///
/// (1) the work-source arm gate — DASHWorkSource only consults the embedded
/// bundle when `is_testnet_ || embedded_mainnet_` (work_source.cpp
/// get_work / resource_template_now). On mainnet that means
/// `--embedded-mainnet`.
/// (2) a live coin-state FEED — NodeCoinState::populated() only flips once
/// CoinStateMaintainer publishes a tip, and the maintainer + header chain
/// + ingest legs are constructed ONLY inside main_dash's `if (coin_p2p)`
/// block, i.e. only when `--coin-p2p-connect` / `--coin-p2p-discover`
/// built a CoinClient.
///
/// `--embedded-mainnet` satisfied (1) but not (2): with no coin-P2P flag the
/// bundle was never fed, populated() stayed false forever, and every template
/// came off the dashd fallback. `--coin-p2p-connect` satisfied (2) but not (1).
/// So the whole daemonless stack was unreachable from any documented
/// invocation. This resolver makes the EMBEDDED OPT-IN imply its own feed, so
/// one flag arms both halves end to end.
///
/// ★ REWARD-SAFETY INVARIANT (hotel incident: `--coin-p2p-connect` once
/// activated an unguarded embedded arm on a live production node) ★
/// The implication is ONE-WAY and runs only in the safe direction:
///
/// embedded opt-in => coin-state feed (NEW — this is the wiring)
/// coin-state feed => embedded opt-in (NEVER — explicitly not)
///
/// A transport flag can therefore never flip the arm. `--coin-p2p-connect`
/// alone on mainnet still resolves DashdFallback, exactly as today, and a bare
/// `--run` touches none of this. Both are pinned by tests.
///
/// STRICTLY single-coin (src/impl/dash only) and dependency-free: no I/O, no
/// core reach, no allocation. main_dash.cpp is the sole production caller.

namespace dash {
namespace coin {

/// Which arm a `--run` invocation resolves to. "EmbeddedEligible" means the
/// arm is ARMED, not that it will serve: every per-template viability gate
/// (SML fresh at tip, non-superblock height, credit-pool seed height, bestCL,
/// MN-payee cursor, DKG plan completeness) still fails closed to the dashd
/// fallback on the hot path. This is the outer, invocation-level decision only.
enum class WorkArm {
DashdFallback, ///< retained dashd getblocktemplate RPC arm (reward-safe default)
EmbeddedEligible, ///< daemonless embedded arm armed AND fed
};

/// Why the resolution came out the way it did — the operator-facing diagnosis
/// for the startup banner, and the assertion handle for the tests.
enum class ArmReason {
NoOptIn, ///< mainnet, no --embedded-mainnet: the default posture
MainnetGateOff, ///< a coin-P2P feed was requested but the arm was NOT opted in
NoCoinStateFeed, ///< the arm is enabled but nothing feeds NodeCoinState
Armed, ///< arm enabled AND fed
};

/// The invocation-level inputs, straight off argv.
struct ArmInputs {
bool testnet = false; ///< --testnet / --regtest
bool embedded_mainnet = false; ///< --embedded-mainnet (the embedded opt-in)
bool coin_p2p_connect = false; ///< at least one --coin-p2p-connect HOST:PORT
bool coin_p2p_discover = false; ///< --coin-p2p-discover
};

struct ArmResolution {
WorkArm arm = WorkArm::DashdFallback;
ArmReason reason = ArmReason::NoOptIn;

/// Construct the CoinClient + HeaderChain + CoinStateMaintainer + ingest
/// legs? (main_dash's `if (coin_p2p)` block.)
bool coin_feed_armed = false;

/// Turn on seed-based peer discovery because the embedded opt-in was given
/// with no pinned peer — the daemonless posture needs somewhere to sync
/// from. False whenever the operator named peers or asked for discovery
/// themselves.
bool discover_implied = false;

/// The work-source gate: DASHWorkSource::set_embedded_mainnet / is_testnet.
bool embedded_arm_enabled = false;
};

/// Pure resolution. No hidden state, no ordering dependence.
inline ArmResolution resolve_embedded_arm(const ArmInputs& in)
{
ArmResolution r;

// Half (1): the arm gate. Byte-identical to the predicate DASHWorkSource
// applies internally (`is_testnet_ || embedded_mainnet_`), so the two
// halves cannot drift.
r.embedded_arm_enabled = in.testnet || in.embedded_mainnet;

// Half (2): the feed. An operator-named transport (pinned peers or explicit
// discovery) arms it, exactly as before.
const bool explicit_feed = in.coin_p2p_connect || in.coin_p2p_discover;

// ★ The one-way implication. ONLY the embedded opt-in may imply a feed, and
// only when the operator named no transport of their own. The converse is
// deliberately absent: no transport flag ever sets embedded_arm_enabled.
r.discover_implied = in.embedded_mainnet && !explicit_feed;
r.coin_feed_armed = explicit_feed || r.discover_implied;

if (!r.embedded_arm_enabled) {
// Mainnet with no opt-in. A requested feed still runs (block relay,
// witness) but cannot move the arm — the hotel-incident invariant.
r.arm = WorkArm::DashdFallback;
r.reason = explicit_feed ? ArmReason::MainnetGateOff : ArmReason::NoOptIn;
} else if (!r.coin_feed_armed) {
// Reachable for a bare `--testnet` (arm enabled by network, no feed
// requested and none implied, because --embedded-mainnet is what
// implies one). Unchanged behaviour.
r.arm = WorkArm::DashdFallback;
r.reason = ArmReason::NoCoinStateFeed;
} else {
r.arm = WorkArm::EmbeddedEligible;
r.reason = ArmReason::Armed;
}
return r;
}

inline const char* arm_name(WorkArm a)
{
switch (a) {
case WorkArm::EmbeddedEligible: return "embedded";
case WorkArm::DashdFallback: break;
}
return "dashd-fallback";
}

inline const char* arm_reason_text(ArmReason r)
{
switch (r) {
case ArmReason::NoOptIn:
return "no --embedded-mainnet (default reward-safe posture)";
case ArmReason::MainnetGateOff:
return "coin-P2P feed requested but --embedded-mainnet absent "
"(transport only; the arm never follows the transport)";
case ArmReason::NoCoinStateFeed:
return "embedded arm enabled but no coin-state feed "
"(--coin-p2p-connect / --coin-p2p-discover / --embedded-mainnet)";
case ArmReason::Armed:
break;
}
return "embedded opt-in + coin-state feed both present "
"(per-template viability gates still fail closed to dashd)";
}

} // namespace coin
} // namespace dash
Loading
Loading