diff --git a/src/impl/dash/config_pool.hpp b/src/impl/dash/config_pool.hpp new file mode 100644 index 000000000..b87be24ae --- /dev/null +++ b/src/impl/dash/config_pool.hpp @@ -0,0 +1,85 @@ +#pragma once + +// DASH P2Pool sharechain network configuration (oracle-sourced SSOT). +// +// SOURCE OF TRUTH: the DASH oracle frstrtr/p2pool-dash, networks/dash.py + +// networks/dash_testnet.py (operator 2026-06-17 per-coin re-scope: DASH conforms +// to its OWN older-than-v35 oracle, NOT a v35-uniform baseline). +// +// SCOPE: pins the p2pool *sharechain* framing constants (PREFIX/IDENTIFIER/ +// SHARE_PERIOD/CHAIN_LENGTH/TARGET_LOOKBEHIND/SPREAD/P2P_PORT/WORKER_PORT/ +// MIN_PROTOCOL/MAX_TARGET) as the single place a DASH sharechain constant can +// drift. Consumed by test_dash_conformance (oracle pin) and is the SSOT the +// dash::make_coin_params factory will read. Factory wiring + Fileconfig/pool.yaml +// runtime-override integration are the S6 follow-on, deliberately out of scope +// here so this header carries no file-loading machinery. +// +// PREFIX/IDENTIFIER are ISOLATION PRIMITIVES (operator v36_standardization_goal +// 2026-06-17): kept per-coin AND per-instance, NEVER unified cross-coin. + +#include +#include + +#include + +namespace dash +{ + +// DASH sharechain p2pool constants. Source of truth: p2pool-dash oracle +// networks/dash.py (mainnet) + networks/dash_testnet.py (testnet). +struct PoolConfig +{ + // ---- mainnet (networks/dash.py) ---- + static constexpr uint16_t P2P_PORT = 8999; + static constexpr uint16_t WORKER_PORT = 7903; + static constexpr uint32_t SHARE_PERIOD = 20; // seconds + static constexpr uint32_t CHAIN_LENGTH = 4320; // 24*60*60//20 + static constexpr uint32_t REAL_CHAIN_LENGTH = 4320; + static constexpr uint32_t TARGET_LOOKBEHIND = 100; + static constexpr uint32_t SPREAD = 10; // blocks + static constexpr uint32_t MINIMUM_PROTOCOL_VERSION = 1700; // protocol v1700 floor + + // ---- testnet (networks/dash_testnet.py) ---- + static constexpr uint16_t TESTNET_P2P_PORT = 18999; + static constexpr uint16_t TESTNET_WORKER_PORT = 17903; + static constexpr uint32_t TESTNET_SHARE_PERIOD = 20; + static constexpr uint32_t TESTNET_CHAIN_LENGTH = 4320; + static constexpr uint32_t TESTNET_REAL_CHAIN_LENGTH = 4320; + + static inline bool is_testnet = false; + + static uint16_t p2p_port() { return is_testnet ? TESTNET_P2P_PORT : P2P_PORT; } + static uint16_t worker_port() { return is_testnet ? TESTNET_WORKER_PORT : WORKER_PORT; } + static uint32_t share_period() { return is_testnet ? TESTNET_SHARE_PERIOD : SHARE_PERIOD; } + static uint32_t chain_length() { return is_testnet ? TESTNET_CHAIN_LENGTH : CHAIN_LENGTH; } + static uint32_t real_chain_length() { return is_testnet ? TESTNET_REAL_CHAIN_LENGTH : REAL_CHAIN_LENGTH; } + + // ISOLATION PRIMITIVES — per-coin AND per-instance, never unified cross-coin. + static inline const std::string IDENTIFIER_HEX = "7242ef345e1bed6b"; + static inline const std::string PREFIX_HEX = "3b3e1286f446b891"; + static inline const std::string TESTNET_IDENTIFIER_HEX = "b6deb1e543fe2427"; + static inline const std::string TESTNET_PREFIX_HEX = "198b644f6821e3b3"; + + static const std::string& identifier_hex() { return is_testnet ? TESTNET_IDENTIFIER_HEX : IDENTIFIER_HEX; } + static const std::string& prefix_hex() { return is_testnet ? TESTNET_PREFIX_HEX : PREFIX_HEX; } + + // MAX_TARGET: easiest allowed share difficulty (share-diff floor). + // mainnet : 0xFFFF * 2**208 (standard bdiff difficulty-1 target) + // testnet : 2**256 // 2**20 - 1 + static uint256 max_target() + { + static const uint256 MAINNET_MAX = [] { + uint256 t; + t.SetHex("00000000ffff0000000000000000000000000000000000000000000000000000"); + return t; + }(); + static const uint256 TESTNET_MAX = [] { + uint256 t; + t.SetHex("00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + return t; + }(); + return is_testnet ? TESTNET_MAX : MAINNET_MAX; + } +}; + +} // namespace dash diff --git a/src/impl/dash/params.hpp b/src/impl/dash/params.hpp new file mode 100644 index 000000000..ecf3b4a45 --- /dev/null +++ b/src/impl/dash/params.hpp @@ -0,0 +1,138 @@ +#pragma once + +// DASH CoinParams factory: builds a fully populated core::CoinParams for the +// DASH (X11) p2pool parent chain. +// +// SOURCE OF TRUTH: the DASH oracle frstrtr/p2pool-dash, networks/dash.py + +// networks/dash_testnet.py (operator 2026-06-17 per-coin re-scope: DASH conforms +// to its OWN older-than-v35 oracle, NOT a v35-uniform baseline). +// +// POOL-LEVEL FIELDS are sourced exclusively from dash::PoolConfig +// (config_pool.hpp) — the sharechain SSOT shipped in PR #146 — so there is +// exactly one place a DASH sharechain constant can drift. COIN-LEVEL fields are +// inlined here with oracle citations (mirroring ltc/params.hpp; DASH carries no +// separate config_coin.hpp). +// +// PREFIX/IDENTIFIER are ISOLATION PRIMITIVES (operator v36_standardization_goal +// 2026-06-17): kept per-coin AND per-instance, NEVER unified cross-coin. + +#include "config_pool.hpp" +#include "crypto/hash_x11.hpp" +#include "share_check.hpp" // dash::DONATION_SCRIPT (consensus-critical SSOT) + +#include +#include + +#include +#include +#include + +namespace dash +{ + +// Runtime override seam for pool.yaml (Fileconfig consume-side, S6 follow-on). +// ONLY operationally-tunable, NON-consensus, NON-isolation pool fields may be +// overridden by an operator pool.yaml. Consensus-critical fields (share version, +// max_target, donation script, X11 pow/block identity) and the network ISOLATION +// PRIMITIVES (prefix/identifier) are deliberately ABSENT from this struct and are +// therefore NEVER overridable: they stay pinned to the dash::PoolConfig SSOT +// regardless of any override file, so a mis-edited pool.yaml can retune +// ports/peers but can NEVER fork the sharechain off its oracle-conformant +// identity. The YAML file-load half lands when DASH gains its config_pool.cpp +// Fileconfig (mirrors dgb/btc); this header carries no file IO. +struct PoolOverrides +{ + std::optional p2p_port; + std::optional worker_port; + std::optional> bootstrap_addrs; +}; + +inline core::CoinParams make_coin_params(bool testnet, const PoolOverrides& overrides) +{ + core::CoinParams p; + + // ===== Coin-level (net.PARENT) — DASH oracle networks/dash.py PARENT ===== + p.symbol = "DASH"; + p.block_period = 150; // target_spacing (2.5 min), matches header_chain DGW target_spacing + + // Address encoding (oracle PARENT). DASH has a SINGLE P2SH version; there is + // no secondary P2SH prefix and no bech32 HRP on the older baseline. + if (testnet) { + p.address_version = 140; // testnet PUBKEY_ADDRESS (y...) + p.address_p2sh_version = 19; // testnet SCRIPT_ADDRESS + } else { + p.address_version = 76; // mainnet PUBKEY_ADDRESS (X...) + p.address_p2sh_version = 16; // mainnet SCRIPT_ADDRESS (7...) + } + p.address_p2sh_version2 = 0; // DASH: no secondary P2SH prefix + p.bech32_hrp = ""; // DASH: no segwit / no bech32 + + // PoW: X11 work AND X11 block identity (unlike LTC, DASH identifies blocks by + // X11, not SHA256d — see header_chain.hpp block_hash_func = pow_func = x11). + p.pow_func = [](std::span d) -> uint256 { return dash::crypto::hash_x11(d); }; + p.block_hash_func = [](std::span d) -> uint256 { return dash::crypto::hash_x11(d); }; + + // Subsidy: DASH initial 5 DASH, -1/14 (~7.14%) every 210240 blocks (oracle + // PARENT.SUBSIDY_FUNC; halving_interval/initial_subsidy match header_chain). + // [confirm-vs-oracle] gradual-decrease rounding to be re-pinned against a + // captured node corpus in the real-node KAT follow-on. + p.subsidy_func = [](uint32_t height) -> uint64_t { + uint64_t subsidy = 500000000ULL; // 5 DASH + for (uint32_t period = (height + 1) / 210240; period > 0; --period) + subsidy -= subsidy / 14; + return subsidy; + }; + + p.dust_threshold = 5460; // [confirm-vs-oracle] legacy DASH dust floor (relay policy, not consensus) + + // Softforks: DASH older baseline has NO segwit (segwit_activation_version=0). + p.softforks_required = {}; + p.segwit_activation_version = 0; + + // ===== Pool-level (net) — from dash::PoolConfig (config_pool.hpp SSOT) ===== + PoolConfig::is_testnet = testnet; + p.p2p_port = PoolConfig::p2p_port(); + p.worker_port = PoolConfig::worker_port(); + + p.share_period = PoolConfig::share_period(); + p.chain_length = PoolConfig::chain_length(); + p.real_chain_length = PoolConfig::real_chain_length(); + + p.target_lookbehind = PoolConfig::TARGET_LOOKBEHIND; + p.spread = PoolConfig::SPREAD; + p.minimum_protocol_version = PoolConfig::MINIMUM_PROTOCOL_VERSION; + p.block_max_size = 0; // DASH: no segwit weight accounting + p.block_max_weight = 0; + + p.max_target = PoolConfig::max_target(); + + // Network identification — DASH oracle (isolation primitives, never unified). + p.identifier_hex = PoolConfig::IDENTIFIER_HEX; + p.prefix_hex = PoolConfig::PREFIX_HEX; + p.testnet_identifier_hex = PoolConfig::TESTNET_IDENTIFIER_HEX; + p.testnet_prefix_hex = PoolConfig::TESTNET_PREFIX_HEX; + + // Donation script (consensus-critical) — DASH is ALWAYS P2PKH (no segwit, no + // v36 combined-P2SH on the older baseline). Single SSOT = dash::DONATION_SCRIPT. + p.donation_script_func = [](int64_t /*share_version*/) -> std::vector { + return DONATION_SCRIPT; + }; + + p.current_share_version = 16; // DASH older-than-v35 baseline (m_desired_version{16}) + p.is_testnet = testnet; + + // ----- pool.yaml runtime overrides (tunable, non-consensus only) ----- + if (overrides.p2p_port) p.p2p_port = *overrides.p2p_port; + if (overrides.worker_port) p.worker_port = *overrides.worker_port; + if (overrides.bootstrap_addrs) p.bootstrap_addrs = *overrides.bootstrap_addrs; + + return p; +} + +// Convenience overload: no operator overrides -> pure SSOT/oracle CoinParams. +inline core::CoinParams make_coin_params(bool testnet) +{ + return make_coin_params(testnet, PoolOverrides{}); +} + +} // namespace dash diff --git a/test/test_dash_conformance.cpp b/test/test_dash_conformance.cpp index fa67dfbda..be10b7173 100644 --- a/test/test_dash_conformance.cpp +++ b/test/test_dash_conformance.cpp @@ -33,6 +33,7 @@ #include // dash::pplns::compute_payouts, dash::ShareChain, DashShare #include // dash::version_negotiation::get_desired_version_counts/weights #include // dash::coin::vendor::CCbTx, parse_cbtx +#include // dash::PoolConfig (sharechain SSOT) #include // bitcoin_family::coin::SmallBlockHeaderType #include // Hash (sha256d) @@ -742,3 +743,162 @@ TEST(DashConformanceCbtx, ParseCbtxIsExactInverseOfKat) { EXPECT_FALSE(dash::coin::vendor::parse_cbtx(bytes, tx2)) << "parse_cbtx accepted trailing garbage (wire-drift guard defeated)"; } + + +// -- Sharechain network-params conformance (S6 slice 3) ----------------------- +// Pin DASH's p2pool sharechain framing constants against its OWN older-than-v35 +// oracle (frstrtr/p2pool-dash networks/dash.py + dash_testnet.py). The expected +// values below are an INDEPENDENT transcription of the oracle, NOT a re-export of +// dash::PoolConfig, so the assertions catch a drift in either copy -- the same +// anti-circularity design used by the merkle/payout KATs above. +// +// PREFIX/IDENTIFIER are isolation primitives: pinned per-coin here, never to be +// unified cross-coin (operator v36_standardization_goal 2026-06-17). +TEST(DashConformanceNetworkParams, MainnetMatchesP2poolDashOracle) { + dash::PoolConfig::is_testnet = false; + EXPECT_EQ(dash::PoolConfig::p2p_port(), 8999); + EXPECT_EQ(dash::PoolConfig::worker_port(), 7903); + EXPECT_EQ(dash::PoolConfig::share_period(), 20u); + EXPECT_EQ(dash::PoolConfig::chain_length(), 4320u); // 24*60*60//20 + EXPECT_EQ(dash::PoolConfig::real_chain_length(), 4320u); + EXPECT_EQ(dash::PoolConfig::TARGET_LOOKBEHIND, 100u); + EXPECT_EQ(dash::PoolConfig::SPREAD, 10u); + EXPECT_EQ(dash::PoolConfig::MINIMUM_PROTOCOL_VERSION, 1700u); + EXPECT_EQ(dash::PoolConfig::identifier_hex(), std::string("7242ef345e1bed6b")); + EXPECT_EQ(dash::PoolConfig::prefix_hex(), std::string("3b3e1286f446b891")); + uint256 expect_max; + expect_max.SetHex("00000000ffff0000000000000000000000000000000000000000000000000000"); + EXPECT_EQ(dash::PoolConfig::max_target(), expect_max); // 0xFFFF * 2**208 +} + +TEST(DashConformanceNetworkParams, TestnetMatchesP2poolDashOracle) { + dash::PoolConfig::is_testnet = true; + EXPECT_EQ(dash::PoolConfig::p2p_port(), 18999); + EXPECT_EQ(dash::PoolConfig::worker_port(), 17903); + EXPECT_EQ(dash::PoolConfig::share_period(), 20u); + EXPECT_EQ(dash::PoolConfig::chain_length(), 4320u); + EXPECT_EQ(dash::PoolConfig::identifier_hex(), std::string("b6deb1e543fe2427")); + EXPECT_EQ(dash::PoolConfig::prefix_hex(), std::string("198b644f6821e3b3")); + uint256 expect_max; + expect_max.SetHex("00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + EXPECT_EQ(dash::PoolConfig::max_target(), expect_max); // 2**256 // 2**20 - 1 + dash::PoolConfig::is_testnet = false; // restore global for any later tests +} + +// ── Factory↔SSOT conformance (S6 slice: make_coin_params wiring) ───────────── +// The dash::make_coin_params factory MUST populate core::CoinParams pool-level +// fields from the dash::PoolConfig SSOT (config_pool.hpp) and the coin-level +// fields from the DASH oracle, so share_check/coinbase_builder — which consume a +// const core::CoinParams& — never run on an unpopulated or drifted struct. This +// pins the factory output WITHOUT a node dependency: it is the node-free half of +// S6, complementary to the real-node KAT capture. +#include // dash::make_coin_params + +TEST(DashConformanceFactory, PoolFieldsSourcedFromSSOT) { + for (bool testnet : {false, true}) { + dash::PoolConfig::is_testnet = testnet; + auto p = dash::make_coin_params(testnet); + EXPECT_EQ(p.p2p_port, dash::PoolConfig::p2p_port()); + EXPECT_EQ(p.worker_port, dash::PoolConfig::worker_port()); + EXPECT_EQ(p.share_period, dash::PoolConfig::share_period()); + EXPECT_EQ(p.chain_length, dash::PoolConfig::chain_length()); + EXPECT_EQ(p.real_chain_length, dash::PoolConfig::real_chain_length()); + EXPECT_EQ(p.target_lookbehind, dash::PoolConfig::TARGET_LOOKBEHIND); + EXPECT_EQ(p.spread, dash::PoolConfig::SPREAD); + EXPECT_EQ(p.minimum_protocol_version, dash::PoolConfig::MINIMUM_PROTOCOL_VERSION); + EXPECT_EQ(p.identifier_hex, dash::PoolConfig::IDENTIFIER_HEX); + EXPECT_EQ(p.prefix_hex, dash::PoolConfig::PREFIX_HEX); + EXPECT_EQ(p.testnet_identifier_hex, dash::PoolConfig::TESTNET_IDENTIFIER_HEX); + EXPECT_EQ(p.testnet_prefix_hex, dash::PoolConfig::TESTNET_PREFIX_HEX); + dash::PoolConfig::is_testnet = testnet; + EXPECT_EQ(p.max_target, dash::PoolConfig::max_target()); + EXPECT_EQ(p.is_testnet, testnet); + } +} + +TEST(DashConformanceFactory, CoinFieldsMatchDashOracle) { + auto mn = dash::make_coin_params(/*testnet=*/false); + EXPECT_EQ(mn.symbol, "DASH"); + EXPECT_EQ(mn.block_period, 150u); + EXPECT_EQ(mn.address_version, 76); // X... + EXPECT_EQ(mn.address_p2sh_version, 16); // 7... + EXPECT_EQ(mn.address_p2sh_version2, 0); // no secondary P2SH + EXPECT_EQ(mn.segwit_activation_version, 0u); // DASH: no segwit + EXPECT_EQ(mn.current_share_version, 16u); // older-than-v35 baseline + + auto tn = dash::make_coin_params(/*testnet=*/true); + EXPECT_EQ(tn.address_version, 140); // y... + EXPECT_EQ(tn.address_p2sh_version, 19); +} + +// The factory donation script is byte-identical to the DONATION_SCRIPT SSOT for +// every share version (DASH is always P2PKH; no v36 combined-P2SH switch). +TEST(DashConformanceFactory, DonationScriptIsSSOTForAllVersions) { + auto p = dash::make_coin_params(/*testnet=*/false); + for (int64_t v : {0, 15, 16, 35, 36}) { + EXPECT_EQ(p.donation_script_func(v), dash::DONATION_SCRIPT) + << "donation script diverged at share_version=" << v; + } +} + +// PoW and block-identity hashes are BOTH X11 (DASH identifies blocks by X11, +// not SHA256d) — guard against an accidental copy of the LTC sha256d shape. +TEST(DashConformanceFactory, PowAndBlockHashAreBothX11) { + auto p = dash::make_coin_params(/*testnet=*/false); + ASSERT_TRUE(p.pow_func); + ASSERT_TRUE(p.block_hash_func); + std::vector sample(80, 0xab); + std::span s(sample.data(), sample.size()); + uint256 expect = dash::crypto::hash_x11(s); + EXPECT_EQ(p.pow_func(s), expect); + EXPECT_EQ(p.block_hash_func(s), expect); +} + +// ── pool.yaml runtime-override seam (S6 slice: make_coin_params overrides) ──── +// The override overload lets an operator pool.yaml retune ONLY non-consensus, +// non-isolation pool fields (ports, bootstrap peers). With no overrides the +// factory output is byte-identical to the pure-SSOT overload; consensus-critical +// and isolation fields are NEVER reachable from PoolOverrides (compile-time: +// the struct has no field for them) and stay pinned to the SSOT even when the +// tunable fields are overridden. Node-free — pins the seam pool.yaml populates. +TEST(DashConformanceFactory, NoOverridesEqualsSSOTOverload) { + for (bool testnet : {false, true}) { + auto base = dash::make_coin_params(testnet); + auto ovr = dash::make_coin_params(testnet, dash::PoolOverrides{}); + EXPECT_EQ(ovr.p2p_port, base.p2p_port); + EXPECT_EQ(ovr.worker_port, base.worker_port); + EXPECT_EQ(ovr.bootstrap_addrs, base.bootstrap_addrs); + } +} + +TEST(DashConformanceFactory, OverridesApplyToTunableFields) { + dash::PoolOverrides ov; + ov.p2p_port = 19000; + ov.worker_port = 19001; + ov.bootstrap_addrs = std::vector{"seed.example:8999", "node2.example:8999"}; + auto p = dash::make_coin_params(/*testnet=*/false, ov); + EXPECT_EQ(p.p2p_port, 19000); + EXPECT_EQ(p.worker_port, 19001); + ASSERT_EQ(p.bootstrap_addrs.size(), 2u); + EXPECT_EQ(p.bootstrap_addrs[0], "seed.example:8999"); + EXPECT_EQ(p.bootstrap_addrs[1], "node2.example:8999"); +} + +// Even with tunable overrides set, consensus + isolation fields MUST equal the +// SSOT/oracle values — a mis-edited pool.yaml can never fork the sharechain. +TEST(DashConformanceFactory, OverridesNeverTouchConsensusOrIsolation) { + dash::PoolOverrides ov; + ov.p2p_port = 19000; + ov.worker_port = 19001; + auto ssot = dash::make_coin_params(/*testnet=*/false); + auto p = dash::make_coin_params(/*testnet=*/false, ov); + EXPECT_EQ(p.identifier_hex, ssot.identifier_hex); + EXPECT_EQ(p.prefix_hex, ssot.prefix_hex); + EXPECT_EQ(p.testnet_identifier_hex, ssot.testnet_identifier_hex); + EXPECT_EQ(p.testnet_prefix_hex, ssot.testnet_prefix_hex); + EXPECT_EQ(p.max_target, ssot.max_target); + EXPECT_EQ(p.current_share_version, ssot.current_share_version); + EXPECT_EQ(p.minimum_protocol_version, ssot.minimum_protocol_version); + for (int64_t v : {0, 16, 35, 36}) + EXPECT_EQ(p.donation_script_func(v), dash::DONATION_SCRIPT); +}