Skip to content

Commit d9037a8

Browse files
author
integrator
committed
dash(oracle): embedded ORACLE-SHADOW validator + graduation gate (first slice)
Run c2pool ALONGSIDE a Dash Core daemon and use dashd as a CONTINUOUS REALTIME ORACLE for the daemonless embedded DIP4 CbTx/template arm. On every new tip the shadow (OBSERVE-only, --embedded-oracle-shadow): 1. builds the embedded template via the SAME path the serve arm uses (NodeCoinState::select_work) -- Embedded arm, or records a FALL-CLOSED serve-gap when the arm is not armed; 2. VERDICT: assembles the embedded block and submits dashd getblocktemplate{mode:proposal} (NodeRPC::propose_block_hex) -- TestBlockValidity is the authoritative, mempool-independent pass/fail; 3. DIAGNOSIS: regime-aware field compare vs dashd getblocktemplate. Regime model (adversarial-critique determinism table): EQUALITY (nHeight, CbTx version, bits, mintime, payee identity, platform burn, normalized subsidy, MN/ quorum roots on empty templates) COUNT; creditPool is an INVARIANT checked against the normalized base (committed - platformReward - ownDelta == creditPool(N-1)) -- the exact check that caught the seed bug 3x; bestCL is a CONSTRAINT (range, sig-equality only when CL heights coincide); fee/mempool fields are informational and NEVER counted (the separate-mempool false-positive the naive compare would cause). Graduation ledger (JSONL divergence log + JSON state under the data-dir, /embedded_oracle endpoint): GRADUATED == N consecutive served+proposal-accepted clean blocks AND >=K served coverage of each height class (dkg_window, superblock, post_restart_cold, quorum_rotation, credit_pool_transition, non_empty_template) AND >=1 reorg AND serve-rate floor. Selection-bias fix: fall-closed classes are recorded and declared as residual serve-gaps (disabling dashd removes the fallback those classes rely on). Ledger hard-keyed to {c2pool_commit, comparator_version, dashd_version, net}; revocable by chain-drift sentinels (unknown CbTx version / payload parse fail). Soundness grounded on the embedded arm's INDEPENDENT chain-state derivation, NOT peer diversity (DASH has no CoinPeerManager). Graduation scope = chain-state equivalence over the served domain (reward-safe daemonless block construction); full network-standalone operation needs the separate CoinPeerManager gate. Consensus-neutral: the serve/consensus/share path (work_source.cpp, coinbase_builder.hpp, share_check.hpp, pplns.hpp, node_coin_state.hpp, embedded_gbt.hpp) is UNTOUCHED -- the shadow is a separate new_tip subscriber that only reads. web_server gains an additive /embedded_oracle reporting route. Builds c2pool-dash + test_dash_embedded_oracle_shadow (11 KATs, green) pinning the regime model, the fee-difference-is-not-a-divergence keystone, the creditPool invariant, the bestCL constraint, and the graduation predicate. Design doc: frstrtr/the docs/c2pool-dash-embedded-oracle-shadow-validator.md.
1 parent 14718d5 commit d9037a8

9 files changed

