diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4a594e57b..944ca371e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -114,7 +114,7 @@ jobs: core_test sharechain_test share_test btc_share_test \ test_threading test_weights \ test_header_chain test_mempool test_template_builder \ - 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_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_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_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_multiaddress_pplns test_pplns_stress \ test_hash_link test_decay_pplns \ test_pplns_consensus \ @@ -272,7 +272,7 @@ jobs: test_threading test_weights \ test_header_chain test_mempool test_template_builder \ 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_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_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_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_hash_link test_decay_pplns \ test_pplns_consensus \ test_v36_script_sorting test_v36_cross_impl_refhash \ diff --git a/src/impl/dash/CMakeLists.txt b/src/impl/dash/CMakeLists.txt index fac9b574e..15fb0531b 100644 --- a/src/impl/dash/CMakeLists.txt +++ b/src/impl/dash/CMakeLists.txt @@ -8,6 +8,11 @@ # dash_x11, so ltc/btc/doge/dgb build + ctest surfaces are untouched. add_subdirectory(crypto) +# S8 stratum work-source module (dash::stratum::DASHWorkSource, the concrete +# core::stratum::IWorkSource). OBJECT lib compiled here; linked by c2pool-dash + +# the test_dash_stratum_work_source KAT only. SAFE-ADDITIVE, src/impl/dash only. +add_subdirectory(stratum) + # S5 block-replay slice (PR-0 foundation decomposition): compact-block # reconstruction primitives (BIP-152 short-tx-id / PartiallyDownloadedBlock), # vendored from dashcore with the C2POOL_SERIALIZE_METHODS idiom. SAFE-ADDITIVE: diff --git a/src/impl/dash/stratum/CMakeLists.txt b/src/impl/dash/stratum/CMakeLists.txt new file mode 100644 index 000000000..4c34dcdfc --- /dev/null +++ b/src/impl/dash/stratum/CMakeLists.txt @@ -0,0 +1,22 @@ +# dash::stratum module -- concrete core::stratum::IWorkSource implementation for +# c2pool-dash (DASH X11, V36). Bridges the coin-agnostic core::StratumServer +# (TCP, JSON-RPC, sessions, vardiff, rate monitor) to DASH work generation + +# share validation: the fused get_work() capstone (#698) sources a base template +# off the node-held NodeCoinState (embedded arm) or the RETAINED dashd GBT RPC +# fallback ([GBT-XCHECK], never removed), and the 4d hot path runs the X11 PoW. +# +# OBJECT lib (mirrors src/impl/dash/CMakeLists.txt add_library(dash OBJECT ...)): +# the work-source TU compiles against the header-only coin selector + get_work +# capstone; only c2pool-dash (and the test_dash_stratum_work_source KAT) link the +# objects in, so ltc/btc/doge/dgb build + ctest surfaces stay untouched. +# SAFE-ADDITIVE, per-coin isolation held: src/impl/dash only, dashd RPC fallback +# preserved. Stage 4b: bodies landed (getters + registry + get_work adapter real; +# X11 work-assembly + share-validation held at safe defaults for 4c/4d). +add_library(dash_stratum OBJECT + work_source.hpp work_source.cpp +) +target_include_directories(dash_stratum PUBLIC + ${CMAKE_SOURCE_DIR}/src + ${CMAKE_SOURCE_DIR}/src/btclibs + ${CMAKE_SOURCE_DIR}/include) +target_link_libraries(dash_stratum core dash_x11 nlohmann_json::nlohmann_json ${Boost_LIBRARIES}) diff --git a/src/impl/dash/stratum/work_source.cpp b/src/impl/dash/stratum/work_source.cpp new file mode 100644 index 000000000..196b97c50 --- /dev/null +++ b/src/impl/dash/stratum/work_source.cpp @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// dash::stratum::DASHWorkSource -- Stage 4b bodies. +// +// This TU gives the 4a header a linkable definition: a constructor that binds +// the node-held coin-state + the REQUIRED dashd fallback + the dual-path +// won-block submit callback, real bodies for the read-only getters and the +// per-connection worker registry, and the fused get_work() adapter. The +// substantive X11 work-assembly + share-validation methods (work template, +// coinbase split, mining_submit hot path, compute_share_difficulty) are held +// at their documented safe defaults; they flesh out in 4c/4d exactly as +// dgb::stratum::DGBWorkSource progressed 4a->4b->4c->4d. Landing the bodies +// now makes DASHWorkSource a concrete, INSTANTIABLE core::stratum::IWorkSource +// (all pure virtuals defined) so the next stacked slice can hold it via +// shared_ptr and validate the StratumServer wiring in +// main_dash.cpp end-to-end before the X11 logic lands. +// +// Embedded / fallback duality (MUST PERSIST): get_work() sources the base +// template through dash::stratum::get_work(), which picks the EMBEDDED arm when +// the node-held NodeCoinState bundle is populated and otherwise falls back to +// the always-reachable dashd GBT RPC arm. The dashd-RPC fallback is never +// removed -- it is the safety + [GBT-XCHECK] cross-check path (operator rule). + +#include + +#include + +#include + +namespace dash::stratum { + +DASHWorkSource::DASHWorkSource(const coin::NodeCoinState& coin_state, + std::function dashd_fallback, + SubmitBlockFn submit_fn, + core::stratum::StratumConfig config) + : coin_state_(coin_state) + , dashd_fallback_(std::move(dashd_fallback)) + , submit_block_fn_(std::move(submit_fn)) + , config_(std::move(config)) +{ + LOG_INFO << "[DASH-STRATUM] DASHWorkSource constructed" + << " (min_diff=" << config_.min_difficulty + << " max_diff=" << config_.max_difficulty + << " target_time=" << config_.target_time + << "s vardiff=" << (config_.vardiff_enabled ? "on" : "off") + << " dashd_fallback=" << (dashd_fallback_ ? "wired" : "UNSET") + << " submit_fn=" << (submit_block_fn_ ? "wired" : "UNSET") << ")"; + if (!dashd_fallback_) { + // The fallback is the always-reachable safety arm; an unbound one means + // a set-gap (unpopulated bundle) would have NOTHING to source from. + LOG_WARNING << "[DASH-STRATUM] dashd_fallback UNSET -- get_work() on a " + "coin-state set-gap has no template source (embedded arm " + "only). Bind the dashd GBT RPC fallback in main_dash.cpp."; + } +} + +DASHWorkSource::~DASHWorkSource() = default; + +// ───────────────────────────────────────────────────────────────────────────── +// Fused get_work(): the single miner-facing entry point. Thin node-bound +// adapter over the free dash::stratum::get_work() capstone (#698) -- source the +// base template off the node-held coin-state (embedded when populated, retained +// dashd fallback on a set-gap) and assemble the per-miner job targets over it. +// ───────────────────────────────────────────────────────────────────────────── +GetWork DASHWorkSource::get_work(const WorkJobTargetInputs& job_in) const +{ + // Qualify the free function explicitly: unqualified `get_work` in this + // member body resolves to THIS member (class scope shadows the namespace), + // so name the capstone by its namespace to avoid a self-call. + return ::dash::stratum::get_work(coin_state_, dashd_fallback_, job_in); +} + +// ───────────────────────────────────────────────────────────────────────────── +// IWorkSource: config + read-only state -- real now. +// ───────────────────────────────────────────────────────────────────────────── + +std::function DASHWorkSource::get_best_share_hash_fn() const +{ + std::lock_guard lk(best_share_mutex_); + return best_share_hash_fn_; // empty until set_best_share_hash_fn() wires it +} + +std::string DASHWorkSource::get_current_gbt_prevhash() const +{ + // Held back at the 4b stage: the served template + its previousblockhash + // wire lands with the 4c work-assembly slice (mirrors dgb::stratum 4a/4c). + // A truthful empty absence -- never a fabricated tip id. The stratum server + // falls back to the best-share hash when this is empty. + return {}; +} + +bool DASHWorkSource::has_merged_chain(uint32_t /*chain_id*/) const +{ + // DASH V36: standalone X11 parent, no merged mining. (Any future + // merged-mining seam folds in at v37; never fabricated here.) + return false; +} + +// ───────────────────────────────────────────────────────────────────────────── +// IWorkSource: per-connection bookkeeping -- minimal but real now. +// ───────────────────────────────────────────────────────────────────────────── + +void DASHWorkSource::register_stratum_worker(const std::string& session_id, + const core::stratum::WorkerInfo& info) +{ + std::lock_guard lk(workers_mutex_); + workers_[session_id] = info; + LOG_INFO << "[DASH-STRATUM] worker registered: session=" << session_id + << " user=" << info.username; +} + +void DASHWorkSource::unregister_stratum_worker(const std::string& session_id) +{ + std::lock_guard lk(workers_mutex_); + auto it = workers_.find(session_id); + if (it != workers_.end()) { + LOG_INFO << "[DASH-STRATUM] worker unregistered: session=" << session_id + << " user=" << it->second.username + << " accepted=" << it->second.accepted + << " rejected=" << it->second.rejected + << " stale=" << it->second.stale; + workers_.erase(it); + } + // Unknown session_id: idempotent no-op (the server may double-unregister on + // a racing disconnect) -- never a crash. +} + +void DASHWorkSource::update_stratum_worker(const std::string& session_id, + double hashrate, double dead_hashrate, + double difficulty, + uint64_t accepted, uint64_t rejected, + uint64_t stale) +{ + std::lock_guard lk(workers_mutex_); + auto it = workers_.find(session_id); + if (it == workers_.end()) return; // update for an unregistered session: drop. + it->second.hashrate = hashrate; + it->second.dead_hashrate = dead_hashrate; + it->second.difficulty = difficulty; + it->second.accepted = accepted; + it->second.rejected = rejected; + it->second.stale = stale; +} + +// ───────────────────────────────────────────────────────────────────────────── +// IWorkSource: work generation -- Stage 4c fills these in (safe defaults now). +// ───────────────────────────────────────────────────────────────────────────── + +nlohmann::json DASHWorkSource::get_current_work_template() const +{ + // 4c: assemble the GBT-shaped template off the node-held coin-state (or the + // dashd fallback). An empty object until then -- an honest "no template yet" + // the stratum session treats as no work to push (safe, non-functional). + return nlohmann::json::object(); +} + +std::vector DASHWorkSource::get_stratum_merkle_branches() const +{ + return {}; // 4c: cached branches from the last template build. +} + +std::pair DASHWorkSource::get_coinbase_parts() const +{ + return { {}, {} }; // 4c: cached coinb1/coinb2 (extranonce slot between them). +} + +core::stratum::CoinbaseResult DASHWorkSource::build_connection_coinbase( + const uint256& /*prev_share_hash*/, + const std::string& /*extranonce1_hex*/, + const std::vector& /*payout_script*/, + const std::vector>>& /*merged_addrs*/) const +{ + // 4c/4d: PPLNS->coinbase assembly (byte-identical to the verifier split). + // Empty result until wired -> the session pushes no work (safe, non-functional). + return {}; +} + +// ───────────────────────────────────────────────────────────────────────────── +// IWorkSource: share submission -- Stage 4d (the X11 hot path). +// ───────────────────────────────────────────────────────────────────────────── + +nlohmann::json DASHWorkSource::mining_submit( + const std::string& username, const std::string& job_id, + const std::string& /*extranonce1*/, const std::string& /*extranonce2*/, + const std::string& /*ntime*/, const std::string& /*nonce*/, + const std::string& /*request_id*/, + const std::map>& /*merged_addresses*/, + const core::stratum::JobSnapshot* /*job*/) +{ + // 4d: reconstruct the 80-byte header, run the DASH X11 chained-hash PoW, and + // classify WonBlock -> dual-path broadcaster / ShareAccept -> sharechain mint + // / Reject. Until then every submission is rejected -- the coin-agnostic + // stratum server must never credit a share this skeleton cannot score. + LOG_INFO << "[DASH-STRATUM] mining_submit (4b skeleton -> reject): user=" + << username << " job=" << job_id; + return nlohmann::json::array({ + false, nlohmann::json::array({20, "Not yet implemented (4b skeleton)", nullptr}) + }); +} + +double DASHWorkSource::compute_share_difficulty( + const std::string& /*coinb1*/, const std::string& /*coinb2*/, + const std::string& /*extranonce1*/, const std::string& /*extranonce2*/, + const std::string& /*ntime*/, const std::string& /*nonce*/, + uint32_t /*version*/, const std::string& /*prevhash_hex*/, + const std::string& /*nbits_hex*/, + const std::vector& /*merkle_branches*/) const +{ + // 4b/4c: reconstruct the header, x11_hash(header), return diff1 / pow_hash. + // Until then return 0.0 -- the documented parse-error / not-yet sentinel the + // vardiff gate already treats as a hard reject (no garbage diff leaks into + // the rate monitor). + return 0.0; +} + +// ───────────────────────────────────────────────────────────────────────────── +// DASH-specific control surface. +// ───────────────────────────────────────────────────────────────────────────── + +void DASHWorkSource::set_best_share_hash_fn(std::function fn) +{ + std::lock_guard lk(best_share_mutex_); + best_share_hash_fn_ = std::move(fn); +} + +} // namespace dash::stratum diff --git a/src/impl/dash/stratum/work_source.hpp b/src/impl/dash/stratum/work_source.hpp new file mode 100644 index 000000000..e4b9b9774 --- /dev/null +++ b/src/impl/dash/stratum/work_source.hpp @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +#pragma once + +// dash::stratum::DASHWorkSource -- concrete `core::stratum::IWorkSource` +// implementation for c2pool-dash (Dash X11, V36). +// +// Responsibility: bridge the coin-agnostic `core::StratumServer` (TCP, +// JSON-RPC, sessions, vardiff, rate monitor) to DASH-specific work +// generation + share validation. It is the concrete class the fused +// `dash::stratum::get_work()` capstone (get_work.hpp) refers to as the +// eventual "DASHWorkSource::get_work()" -- the single miner-facing entry +// point that sources a base template off the node-held coin-state and +// assembles the per-miner stratum job targets over it. +// +// The X11 note (NOT scrypt, NOT SHA256d): DASH mines the X11 chained-hash +// PoW. `compute_share_difficulty` encapsulates the X11 hash so the +// coin-agnostic stratum server never hardcodes an algo; the LTC-scrypt / +// BTC-sha256d / DGB-scrypt siblings each override the same seam. +// +// Embedded / fallback duality (MUST PERSIST): work generation sources the +// base block template through `dash::stratum::get_work()`, which itself +// picks the EMBEDDED arm when the node-held `coin::NodeCoinState` bundle is +// populated and otherwise falls back to the always-reachable dashd GBT RPC +// arm (`dashd_fallback_`). The dashd-RPC fallback is never removed -- it is +// the safety + [GBT-XCHECK] cross-check path (operator standing rule). +// +// Lifetime: holds non-owning references to the node-held coin-state and the +// header chain / mempool -- main_dash.cpp owns those for the process +// lifetime, DASHWorkSource is constructed after them and destroyed before. +// The submit-block callback captures whatever upstream state it needs +// (a coin_node ref + the dual-path won-block dispatcher from the +// block-broadcast slice: embedded P2P relay primary + submitblock RPC +// fallback). +// +// Threading: `core::StratumServer` runs on its own io_context; methods here +// may be invoked from any thread serviced by it. Internal synchronisation: +// - `work_generation_`, `share_bits_`, `share_max_bits_` are atomics +// - `workers_` is guarded by `workers_mutex_` +// - `best_share_hash_fn_` / `mint_share_fn_` guarded by their own mutexes +// - the template cache (stage 4c) is guarded by `template_mutex_` +// - `coin_state_` has its own internal locking; `dashd_fallback_` is const +// +// What's deliberately MVP-incomplete in this 4a skeleton (mirrors +// dgb::stratum::DGBWorkSource's own 4a landing): every work-generation / +// submit method returns defaults or empty results; no vardiff feedback loop +// yet. The skeleton compiles, instantiates, and lets the next stacked slice +// validate the StratumServer wiring in main_dash.cpp end-to-end before the +// substantive X11 work-assembly + share-validation logic lands (4b/4c/4d). + +#include +#include + +#include // coin::NodeCoinState, coin::DashWorkData seam +#include // dash::stratum::get_work() fused capstone + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Forward declarations -- heavy headers live in the .cpp. +namespace dash::coin { +class HeaderChain; +class Mempool; +} // namespace dash::coin + +namespace dash::stratum { + +class DASHWorkSource : public core::stratum::IWorkSource +{ +public: + /// Callback invoked when `mining_submit` validates a submission whose + /// **X11** PoW meets DASH mainnet difficulty. main_dash.cpp wires this + /// to the dual-path won-block broadcaster: embedded P2P relay primary + + /// submitblock RPC fallback. Raw-bytes form keeps DASHWorkSource + /// decoupled from the BlockType serialization details. Returns true iff + /// the won block reached at least one network sink -- a false return + /// means it reached NEITHER and the won-block path must log loudly + /// (no silent drop -- the block-viability gate). + using SubmitBlockFn = std::function& block_bytes, + uint32_t height)>; + + /// Construct the DASH work source. Holds a NON-OWNING reference to the + /// node-held coin-state (the embedded arm) and captures the REQUIRED dashd + /// getblocktemplate fallback closure -- the always-reachable safety path + + /// [GBT-XCHECK] cross-check, NEVER removed -- plus the dual-path won-block + /// submit callback. main_dash.cpp owns coin_state for the process lifetime + /// and constructs this after it (see the Lifetime note above); the fallback + /// and submit closures capture the dashd RPC client + won-block dispatcher. + DASHWorkSource(const coin::NodeCoinState& coin_state, + std::function dashd_fallback, + SubmitBlockFn submit_fn = {}, + core::stratum::StratumConfig config = {}); + + ~DASHWorkSource(); + + /// Fused DASH work source: source the base template off the node-held + /// coin-state (embedded when populated, retained dashd fallback on a + /// set-gap) and assemble the per-miner job targets. This is the concrete + /// DASHWorkSource::get_work() the capstone (get_work.hpp) forward-refs; + /// the free function `dash::stratum::get_work()` carries the fusion so + /// the member is a thin, node-bound adapter over it. + GetWork get_work(const WorkJobTargetInputs& job_in) const; + + // ── IWorkSource: config + read-only state ──────────────────────────── + const core::stratum::StratumConfig& get_stratum_config() const override { return config_; } + std::function get_best_share_hash_fn() const override; + std::string get_current_gbt_prevhash() const override; + uint64_t get_work_generation() const override { return work_generation_.load(); } + bool has_merged_chain(uint32_t chain_id) const override; + + // ── IWorkSource: per-connection bookkeeping ────────────────────────── + void register_stratum_worker(const std::string& session_id, + const core::stratum::WorkerInfo& info) override; + void unregister_stratum_worker(const std::string& session_id) override; + void update_stratum_worker(const std::string& session_id, + double hashrate, double dead_hashrate, double difficulty, + uint64_t accepted, uint64_t rejected, uint64_t stale) override; + + // ── IWorkSource: work generation ───────────────────────────────────── + nlohmann::json get_current_work_template() const override; + std::vector get_stratum_merkle_branches() const override; + std::pair get_coinbase_parts() const override; + core::stratum::CoinbaseResult build_connection_coinbase( + const uint256& prev_share_hash, + const std::string& extranonce1_hex, + const std::vector& payout_script, + const std::vector>>& merged_addrs) const override; + + // ── IWorkSource: share submission (the hot path) ───────────────────── + nlohmann::json mining_submit( + const std::string& username, const std::string& job_id, + const std::string& extranonce1, const std::string& extranonce2, + const std::string& ntime, const std::string& nonce, + const std::string& request_id, + const std::map>& merged_addresses, + const core::stratum::JobSnapshot* job) override; + + /// Per-coin PoW-hash difficulty for a pseudoshare. DASH = X11 + /// (chained BLAKE/BMW/Groestl/... over the reconstructed 80-byte + /// header). Stage 4b/4c implements the assembly + X11 call; the 4a + /// skeleton returns 0.0 (the documented parse-error / not-yet default). + double compute_share_difficulty( + const std::string& coinb1, const std::string& coinb2, + const std::string& extranonce1, const std::string& extranonce2, + const std::string& ntime, const std::string& nonce, + uint32_t version, const std::string& prevhash_hex, + const std::string& nbits_hex, + const std::vector& merkle_branches) const override; + + // ── IWorkSource: atomic state ──────────────────────────────────────── + uint32_t get_share_bits() const override { return share_bits_.load(); } + uint32_t get_share_max_bits() const override { return share_max_bits_.load(); } + + // ── DASH-specific control surface (called from main_dash.cpp) ──────── + + /// Increment work_generation. Called when the DASH tip moves (new + /// headers / node coin-state republish) or the sharechain tip moves. + /// Triggers stratum sessions to re-push work on their next heartbeat. + void bump_work_generation() { work_generation_.fetch_add(1, std::memory_order_relaxed); } + + /// Set the current share-target bits (compact-target encoding). + /// `max_bits` is the easiest the share target can be. Both atomically + /// visible to stratum sessions. + void set_share_target(uint32_t bits, uint32_t max_bits) + { + share_bits_.store(bits, std::memory_order_relaxed); + share_max_bits_.store(max_bits, std::memory_order_relaxed); + } + + /// Wire the share-tracker accessor that returns the current best-share + /// hash. Called once at startup from main_dash.cpp after the DASH + /// ShareTracker is constructed. + void set_best_share_hash_fn(std::function fn); + +private: + // External dependencies (non-owning references) -- see Lifetime note. + const coin::NodeCoinState& coin_state_; ///< embedded work arm (populated -> Embedded) + std::function dashd_fallback_; ///< always-reachable dashd GBT RPC arm (never removed) + + // Submission dispatch (dual-path won-block broadcaster). + SubmitBlockFn submit_block_fn_; + + // Config (held by value; const after construction in MVP). + core::stratum::StratumConfig config_; + + // Atomic state. + std::atomic work_generation_{0}; + std::atomic share_bits_{0}; + std::atomic share_max_bits_{0}; + + // Worker registry (per-connection metadata). + mutable std::mutex workers_mutex_; + std::map workers_; + + // Best-share callback (from ShareTracker). Empty until wired. + mutable std::mutex best_share_mutex_; + std::function best_share_hash_fn_; + + // Template cache (filled lazily; invalidated when work_generation_ bumps). + // Stage 4c populates these. + mutable std::mutex template_mutex_; + // ... cache fields land here in stage 4c +}; + +} // namespace dash::stratum diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9cd894ed0..64e58428a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -534,6 +534,19 @@ if (BUILD_TESTING AND GTest_FOUND) target_link_libraries(test_dash_node_coin_state PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39) gtest_add_tests(test_dash_node_coin_state "" AUTO) + # DASHWorkSource (dash::stratum): concrete core::stratum::IWorkSource for + # DASH X11 -- 4b construction + contract KAT. Links the dash_stratum OBJECT + # lib (work_source.cpp) on top of the header-only coin selector link set. + add_executable(test_dash_stratum_work_source test_dash_stratum_work_source.cpp) + target_link_libraries(test_dash_stratum_work_source PRIVATE + GTest::gtest_main GTest::gtest + dash_stratum dash_x11 core + nlohmann_json::nlohmann_json + ${Boost_LIBRARIES} + ) + target_link_libraries(test_dash_stratum_work_source PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39) + gtest_add_tests(test_dash_stratum_work_source "" AUTO) + # CoinStateMaintainer: drives NodeCoinState off async tip/MN/mempool update # events (readiness gate: MN+tip required before publish). Same link set as # test_dash_node_coin_state -- header-only over the work_source selector set. diff --git a/test/test_dash_stratum_work_source.cpp b/test/test_dash_stratum_work_source.cpp new file mode 100644 index 000000000..c6edf5d7c --- /dev/null +++ b/test/test_dash_stratum_work_source.cpp @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// dash::stratum::DASHWorkSource -- Stage 4b construction + contract KAT. +// +// Proves the work source instantiates against the live node-held coin-state +// (dash::coin::NodeCoinState) + the REQUIRED dashd fallback closure, satisfies +// the full core::stratum::IWorkSource pure-virtual contract (so a future +// core::StratumServer can hold it via shared_ptr), that its +// real-now surface (config defaults, work-generation counter, share-target +// atomics, worker registry, best-share callback) behaves, that the 4b-held +// work-generation / submit methods return their documented SAFE DEFAULTS (so a +// regression that accidentally "implements" them with garbage is caught), and +// that the fused get_work() adapter routes to the RETAINED dashd fallback on an +// unpopulated coin-state (the always-reachable [GBT-XCHECK] safety arm). +// +// MUST appear in BOTH this ctest registration AND the build.yml --target +// allowlist, or it becomes a #143-style NOT_BUILT sentinel that reds master. + +#include + +#include +#include + +#include + +#include +#include +#include + +namespace { + +// Construct a DASHWorkSource over a default (unpopulated) NodeCoinState. The +// submit callback records whether it was invoked (it must NOT be in 4b); the +// dashd fallback returns a distinctive sentinel so a get_work() routed to the +// fallback arm is provable by the returned work. +struct Fixture { + dash::coin::NodeCoinState coin_state; // default: populated()==false + bool submit_called = false; + dash::coin::DashWorkData fallback_work; // sentinel, seeded in make() + + std::unique_ptr make() + { + fallback_work.m_height = 424242u; // distinctive fallback marker + fallback_work.m_coinbase_value = 500000000ULL; + auto submit = [this](const std::vector&, uint32_t) -> bool { + submit_called = true; + return false; + }; + auto fallback = [this]() -> dash::coin::DashWorkData { return fallback_work; }; + return std::make_unique( + coin_state, fallback, submit); + } +}; + +TEST(DashStratumWorkSource, ConstructsAndSatisfiesIWorkSourceContract) +{ + Fixture fx; + auto ws = fx.make(); + core::stratum::IWorkSource* iface = ws.get(); // usable through the abstract iface + ASSERT_NE(iface, nullptr); +} + +TEST(DashStratumWorkSource, ConfigDefaultsMatchStratumConfig) +{ + Fixture fx; + auto ws = fx.make(); + const auto& cfg = ws->get_stratum_config(); + EXPECT_DOUBLE_EQ(cfg.min_difficulty, 0.0005); + EXPECT_DOUBLE_EQ(cfg.max_difficulty, 65536.0); + EXPECT_DOUBLE_EQ(cfg.target_time, 3.0); + EXPECT_TRUE(cfg.vardiff_enabled); +} + +TEST(DashStratumWorkSource, WorkGenerationStartsZeroAndBumps) +{ + Fixture fx; + auto ws = fx.make(); + EXPECT_EQ(ws->get_work_generation(), 0u); + ws->bump_work_generation(); + ws->bump_work_generation(); + EXPECT_EQ(ws->get_work_generation(), 2u); +} + +TEST(DashStratumWorkSource, ShareTargetAtomicsRoundTrip) +{ + Fixture fx; + auto ws = fx.make(); + EXPECT_EQ(ws->get_share_bits(), 0u); + EXPECT_EQ(ws->get_share_max_bits(), 0u); + ws->set_share_target(0x1d00ffffu, 0x1e0fffffu); + EXPECT_EQ(ws->get_share_bits(), 0x1d00ffffu); + EXPECT_EQ(ws->get_share_max_bits(), 0x1e0fffffu); +} + +TEST(DashStratumWorkSource, NoMergedChainInV36) +{ + Fixture fx; + auto ws = fx.make(); + // DASH V36 is a standalone X11 parent (no merged mining). + EXPECT_FALSE(ws->has_merged_chain(0x0001)); +} + +TEST(DashStratumWorkSource, BestShareHashFnEmptyUntilWired) +{ + Fixture fx; + auto ws = fx.make(); + EXPECT_FALSE(static_cast(ws->get_best_share_hash_fn())); + ws->set_best_share_hash_fn([]() { return uint256::ZERO; }); + auto fn = ws->get_best_share_hash_fn(); + ASSERT_TRUE(static_cast(fn)); + EXPECT_EQ(fn(), uint256::ZERO); +} + +TEST(DashStratumWorkSource, WorkerRegistryRoundTrip) +{ + Fixture fx; + auto ws = fx.make(); + core::stratum::WorkerInfo info; + info.username = "Xaddr.worker1"; + info.worker_name = "worker1"; + ws->register_stratum_worker("sess-1", info); + ws->update_stratum_worker("sess-1", /*hashrate=*/1.0e9, /*dead=*/0.0, + /*difficulty=*/16.0, /*accepted=*/3, /*rejected=*/0, /*stale=*/0); + // Idempotent: unregister of a known + unknown session must not crash, and an + // update for an unregistered session is a safe drop. + ws->update_stratum_worker("sess-unknown", 1.0, 0.0, 1.0, 0, 0, 0); + ws->unregister_stratum_worker("sess-1"); + ws->unregister_stratum_worker("sess-unknown"); + SUCCEED(); +} + +TEST(DashStratumWorkSource, WorkGenStubsReturnSafeDefaults) +{ + Fixture fx; + auto ws = fx.make(); + // 4b skeleton: every work-generation getter returns its documented + // empty/default form (4c fills them in) -- no fabricated template state. + EXPECT_TRUE(ws->get_current_gbt_prevhash().empty()); + EXPECT_TRUE(ws->get_current_work_template().is_object()); + EXPECT_TRUE(ws->get_current_work_template().empty()); + EXPECT_TRUE(ws->get_stratum_merkle_branches().empty()); + auto parts = ws->get_coinbase_parts(); + EXPECT_TRUE(parts.first.empty()); + EXPECT_TRUE(parts.second.empty()); + auto cb = ws->build_connection_coinbase(uint256::ZERO, "deadbeef", {}, {}); + EXPECT_TRUE(cb.coinb1.empty()); + EXPECT_TRUE(cb.coinb2.empty()); +} + +TEST(DashStratumWorkSource, MiningSubmitStubRejectsWithoutBroadcasting) +{ + Fixture fx; + auto ws = fx.make(); + auto result = ws->mining_submit( + "Xaddr.worker1", "job-0", "en1", "en2", "ntime", "nonce", "rid-0", + /*merged_addresses=*/{}, /*job=*/nullptr); + // Stratum reject form = [false, [code, msg, null]]. + ASSERT_TRUE(result.is_array()); + ASSERT_GE(result.size(), 1u); + EXPECT_FALSE(result[0].get()); + // The 4b stub must NOT have reached the won-block broadcaster. + EXPECT_FALSE(fx.submit_called); +} + +TEST(DashStratumWorkSource, ComputeShareDifficultyReturnsNotYetSentinel) +{ + Fixture fx; + auto ws = fx.make(); + // 4b: the per-coin (X11) PoW-difficulty hook returns the documented 0.0 + // parse-error/not-yet sentinel the vardiff gate treats as a hard reject. + double diff = ws->compute_share_difficulty( + "coinb1", "coinb2", "en1", "en2", "ntime", "nonce", + /*version=*/0x20000000u, "prevhash", "1e0ffff0", + /*merkle_branches=*/{}); + EXPECT_DOUBLE_EQ(diff, 0.0); +} + +// The fused adapter: with an UNPOPULATED coin-state, get_work() routes to the +// RETAINED dashd fallback (always-reachable safety + [GBT-XCHECK] arm), and the +// returned work IS the fallback closure output -- proving the member is a thin +// node-bound wrapper over the #698 capstone, and that the fallback is never +// bypassed on a set-gap. Job targets are assembled over it (pure transform). +TEST(DashStratumWorkSource, GetWorkRoutesToDashdFallbackWhenCoinStateUnpopulated) +{ + Fixture fx; + ASSERT_FALSE(fx.coin_state.populated()); // default bundle is a set-gap + auto ws = fx.make(); + + dash::stratum::WorkJobTargetInputs job_in; + job_in.sane_target_min.SetHex( + "0000000000000000000000000000000000000000000000000000000000000001"); + job_in.sane_target_max.SetHex( + "00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + job_in.share_info_bits_target.SetHex( + "0000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + job_in.have_desired_pseudoshare = false; + job_in.local_hash_rate = 0.0; + + dash::stratum::GetWork gw = ws->get_work(job_in); + + // Routed to the retained dashd fallback, returning ITS sentinel work. + EXPECT_EQ(gw.source, dash::coin::WorkSource::DashdFallback); + EXPECT_EQ(gw.work.m_height, 424242u); + EXPECT_EQ(gw.work.m_coinbase_value, 500000000ULL); + // Job targets assembled over the sourced template (clipped into sane range). + EXPECT_EQ(gw.targets.share_target, job_in.sane_target_max); // ffff.. clipped to max + EXPECT_EQ(gw.targets.min_share_target, job_in.share_info_bits_target); // < sane_max -> floor +} + +} // namespace