Lines changed: 1289 additions & 5 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ jobs:
138138
core_test sharechain_test share_test btc_share_test \
139139
test_threading test_weights \
140140
test_header_chain test_mempool test_template_builder \
141-
test_doge_chain test_compact_blocks test_dash_x11_kat test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_underfill_guard test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_embedded_relay_e2e test_dash_block_relay_plan test_dash_version_activation_latch test_dash_block_relay_binding test_dash_block_relay_dual_arm test_dash_coinbase_parity test_dash_coinbase_muldiv test_dash_donation_combined test_dash_g3_assembled test_dash_work_target test_dash_work_job_targets test_dash_stratum_binding test_dash_job_notify_roundtrip test_dash_cb_payee test_dash_stratum_notify_roundtrip test_dash_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node test_dash_share_messages test_dash_stratum_extranonce_split test_dash_stratum_submit_reassembly test_dash_difficulty_parity test_dash_auto_ratchet test_dash_min_protocol_gate test_dash_work_source test_dash_node_coin_state test_dash_coin_state_maintainer test_dash_node_embedded_wire test_dash_node_reception_wire test_dash_get_work test_dash_stratum_work_source \
141+
test_doge_chain test_compact_blocks test_dash_x11_kat test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_underfill_guard test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_embedded_relay_e2e test_dash_block_relay_plan test_dash_version_activation_latch test_dash_block_relay_binding test_dash_block_relay_dual_arm test_dash_coinbase_parity test_dash_coinbase_muldiv test_dash_donation_combined test_dash_g3_assembled test_dash_work_target test_dash_work_job_targets test_dash_stratum_binding test_dash_job_notify_roundtrip test_dash_cb_payee test_dash_stratum_notify_roundtrip test_dash_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node test_dash_share_messages test_dash_stratum_extranonce_split test_dash_stratum_submit_reassembly test_dash_difficulty_parity test_dash_auto_ratchet test_dash_min_protocol_gate test_dash_work_source test_dash_node_coin_state test_dash_coin_state_maintainer test_dash_node_embedded_wire test_dash_node_reception_wire test_dash_get_work test_dash_stratum_work_source test_dash_embedded_oracle_shadow \
142142
test_multiaddress_pplns test_pplns_stress \
143143
test_hash_link test_decay_pplns \
144144
test_pplns_consensus \
@@ -320,7 +320,7 @@ jobs:
320320
test_threading test_weights \
321321
test_header_chain test_mempool test_template_builder \
322322
test_doge_chain test_compact_blocks test_dash_x11_kat \
323-
test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_underfill_guard test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_embedded_relay_e2e test_dash_block_relay_plan test_dash_version_activation_latch test_dash_block_relay_binding test_dash_block_relay_dual_arm test_dash_coinbase_parity test_dash_coinbase_muldiv test_dash_donation_combined test_dash_g3_assembled test_dash_work_target test_dash_work_job_targets test_dash_stratum_binding test_dash_job_notify_roundtrip test_dash_cb_payee test_dash_stratum_notify_roundtrip test_dash_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node test_dash_share_messages test_dash_stratum_extranonce_split test_dash_stratum_submit_reassembly test_dash_difficulty_parity test_dash_auto_ratchet test_dash_min_protocol_gate test_dash_work_source test_dash_node_coin_state test_dash_coin_state_maintainer test_dash_node_embedded_wire test_dash_node_reception_wire test_dash_get_work test_dash_stratum_work_source \
323+
test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_underfill_guard test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_embedded_relay_e2e test_dash_block_relay_plan test_dash_version_activation_latch test_dash_block_relay_binding test_dash_block_relay_dual_arm test_dash_coinbase_parity test_dash_coinbase_muldiv test_dash_donation_combined test_dash_g3_assembled test_dash_work_target test_dash_work_job_targets test_dash_stratum_binding test_dash_job_notify_roundtrip test_dash_cb_payee test_dash_stratum_notify_roundtrip test_dash_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node test_dash_share_messages test_dash_stratum_extranonce_split test_dash_stratum_submit_reassembly test_dash_difficulty_parity test_dash_auto_ratchet test_dash_min_protocol_gate test_dash_work_source test_dash_node_coin_state test_dash_coin_state_maintainer test_dash_node_embedded_wire test_dash_node_reception_wire test_dash_get_work test_dash_stratum_work_source test_dash_embedded_oracle_shadow \
324324
test_hash_link test_decay_pplns \
325325
test_pplns_consensus \
326326
test_v36_script_sorting test_v36_cross_impl_refhash \

src/c2pool/main_dash.cpp

Lines changed: 131 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
#include <impl/dash/coin/live_feed.hpp> // E2a live-feed bridge (raw wire events -> derived ingest events)
6464
#include <impl/dash/coin/mempool_ingest.hpp> // wire_mempool_ingest (leg 1)
6565
#include <impl/dash/coin/tip_ingest.hpp> // wire_tip_ingest (leg 2)
66+
#include <impl/dash/coin/embedded_oracle_shadow.hpp> // dash::coin::EmbeddedOracleShadow — per-block dashd cross-check (OBSERVE-only)
6667
#include <impl/dash/coin/block_connect_ingest.hpp> // wire_block_connect_ingest (leg 3)
6768
#include <impl/dash/coin/mn_list_ingest.hpp> // wire_mn_list_ingest (leg 4)
6869
#include <impl/dash/coin/mn_seed.hpp> // E2c: RPC protx-list MN-set seed (parse_protx_list_seed)
@@ -192,7 +193,8 @@ void print_banner(const char* argv0)
192193
<< " [--listen [HOST:]PORT] [--addnode HOST:PORT]... [--connect HOST:PORT]...\n"
193194
<< " [--stratum [HOST:]PORT] [--coin-p2p-connect HOST:PORT]...\n"
194195
<< " [--web-port PORT] [--web-host ADDR] [--dashboard-dir PATH]\n"
195-
<< " [--embedded-utxo]\n"
196+
<< " [--embedded-utxo] [--embedded-oracle-shadow]\n"
197+
<< " [--oracle-graduation-blocks N] [--oracle-class-coverage K]\n"
196198
<< " [--give-author PCT] [-f|--fee PCT] [--node-owner-address ADDR]\n"
197199
<< " [--redistribute pplns|fee|boost|donate]\n"
198200
<< " " << argv0 << " --mine-block [--coin-rpc H:P] [--coin-rpc-auth PATH]\n"
@@ -212,6 +214,12 @@ void print_banner(const char* argv0)
212214
<< " --web-port PORT (alias --http-port, default 8080) serves the FULL\n"
213215
<< " c2pool web dashboard + JSON API on --web-host (default 0.0.0.0)\n"
214216
<< " from --dashboard-dir (default web-static); --web-port 0 disables.\n"
217+
<< " --embedded-oracle-shadow runs the OBSERVE-only per-block dashd\n"
218+
<< " cross-check: dashd getblocktemplate{mode:proposal} is the VERDICT,\n"
219+
<< " regime-aware field-compare is the DIAGNOSIS; a persisted graduation\n"
220+
<< " ledger + /embedded_oracle verdict signal when the embedded arm is\n"
221+
<< " proven equivalent (safe to disable dashd, served domain). Needs the\n"
222+
<< " dashd RPC arm; never changes serving. N/K tune the graduation gate.\n"
215223
<< " Live sharechain tip/stats, pool hashrate and per-share difficulty\n"
216224
<< " are bound to the REAL DASH tracker; local hashrate comes from the\n"
217225
<< " DASH stratum acceptor. If stratum and web ports collide the web\n"
@@ -380,7 +388,10 @@ int run_node(bool testnet, const std::string& rpc_endpoint,
380388
double dev_donation, double node_owner_fee,
381389
const std::string& node_owner_address,
382390
const std::string& redistribute_mode,
383-
bool no_p2p_relay)
391+
bool no_p2p_relay,
392+
bool embedded_oracle_shadow = false,
393+
uint64_t oracle_grad_blocks = 5000,
394+
uint64_t oracle_class_coverage = 20)
384395
{
385396
namespace io = boost::asio;
386397

@@ -536,6 +547,12 @@ int run_node(bool testnet, const std::string& rpc_endpoint,
536547
// serve X11 miners from the LTC work source. The dashboard is told the
537548
// miner-facing port through mining_interface->set_worker_port() (display)
538549
// and fed real stratum rates from the DASH acceptor after it starts.
550+
// Embedded ORACLE-SHADOW validator (--embedded-oracle-shadow). Declared here
551+
// (before web_server) so the /embedded_oracle endpoint closure can read it;
552+
// constructed + subscribed to the tip event later in the coin_p2p block, once
553+
// node_coin_state exists. OBSERVE-only: stats_json() touches no serving state.
554+
std::shared_ptr<dash::coin::EmbeddedOracleShadow> oracle_shadow;
555+
539556
std::unique_ptr<core::WebServer> web_server;
540557
auto enhanced_node = std::make_shared<dash::EnhancedDashNode>(testnet);
541558
if (web_port != 0) {
@@ -568,6 +585,16 @@ int run_node(bool testnet, const std::string& rpc_endpoint,
568585
// resolved), not an ICoinNode, so tell the dashboard RPC is present.
569586
mi->set_coin_rpc_available(static_cast<bool>(rpc));
570587

588+
// ── /embedded_oracle stats endpoint (OBSERVE-only) ────────────────
589+
// JSON coverage ledger + objective GRADUATION verdict (safe-to-disable-
590+
// dashd gate). Reads the shadow validator's own structures; never the
591+
// serving path. Empty/disabled shape when --embedded-oracle-shadow off.
592+
mi->set_embedded_oracle_fn([&oracle_shadow]() -> nlohmann::json {
593+
if (oracle_shadow) return oracle_shadow->stats_json();
594+
return nlohmann::json{{"mode", "disabled"},
595+
{"note", "run with --embedded-oracle-shadow to enable"}};
596+
});
597+
571598
web_server->set_dashboard_dir(dashboard_dir);
572599
// Explicitly DISABLE the WebServer's own stratum acceptor. Its ctor
573600
// defaults stratum_port_ to (web_port + 10) (web_server.cpp:8330/8344/
@@ -1278,6 +1305,96 @@ int run_node(bool testnet, const std::string& rpc_endpoint,
12781305
// header chain), NOT the raw wire.
12791306
coin_feed_subs.push_back(
12801307
c2pool::dash::wire_tip_ingest(coin_state, *maintainer));
1308+
1309+
// ── Embedded ORACLE-SHADOW: per-block dashd cross-check (OBSERVE-only) ─
1310+
// Subscribed to the SAME new_tip event AFTER wire_tip_ingest, so the
1311+
// maintainer has already republished the embedded bundle for this tip
1312+
// before the shadow reads it. The shadow builds the embedded template
1313+
// via node_coin_state.select_work (the SAME build path the serve arm
1314+
// uses) and cross-checks it against a fresh dashd getblocktemplate,
1315+
// gating divergence/graduation on the DETERMINISTIC field set only (the
1316+
// two nodes have intentionally different mempools/peers). It NEVER
1317+
// changes the serving decision. Requires the alongside dashd RPC arm.
1318+
if (embedded_oracle_shadow) {
1319+
if (!rpc) {
1320+
std::cout << "[run] --embedded-oracle-shadow given but the dashd "
1321+
"RPC arm is UNARMED (need dash.conf creds / --coin-rpc); "
1322+
"the shadow has no oracle to cross-check against -- "
1323+
"disabled.\n";
1324+
} else {
1325+
dash::coin::EmbeddedOracleShadow::Config oc;
1326+
oc.testnet = testnet;
1327+
oc.grad.consecutive_clean_target = oracle_grad_blocks;
1328+
oc.grad.per_class_coverage_target = oracle_class_coverage;
1329+
#ifdef C2POOL_VERSION
1330+
oc.c2pool_commit = C2POOL_VERSION;
1331+
#endif
1332+
try { oc.dashd_version =
1333+
rpc->getnetworkinfo().value("subversion", std::string{}); }
1334+
catch (...) { /* best-effort ledger identity */ }
1335+
const auto oracle_dir =
1336+
(core::filesystem::config_path() / net_subdir).string();
1337+
oc.divergence_ledger_path =
1338+
oracle_dir + "/embedded_oracle_divergence.jsonl";
1339+
oc.graduation_state_path =
1340+
oracle_dir + "/embedded_oracle_graduation.json";
1341+
1342+
// Proposal VERDICT leg: assemble the embedded block (pool-only
1343+
// coinbase, no miner payout -- consensus-irrelevant to dashd
1344+
// TestBlockValidity) with the SAME SSOTs the --mine-block path
1345+
// uses, and submit getblocktemplate{mode:proposal}. "" = dashd
1346+
// ACCEPTED; else the reject reason. This is the authoritative,
1347+
// mempool-independent per-height verdict (§6 condition 2).
1348+
auto proposal_fn =
1349+
[testnet, rp = rpc.get()](const dash::coin::DashWorkData& wd)
1350+
-> dash::coin::ProposalResult {
1351+
dash::coin::ProposalResult r;
1352+
r.attempted = true;
1353+
try {
1354+
const core::CoinParams params = dash::make_coin_params(testnet);
1355+
std::map<std::vector<unsigned char>, uint64_t> empty_weights;
1356+
uint160 zero_pkh; // pool-only coinbase (no finder)
1357+
auto tx_outs = dash::coinbase::compute_dash_payouts(
1358+
wd.m_coinbase_value, wd.m_packed_payments, zero_pkh,
1359+
empty_weights, /*total_weight=*/0, params);
1360+
auto layout = dash::coinbase::build(
1361+
wd, tx_outs, /*pool_tag=*/"c2pool", params,
1362+
/*ref_hash=*/uint256::ZERO);
1363+
// nonce 0 + curtime: proposal mode skips PoW; validity is
1364+
// structure + payee + CbTx + tx-set (TestBlockValidity).
1365+
auto block = dash::coin::serialize_full_block(
1366+
wd, layout.bytes, /*nonce=*/0,
1367+
wd.m_curtime ? wd.m_curtime
1368+
: static_cast<uint32_t>(std::time(nullptr)));
1369+
const std::string reason = rp->propose_block_hex(HexStr(block));
1370+
r.accepted = reason.empty();
1371+
r.reason = reason;
1372+
} catch (const std::exception& e) {
1373+
r.accepted = false; r.reason = std::string("assemble:") + e.what();
1374+
}
1375+
return r;
1376+
};
1377+
1378+
oracle_shadow = std::make_shared<dash::coin::EmbeddedOracleShadow>(
1379+
node_coin_state,
1380+
[rp = rpc.get()]() { return rp->getwork(); },
1381+
std::move(proposal_fn),
1382+
std::move(oc));
1383+
auto* shadow = oracle_shadow.get();
1384+
coin_feed_subs.push_back(
1385+
coin_state.new_tip.subscribe(
1386+
[shadow](const ::dash::interfaces::TipAdvance& t) {
1387+
shadow->on_new_tip(t.prev_height + 1, t.prev_hash);
1388+
}));
1389+
std::cout << "[run] --embedded-oracle-shadow ARMED: per-block dashd "
1390+
"PROPOSAL verdict + regime-aware field diagnosis (N="
1391+
<< oracle_grad_blocks << " K=" << oracle_class_coverage
1392+
<< "); ledger at " << oracle_dir
1393+
<< "/embedded_oracle_*.{jsonl,json}; verdict at "
1394+
"/embedded_oracle\n";
1395+
}
1396+
}
1397+
12811398
// Leg 3 (block connect): Node::block_connected -> maintainer
12821399
// .on_block_connected (MnStateMachine::apply_block, folds DIP3 special
12831400
// txs into the DMN set). block_connected is fired by the live-feed
@@ -1742,6 +1859,9 @@ int main(int argc, char** argv)
17421859
std::string stratum_host = "0.0.0.0"; // --stratum [HOST:]PORT bind interface (default all)
17431860
uint16_t stratum_port = 0; // 0 disables the Stratum accept-loop; --stratum sets it
17441861
bool embedded_utxo = false; // --embedded-utxo: arm the E2b UTXO/fee lane (opt-in)
1862+
bool embedded_oracle_shadow = false; // --embedded-oracle-shadow: per-block dashd cross-check (OBSERVE-only)
1863+
uint64_t oracle_grad_blocks = 5000; // --oracle-graduation-blocks N (consecutive clean)
1864+
uint64_t oracle_class_coverage = 20; // --oracle-class-coverage K (per height class)
17451865
double dev_donation = 0.1; // --give-author (donation_percentage; README default 0.1%)
17461866
double node_owner_fee = 0.0; // -f / --fee (node_owner_fee; default 0)
17471867
std::string node_owner_address; // --node-owner-address (fee destination)
@@ -1786,6 +1906,12 @@ int main(int argc, char** argv)
17861906
no_p2p_relay = true;
17871907
else if (std::strcmp(argv[i], "--embedded-utxo") == 0)
17881908
embedded_utxo = true;
1909+
else if (std::strcmp(argv[i], "--embedded-oracle-shadow") == 0)
1910+
embedded_oracle_shadow = true;
1911+
else if (std::strcmp(argv[i], "--oracle-graduation-blocks") == 0 && i + 1 < argc)
1912+
oracle_grad_blocks = std::strtoull(argv[++i], nullptr, 10);
1913+
else if (std::strcmp(argv[i], "--oracle-class-coverage") == 0 && i + 1 < argc)
1914+
oracle_class_coverage = std::strtoull(argv[++i], nullptr, 10);
17891915
else if ((std::strcmp(argv[i], "--give-author") == 0 ||
17901916
std::strcmp(argv[i], "--dev-donation") == 0) && i + 1 < argc)
17911917
dev_donation = std::strtod(argv[++i], nullptr);
@@ -1886,7 +2012,9 @@ int main(int argc, char** argv)
18862012
stratum_host, stratum_port, web_host, web_port,
18872013
dashboard_dir, coin_p2p_targets,
18882014
embedded_utxo, dev_donation, node_owner_fee,
1889-
node_owner_address, redistribute_mode, no_p2p_relay);
2015+
node_owner_address, redistribute_mode, no_p2p_relay,
2016+
embedded_oracle_shadow, oracle_grad_blocks,
2017+
oracle_class_coverage);
18902018
}
18912019
return run_selftest();
18922020
}

0 commit comments

Comments
 (0)