From 991a319c13a95bd2fb59b2bddcb12405b2170941 Mon Sep 17 00:00:00 2001 From: integrator Date: Sun, 26 Jul 2026 18:16:24 +0400 Subject: [PATCH] dash: emit the canonical p2pool coinbase marker and wire --coinbase-text Block explorers attribute blocks to a pool by coinbase text. chainz.cryptoid.info/dash/extraction.dws?30.htm registers this pool as "P2Pool-DASH" and has no knowledge of the string "c2pool", so the blocks c2pool won for the p2pool-dash sharechain (2511241, 2511303) were credited to nobody. c2pool was writing scriptSig `03c751266332706f6f6c` = BIP34 height push + bare "c2pool". DASH now defaults to the oracle COINBASEEXT payload plus an implementation tag: mainnet 03c75126 2f5032506f6f6c2d444153482f6332706f6f6c2f = push3 height || "/P2Pool-DASH/c2pool/" (24 bytes) testnet 03f07e0e 2f5032506f6f6c2d74444153482f6332706f6f6c2f = push3 height || "/P2Pool-tDASH/c2pool/" (25 bytes) Sourced from the coin SSOT (config_pool.hpp COINBASEEXT_HEX, transcribed verbatim from p2pool-dash networks/dash.py:11 / dash_testnet.py:11 with the push opcode; coinbaseext_text() strips that opcode so the emitted value is plain text an operator can override). --coinbase-text was a documented, implemented feature (README "Coinbase structure", src/core/coinbase_builder.hpp) that main_ltc.cpp wires and main_dash.cpp never did: it was absent from the DASH binary's --help and unusable on that lane. It is now parsed, bounded by c2pool::MAX_OPERATOR_TEXT_SOLO (64 B; DASH has no merged-mining commitment sharing the budget), resolved once before any coinbase is built, and echoed at startup. The override lives on the coin SSOT rather than being threaded through each call because DASH builds the scriptSig in TWO places -- coinbase::build() for the stratum job / block coinbase and mint::build_producer_job() for share_data['coinbase']. A one-byte divergence between them would make the node self-reject its own shares. Both now go through dash::coinbase::build_coinbase_scriptsig(), and a KAT pins their equality. Not consensus-bearing. COINBASEEXT appears nowhere in the oracle's data.py; the scriptSig travels as share_info.share_data.coinbase (VarStr, 2..100 B) and Share.check() re-derives the gentx from the RECEIVED share's own coinbase field. Framing is preserved and tested: the BIP34 height push stays first and cannot be reached by the 100-byte cap (worst-case 5-byte push + 64-byte operator max), and the stratum extranonce2 slot does not move -- DASH's extranonce2 is the 8-byte nonce64 in the OP_RETURN, and coinb1/coinb2 split at an offset derived from the tx tail. Other lanes are unchanged and reported, not touched: LTC writes /c2pool/, BTC /c2pool-btc/, BCH /c2pool-bch/, DGB writes an empty scriptSig on the stratum path. Only p2pool-dash defines a COINBASEEXT among the oracles available here. README updated: the config table no longer claims a single /c2pool/ default, and a per-lane operator-text table plus the attribution rationale were added. Tests fold into the existing allowlisted test_dash_coinbase_parity target (9 new cases, no new add_executable): mainnet + testnet scriptSig KATs, the pre-change bytes as a negative control, SSOT/oracle-constant pins, the --coinbase-text override, the 100-byte bound with BIP34 intact, end-to-end bytes inside the serialized coinbase with the coinb1/coinb2 split checked, and mint-vs-block scriptSig equality. --- README.md | 27 ++- src/c2pool/main_dash.cpp | 57 ++++- src/impl/dash/coinbase_builder.hpp | 100 ++++++++- src/impl/dash/config_pool.hpp | 122 ++++++++++ src/impl/dash/mint_runloop.hpp | 25 ++- src/impl/dash/stratum/work_source.cpp | 4 +- test/test_dash_coinbase_parity.cpp | 295 ++++++++++++++++++++++++- test/test_dash_stratum_work_source.cpp | 9 +- 8 files changed, 605 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index bb796e1c6..72cc08a20 100644 --- a/README.md +++ b/README.md @@ -393,7 +393,7 @@ complete examples with all options documented. | -- | `explorer_url` | -- | Explorer URL injected into dashboard nav (e.g. `http://localhost:9090`) | | -- | `explorer_depth_ltc` | 288 | LTC blocks to keep in explorer store | | -- | `explorer_depth_doge` | 1440 | DOGE blocks to keep in explorer store | -| `--coinbase-text` | `coinbase_text` | /c2pool/ | Custom coinbase scriptSig text | +| `--coinbase-text` | `coinbase_text` | see below | Custom coinbase scriptSig text | | `--message-blob-hex` | -- | -- | V36 authority message blob | | `--doge-testnet4alpha` | `doge_testnet4alpha` | false | Use DOGE testnet4alpha | @@ -465,7 +465,7 @@ Every block found by c2pool embeds structured data in the coinbase scriptSig: ``` [4] BIP34 block height (consensus) [44] AuxPoW merged mining commitment (when active) -[N] Operator text (--coinbase-text, default "/c2pool/") +[N] Operator text (--coinbase-text, see per-lane defaults below) [32] THE state root (sharechain state commitment) [M] THE metadata (pool analytics, fills remaining space) Total: 100 bytes (Bitcoin consensus limit) @@ -475,6 +475,29 @@ The THE state root commits the sharechain state at block-find time (PPLNS distribution, chain height, difficulty). Any node can verify a found block's payouts match the committed state root. +### Operator text defaults + +`--coinbase-text` is capped at 64 bytes (20 when merged mining is active, since +the AuxPoW commitment shares the same 100-byte budget). + +| Lane | Default operator text | +|------|-----------------------| +| LTC (`c2pool`) | `/c2pool/` | +| DASH (`c2pool-dash`) mainnet | `/P2Pool-DASH/c2pool/` | +| DASH (`c2pool-dash`) testnet / regtest | `/P2Pool-tDASH/c2pool/` | + +The DASH default embeds the canonical p2pool marker (`COINBASEEXT` from the +p2pool-dash oracle `networks/dash.py`) because block explorers attribute blocks +to a pool by coinbase text — [chainz.cryptoid.info](https://chainz.cryptoid.info/dash/extraction.dws?30.htm) +lists the pool as `P2Pool-DASH` and does not know the string `c2pool`. The +`c2pool/` suffix records which implementation mined the block. The coinbase text +is not consensus-bearing (peers never re-derive it), so overriding it cannot +orphan a share — but an override that drops `/P2Pool-DASH/` makes your blocks +unattributable on explorers. + +DASH sources this default from its coin SSOT (`src/impl/dash/config_pool.hpp`); +the same pattern is available for the other lanes but is not yet wired there. + --- ## Authority message blobs (V36) diff --git a/src/c2pool/main_dash.cpp b/src/c2pool/main_dash.cpp index f8e4db1af..e6cfa9ce3 100644 --- a/src/c2pool/main_dash.cpp +++ b/src/c2pool/main_dash.cpp @@ -46,6 +46,7 @@ #include #include +#include // c2pool::MAX_OPERATOR_TEXT_SOLO (--coinbase-text budget SSOT) #include // raise_nofile_limit (hotel interim fix #4) #include #include // NetService (dashd RPC endpoint) @@ -220,7 +221,7 @@ void print_banner(const char* argv0) << " [--give-author PCT] [-f|--fee PCT] [--node-owner-address ADDR]\n" << " [--redistribute pplns|fee|boost|donate]\n" << " [--coin-zmq-hashblock tcp://HOST:PORT]\n" - << " [--message-blob-hex HEX]\n" + << " [--message-blob-hex HEX] [--coinbase-text TEXT]\n" << " " << argv0 << " --mine-block [--coin-rpc H:P] [--coin-rpc-auth PATH]\n" << " [--testnet] [--payout-pubkey-hash HEX] [--max-nonce N]\n\n" << "Status: consensus layer live (X11 PoW, subsidy, oracle CoinParams).\n" @@ -244,6 +245,16 @@ void print_banner(const char* argv0) << " (default mainnet bf0c6bbd / testnet cee2caff; regtest fcc1b7dc).\n" << " --regtest-force-won-block (regtest E5 harness, fail-closed) drives\n" << " ONE real won block through the run-path dual-path dispatch.\n" + << " --coinbase-text TEXT sets the coinbase scriptSig text written\n" + << " after the BIP34 height push (README \"Coinbase structure\"; max\n" + << " 64 bytes, no merged mining on the DASH lane). Default\n" + << " \"/P2Pool-DASH/c2pool/\" (testnet \"/P2Pool-tDASH/c2pool/\") --\n" + << " the /P2Pool-DASH/ marker is what block explorers match on to\n" + << " attribute a block to this pool; the c2pool suffix says which\n" + << " implementation mined it. Non-consensus: the coinbase text is\n" + << " never re-derived by peers, so overriding it cannot orphan a\n" + << " share -- but an override that drops /P2Pool-DASH/ makes your\n" + << " blocks unattributable on explorers.\n" << " --coin-p2p-discover arms the DASH-isolated peer manager: seed\n" << " (dnsseed.dash.org + fixed) bootstrap, source-scored + group-diverse\n" << " (Sybil-capped) peer selection, anchors, and a self-healing dial\n" @@ -508,6 +519,16 @@ int run_node(bool testnet, const std::string& rpc_endpoint, // topology. Mirrors the dgb::Node bring-up (src/c2pool/main_dgb.cpp). dash::SharechainConfig::is_testnet = testnet; + // Effective coinbase scriptSig text, resolved from the coin SSOT (operator + // --coinbase-text override, else the network default). Logged at startup so + // the bytes explorers will see are visible without decoding a block. + std::cout << "[run] coinbase text: \"" + << dash::SharechainConfig::coinbase_text(testnet) << "\"" + << (dash::SharechainConfig::coinbase_text_override.empty() + ? " (default; --coinbase-text to customize)" + : " (--coinbase-text override)") + << "\n"; + // Bucket-1 ISOLATION PRIMITIVE: DASH keeps its own net subdir + PREFIX, // per-coin AND per-pool-instance, in v36 and v37 — never standardised. const std::string net_subdir = testnet ? "dash_testnet" : "dash"; @@ -1529,8 +1550,9 @@ int run_node(bool testnet, const std::string& rpc_endpoint, work.m_coinbase_value, work.m_packed_payments, payout_pkh, empty_weights, /*total_weight=*/0, params); auto layout = dash::coinbase::build( - work, tx_outs, /*pool_tag=*/"c2pool", params, - /*ref_hash=*/uint256::ZERO); + work, tx_outs, + /*coinbase_text=*/dash::SharechainConfig::coinbase_text(params.is_testnet), + params, /*ref_hash=*/uint256::ZERO); // 3) X11-mine to satisfy the template bits. dash::coin::MineResult mr = dash::coin::mine_block( work, layout.bytes, /*max_nonce=*/2000000ull); @@ -1852,7 +1874,8 @@ int run_node(bool testnet, const std::string& rpc_endpoint, guard->chain, mint_params, prev_share_hash, identity->payout_script, wd, static_cast(std::time(nullptr)), share_nonce, - identity->donation_u16, /*pool_tag=*/"c2pool", + identity->donation_u16, + /*coinbase_text=*/dash::SharechainConfig::coinbase_text(mint_params.is_testnet), local_hash_rate); if (!built) return std::nullopt; @@ -3459,8 +3482,10 @@ int run_mine_block(bool testnet, const std::string& rpc_endpoint, empty_weights, /*total_weight=*/0, params); // ref_hash is the PPLNS commitment; for a standalone producer block we use // zero (no sharechain commitment) -- consensus-irrelevant to dashd validity. - auto layout = dash::coinbase::build(work, tx_outs, /*pool_tag=*/"c2pool", - params, /*ref_hash=*/uint256::ZERO); + auto layout = dash::coinbase::build( + work, tx_outs, + /*coinbase_text=*/dash::SharechainConfig::coinbase_text(params.is_testnet), + params, /*ref_hash=*/uint256::ZERO); std::cout << "[mine] coinbase built: " << layout.bytes.size() << " bytes, " << tx_outs.size() << " outputs\n"; @@ -3533,6 +3558,7 @@ int main(int argc, char** argv) uint16_t stratum_port = 0; // 0 disables the Stratum accept-loop; --stratum sets it bool embedded_utxo = false; // --embedded-utxo: arm the E2b UTXO/fee lane (opt-in) double dev_donation = 0.1; // --give-author (donation_percentage; README default 0.1%) + std::string coinbase_text; // --coinbase-text (empty => network default from the SSOT) double node_owner_fee = 0.0; // -f / --fee (node_owner_fee; default 0) std::string node_owner_address; // --node-owner-address (fee destination) // Web dashboard (the EXISTING c2pool dashboard, same defaults as main_ltc.cpp: @@ -3603,6 +3629,8 @@ int main(int argc, char** argv) embedded_superblock = true; else if (std::strcmp(argv[i], "--embedded-utxo") == 0) embedded_utxo = true; + else if (std::strcmp(argv[i], "--coinbase-text") == 0 && i + 1 < argc) + coinbase_text = argv[++i]; else if ((std::strcmp(argv[i], "--give-author") == 0 || std::strcmp(argv[i], "--dev-donation") == 0) && i + 1 < argc) dev_donation = std::strtod(argv[++i], nullptr); @@ -3651,6 +3679,23 @@ int main(int argc, char** argv) // --selftest is the default; accepted explicitly for symmetry. } + // ── --coinbase-text: resolve ONCE, here, before any coinbase is built ──── + // DASH has no merged mining and writes no THE state-root/metadata tail, so + // the operator slot is bounded by MAX_OPERATOR_TEXT_SOLO (README "Coinbase + // structure"); the BIP34 height push (<=5 B) plus 64 B stays well inside the + // 100-byte scriptSig limit. Stored on the coin SSOT rather than threaded + // through every call so the stratum job path and the share-mint path cannot + // disagree -- a one-byte divergence between them would make the node + // self-reject its own shares. + if (!coinbase_text.empty()) { + if (coinbase_text.size() > c2pool::MAX_OPERATOR_TEXT_SOLO) { + std::cout << "[args] --coinbase-text too long: " << coinbase_text.size() + << " bytes (max " << c2pool::MAX_OPERATOR_TEXT_SOLO << ")\n"; + return 2; + } + dash::SharechainConfig::coinbase_text_override = coinbase_text; + } + print_banner(argv[0]); if (want_help) return 0; diff --git a/src/impl/dash/coinbase_builder.hpp b/src/impl/dash/coinbase_builder.hpp index f4dfe9f47..816e46c17 100644 --- a/src/impl/dash/coinbase_builder.hpp +++ b/src/impl/dash/coinbase_builder.hpp @@ -11,8 +11,8 @@ // [TxIn: // [prev_hash 32B zeros][prev_n 0xFFFFFFFF] // [VarStr scriptSig: -// [BIP34 height push][pool_tag] ← no extranonce here anymore -// ] +// [BIP34 height push][coinbase text] +// ] ← no extranonce here anymore // [sequence 0xFFFFFFFF] // ] // [vout VarInt] @@ -37,6 +37,7 @@ #include "coin/transaction.hpp" #include "coin/rpc_data.hpp" +#include "config_pool.hpp" // SSOT: SharechainConfig::COINBASEEXT_HEX / IMPL_TAG #include "share_check.hpp" // decode_payee_script, pubkey_hash_to_script2, DONATION_SCRIPT #include "payout_muldiv.hpp" // dash::payout::payout_share (MSVC-portable 128-bit muldiv) @@ -253,6 +254,84 @@ inline std::vector push_bip34_height(uint32_t height) return out; } +// Maximum coinbase scriptSig length. Oracle data.py Share.__init__ rejects a +// share whose share_data['coinbase'] is outside 2..100 bytes, and work.py:339 +// slices the assembled stratum scriptSig to [:100]. Both bounds are the same +// number; every scriptSig c2pool emits goes through build_coinbase_scriptsig() +// so neither can be breached from one path only. +static constexpr size_t MAX_SCRIPTSIG_LEN = 100; + +// ── build_coinbase_scriptsig — THE single coinbase-scriptSig SSOT ─────────── +// +// Emits: [BIP34 height push][coinbase text] truncated to 100 B +// +// mainnet, height 2511303, default text: +// 03 c75126 | 2f 50 32 50 6f 6f 6c 2d 44 41 53 48 2f 63 32 70 6f 6f 6c 2f +// = push3 "\xc7\x51\x26" + "/P2Pool-DASH/c2pool/" (24 bytes) +// +// `coinbase_text` is the operator-facing text slot (--coinbase-text / +// pool.yaml coinbase_text, README "Coinbase structure"). Pass an empty string +// to take the network default from the SSOT — SharechainConfig::coinbase_text() +// resolves the override-or-default, so the stratum job path and the share-mint +// path cannot end up disagreeing. +// +// WHY THIS DEFAULT +// * "/P2Pool-DASH/" is the oracle's COINBASEEXT payload (networks/dash.py:11; +// testnet "/P2Pool-tDASH/"). Block explorers attribute blocks to a pool by +// coinbase text — chainz.cryptoid.info registers this pool as "P2Pool-DASH" +// and has no knowledge of the string "c2pool", so before this every block +// the pool won through c2pool was credited to nobody. +// * "c2pool/" follows so a human reading the coinbase can still tell WHICH +// p2pool implementation produced the block. +// +// FRAMING — the two things that must not move +// 1. BIP34. The height push stays FIRST and can never be displaced or +// truncated: dashd's ContextualCheckBlock compares the scriptSig PREFIX to +// CScript() << nHeight, so anything appended after it is invisible to that +// check. Worst case the push is 5 bytes (data-push of a 4-byte +// CScriptNum), leaving 95 for the text; the default needs 20 (mainnet) / +// 21 (testnet) and --coinbase-text is capped at 64 by the caller, so the +// 100-byte truncation cannot reach back into the height push. +// 2. Extranonce offsets. DASH's stratum extranonce2 is the 8-byte nonce64 +// inside the OP_RETURN output, NOT inside the scriptSig. coinb1/coinb2 +// split at nonce64_offset, which build() derives from the tx TAIL +// (total - payload - 4 - 8). A longer scriptSig lengthens coinb1 and +// leaves the advertised extranonce2_size (8) and coinb2 byte-identical. +// +// CONSENSUS: the coinbase text is a customizable parameter and is not part of +// what peers re-derive. Oracle data.py Share.check() calls +// generate_transaction(tracker, SELF.share_info['share_data'], ...) — it feeds +// the RECEIVED share's own coinbase field back in; COINBASEEXT appears nowhere +// in data.py. c2pool's mirror, share_check.hpp::generate_share_transaction, +// likewise does `tx << share.m_coinbase`. The hash_link is length-agnostic +// (prefix_to_hash_link/check_hash_link derive the 64-byte-block split from the +// actual prefix length, and dash's hash_link_type carries extra_data as a +// VarStr precisely so a variable-length prefix works). Empirically: c2pool has +// been writing "c2pool" — matching no network constant — and canonical +// p2pool-dash peers accept its shares with zero bans across thousands of shares +// and two dashd-confirmed mainnet blocks (2511241, 2511303). +// +// The ONE hazard this function exists to close: dash builds the scriptSig in +// TWO places — coinbase::build() (the stratum job / real block coinbase) and +// mint::build_producer_job() (share_data['coinbase']). If those two ever +// disagree by a single byte, the minted share's gentx no longer matches the +// block coinbase and the node self-rejects. They now both call this. +inline std::vector build_coinbase_scriptsig( + uint32_t height, const std::string& coinbase_text, bool testnet) +{ + std::vector script = push_bip34_height(height); + + const std::string text = coinbase_text.empty() + ? dash::SharechainConfig::coinbase_text(testnet) + : coinbase_text; + + for (unsigned char c : text) { + if (script.size() >= MAX_SCRIPTSIG_LEN) break; // oracle work.py [:100] + script.push_back(c); + } + return script; +} + // Convert compact nbits → share difficulty (Bitcoin bdiff formula). inline double bits_to_difficulty(uint32_t nbits) { @@ -275,16 +354,16 @@ inline double bits_to_difficulty(uint32_t nbits) // DONATION_SCRIPT so gentx_before_refhash lines up. // Zero-value entries are emitted as-is (callers that // want to strip them should do so before calling). -// pool_tag — short tag in coinbase scriptSig (no extranonce) -// params — coin params (unused here; retained for symmetry) +// coinbase_text — operator coinbase scriptSig text (--coinbase-text); +// empty -> the network default from the SSOT (no extranonce) +// params — coin params; params.is_testnet selects the network default // ref_hash — 32B PPLNS commitment embedded in OP_RETURN inline CoinbaseLayout build(const dash::coin::DashWorkData& work, const std::vector& tx_outs_ordered, - const std::string& pool_tag, + const std::string& coinbase_text, const core::CoinParams& params, const uint256& ref_hash) { - (void)params; using namespace dash::coin; MutableTransaction tx; @@ -295,10 +374,11 @@ inline CoinbaseLayout build(const dash::coin::DashWorkData& work, in.prevout.index = 0xFFFFFFFFu; in.sequence = 0xFFFFFFFFu; { - std::vector script; - auto h_push = push_bip34_height(work.m_height); - script.insert(script.end(), h_push.begin(), h_push.end()); - script.insert(script.end(), pool_tag.begin(), pool_tag.end()); + // SSOT: same helper mint::build_producer_job uses for + // share_data['coinbase'] — the two must stay byte-identical or a + // minted share's gentx stops matching the block coinbase. + auto script = build_coinbase_scriptsig( + work.m_height, coinbase_text, params.is_testnet); in.scriptSig = OPScript(script.data(), script.data() + script.size()); } tx.vin.push_back(std::move(in)); diff --git a/src/impl/dash/config_pool.hpp b/src/impl/dash/config_pool.hpp index 0b79d8dbc..ac5f8af29 100644 --- a/src/impl/dash/config_pool.hpp +++ b/src/impl/dash/config_pool.hpp @@ -88,6 +88,128 @@ struct SharechainConfig 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; } + // ---- COINBASEEXT: the canonical p2pool coinbase marker ------------------ + // SOURCE OF TRUTH: oracle networks/dash.py:11 / dash_testnet.py:11 — + // dash.py COINBASEEXT = '0D2F5032506F6F6C2D444153482F'.decode('hex') + // dash_testnet.py COINBASEEXT = '0E2F5032506F6F6C2D74444153482F'.decode('hex') + // Transcribed VERBATIM, including the leading one-byte push opcode (0x0D = + // push 13, the length of "/P2Pool-DASH/"; 0x0E = push 14 for + // "/P2Pool-tDASH/"), so this header pins the oracle constant exactly as the + // oracle writes it. What c2pool EMITS is the text payload with the push + // opcode stripped — see coinbaseext_text() for why. + // + // WHY IT EXISTS: block explorers attribute blocks to a pool BY COINBASE + // TEXT. chainz.cryptoid.info/dash/extraction.dws?30.htm registers the pool + // as "P2Pool-DASH" and has no knowledge of the string "c2pool", so blocks + // c2pool won for the p2pool-dash sharechain were credited to nobody. + // + // CONSENSUS STATUS: NOT consensus-bearing — the coinbase text is a + // customizable parameter (that is what --coinbase-text is). COINBASEEXT + // appears NOWHERE in the oracle's data.py (the consensus module); it lives + // only in networks/*.py and the stratum assembly at work.py:339. The + // scriptSig travels on the wire as share_info.share_data.coinbase (VarStr, + // 2..100 B) and Share.check() re-derives the gentx from the RECEIVED + // share's own coinbase field, never from a network constant. Framing notes + // (BIP34 prefix, extranonce offsets) on build_coinbase_scriptsig(). + // + // REGTEST: the oracle's dash_regtest.py:11 constant + // '0F2F5032506F6F6C2D724441534828' is MALFORMED — it declares push-15 but + // carries only 14 bytes, and its final byte is 0x28 '(' where a '/' (0x2F) + // was clearly intended ("/P2Pool-rDASH("). c2pool has no separate regtest + // sharechain profile (main_dash.cpp maps --regtest onto testnet=true), so a + // c2pool regtest node emits the tDASH marker and the oracle typo is not + // reproduced. Recorded here so the divergence is deliberate, not drift. + static inline const std::string COINBASEEXT_HEX = "0D2F5032506F6F6C2D444153482F"; + static inline const std::string TESTNET_COINBASEEXT_HEX = "0E2F5032506F6F6C2D74444153482F"; + + // Implementation tag appended after the marker so a human reading the + // coinbase can tell WHICH p2pool implementation produced the block. Shared + // by mainnet and testnet. + static inline const std::string IMPL_TAG = "c2pool/"; + + // Explicit-network forms — PREFERRED. Callers on the coinbase path already + // hold core::CoinParams::is_testnet, so they never have to trust the mutable + // process-global below (and tests can exercise both networks in one binary). + static const std::string& coinbaseext_hex(bool testnet) + { + return testnet ? TESTNET_COINBASEEXT_HEX : COINBASEEXT_HEX; + } + static const std::string& coinbaseext_hex() { return coinbaseext_hex(is_testnet); } + + // COINBASEEXT decoded to raw bytes, push opcode INCLUDED — the oracle's + // literal constant. Local decoder: this header is the SSOT the conformance + // tests pin and deliberately carries no dependency beyond + // //uint256. + static std::string coinbaseext_bytes(bool testnet) + { + const std::string& h = coinbaseext_hex(testnet); + auto nib = [](char c) -> int { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; + }; + std::string out; + out.reserve(h.size() / 2); + for (size_t i = 0; i + 1 < h.size(); i += 2) { + const int hi = nib(h[i]), lo = nib(h[i + 1]); + if (hi < 0 || lo < 0) return {}; // malformed SSOT -> emit nothing + out.push_back(static_cast((hi << 4) | lo)); + } + return out; + } + static std::string coinbaseext_bytes() { return coinbaseext_bytes(is_testnet); } + + // The marker TEXT — COINBASEEXT with its leading push opcode stripped: + // mainnet "/P2Pool-DASH/" testnet "/P2Pool-tDASH/" + // + // This, not the raw constant, is what c2pool writes. The push opcode is + // dropped deliberately: c2pool exposes the coinbase scriptSig payload as an + // operator-settable TEXT parameter (--coinbase-text, README "Coinbase + // structure"), and a bare control byte inside a text field would be lost the + // moment an operator overrode it — the default would then behave unlike + // every other value the field can take. Attribution is unaffected: explorers + // key on the coinbase TEXT (cryptoid lists this pool as "P2Pool-DASH"), and + // the ASCII substring "/P2Pool-DASH/" is byte-identical to what a canonical + // p2pool-dash node renders. The push byte is a script-encoding artefact of + // p2pool's assembly, not part of the name. + // + // Fail-closed: if the SSOT hex is ever edited into an inconsistent state + // (leading byte != payload length, as in the oracle's own regtest constant), + // this returns the decoded bytes unchanged rather than silently trimming a + // real character. + static std::string coinbaseext_text(bool testnet) + { + std::string b = coinbaseext_bytes(testnet); + if (b.size() >= 2 && + static_cast(b[0]) == b.size() - 1) + return b.substr(1); + return b; + } + + // Default coinbase scriptSig text for this network: + // mainnet "/P2Pool-DASH/c2pool/" testnet "/P2Pool-tDASH/c2pool/" + // The p2pool marker makes explorers attribute the block to the pool; the + // c2pool suffix says which implementation mined it. + static std::string default_coinbase_text(bool testnet) + { + return coinbaseext_text(testnet) + IMPL_TAG; + } + + // ---- Operator override (--coinbase-text / pool.yaml coinbase_text) ------- + // Pool-level runtime setting, resolved ONCE at startup in main_dash.cpp + // before any coinbase is built. Empty means "use the network default". + // Lives here rather than being threaded through every build call so the + // stratum job path and the share-mint path cannot end up disagreeing. + static inline std::string coinbase_text_override; + + static std::string coinbase_text(bool testnet) + { + return coinbase_text_override.empty() ? default_coinbase_text(testnet) + : coinbase_text_override; + } + static std::string coinbase_text() { return coinbase_text(is_testnet); } + // ---- Dust threshold (payout-dust semantic) ----------------------------- // DUST_THRESHOLD: minimum per-recipient payout to justify a coinbase output. // SOURCE: p2pool-dash oracle DUST_THRESHOLD = 0.001e8 = 100000 satoshi diff --git a/src/impl/dash/mint_runloop.hpp b/src/impl/dash/mint_runloop.hpp index ae5d6b027..4e016f2c7 100644 --- a/src/impl/dash/mint_runloop.hpp +++ b/src/impl/dash/mint_runloop.hpp @@ -33,7 +33,7 @@ #include "share_producer.hpp" #include "share_producer_bind.hpp" // FrozenMintJob, build_mint_share -#include "coinbase_builder.hpp" // push_bip34_height (BIP34 scriptSig SSOT) +#include "coinbase_builder.hpp" // build_coinbase_scriptsig (BIP34 + coinbase-text SSOT) #include "coin/rpc_data.hpp" // dash::coin::DashWorkData #include "stratum/work_source.hpp" // DASHWorkSource::{MintShareInputs, ProducerJob, PplnsWeights} #include "stratum/work_target.hpp" // dash::stratum::modulate_desired_share_target (Cap 1) @@ -125,7 +125,7 @@ inline std::optional build_producer_job( uint32_t desired_timestamp, uint32_t share_nonce, uint16_t donation, - const std::string& pool_tag, + const std::string& coinbase_text, double local_hash_rate = 0.0) { // Miner identity: DASH sharechain payouts are P2PKH-keyed (share_data @@ -137,16 +137,17 @@ inline std::optional build_producer_job( if (wd.m_bits == 0 || wd.m_height == 0) return std::nullopt; // no real template -> nothing to commit to - // share_data['coinbase'] — BIP34 height push + pool tag, capped at the - // verifier's 100-byte scriptSig bound (oracle work.py packs height+flags - // and slices [:100]). push_bip34_height is the same SSOT coinbase::build - // uses, so the share-gentx scriptSig matches dashd's bad-cb-height check. - std::vector script_sig = dash::coinbase::push_bip34_height(wd.m_height); - for (char c : pool_tag) { - if (script_sig.size() >= 100) break; - script_sig.push_back(static_cast(c)); - } - if (script_sig.size() < 2 || script_sig.size() > 100) + // share_data['coinbase'] — BIP34 height push + the pool's coinbase text + // (default "/P2Pool-DASH/c2pool/"), capped at the verifier's 100-byte + // scriptSig bound (oracle work.py packs height+flags+COINBASEEXT and slices + // [:100]). build_coinbase_scriptsig is the SHARED SSOT coinbase::build uses, so + // this share-gentx scriptSig is byte-identical to the block coinbase served + // over stratum (a divergence here would make the node self-reject its own + // shares) and still carries dashd's bad-cb-height BIP34 prefix first. + std::vector script_sig = + dash::coinbase::build_coinbase_scriptsig(wd.m_height, coinbase_text, params.is_testnet); + if (script_sig.size() < 2 || + script_sig.size() > dash::coinbase::MAX_SCRIPTSIG_LEN) return std::nullopt; // verifier bound (share_check) — fail-closed dash::producer::ProducerJobInputs pin; diff --git a/src/impl/dash/stratum/work_source.cpp b/src/impl/dash/stratum/work_source.cpp index 21a124be1..6d16da95d 100644 --- a/src/impl/dash/stratum/work_source.cpp +++ b/src/impl/dash/stratum/work_source.cpp @@ -695,7 +695,9 @@ core::stratum::CoinbaseResult DASHWorkSource::build_connection_coinbase( weights, total_weight, params); dash::coinbase::CoinbaseLayout layout = dash::coinbase::build( - *wd, tx_outs, /*pool_tag=*/"c2pool", params, ref_hash); + *wd, tx_outs, + /*coinbase_text=*/dash::SharechainConfig::coinbase_text(params.is_testnet), + params, ref_hash); dash::coinbase::CoinbSplit split = dash::coinbase::split_coinb(layout); core::stratum::CoinbaseResult out; diff --git a/test/test_dash_coinbase_parity.cpp b/test/test_dash_coinbase_parity.cpp index 77ed347ca..19a03fea4 100644 --- a/test/test_dash_coinbase_parity.cpp +++ b/test/test_dash_coinbase_parity.cpp @@ -29,6 +29,7 @@ #include +#include #include #include #include @@ -243,4 +244,296 @@ TEST(DashCoinbaseParity, V36DonationFloorOneSat) { uint64_t sum = 0; for (auto& o : outs) sum += o.amount; EXPECT_EQ(sum, subsidy); // invariant preserved -} \ No newline at end of file +} + + +// ───────────────────────────────────────────────────────────────────────────── +// (11)-(19) Coinbase text — the canonical p2pool marker + --coinbase-text. +// +// Block explorers attribute blocks to a pool BY COINBASE TEXT. +// chainz.cryptoid.info/dash/extraction.dws?30.htm registers the pool as +// "P2Pool-DASH"; it has no knowledge of the string "c2pool". Before this, +// c2pool's DASH coinbase read `03c751266332706f6f6c` = BIP34 height + "c2pool", +// so blocks the pool won through c2pool (2511241, 2511303) were attributed to +// nobody. The default coinbase text is now the oracle COINBASEEXT payload +// ("/P2Pool-DASH/", networks/dash.py:11) plus a "c2pool/" implementation tag, +// sourced from the coin SSOT (config_pool.hpp) and overridable per-pool with +// --coinbase-text. +// +// NOT consensus-bearing: the coinbase text is a customizable parameter. +// COINBASEEXT appears nowhere in the oracle's data.py; the scriptSig travels as +// share_info.share_data.coinbase and Share.check() re-derives the gentx from the +// RECEIVED share's own coinbase field. What these tests DO guard is FRAMING — +// the BIP34 height push must stay first, the 100-byte bound must hold, and the +// stratum extranonce2 slot must not move. +// ───────────────────────────────────────────────────────────────────────────── + +namespace { + +// Oracle marker payloads, transcribed independently of config_pool.hpp so a +// silent edit to the SSOT cannot silently move these KATs with it. +// networks/dash.py:11 '0D2F5032506F6F6C2D444153482F' = 0x0D "/P2Pool-DASH/" +// networks/dash_testnet.py:11 '0E2F5032506F6F6C2D74444153482F' = 0x0E "/P2Pool-tDASH/" +const std::string kOracleMarkerMain = "/P2Pool-DASH/"; +const std::string kOracleMarkerTest = "/P2Pool-tDASH/"; + +std::string ascii(const std::vector& v) { + return std::string(v.begin(), v.end()); +} + +// RAII for the pool-level --coinbase-text override: it is a process global, so +// one test must not leak into the next. +struct ScopedCoinbaseText { + std::string saved; + explicit ScopedCoinbaseText(const std::string& v) + : saved(dash::SharechainConfig::coinbase_text_override) { + dash::SharechainConfig::coinbase_text_override = v; + } + ~ScopedCoinbaseText() { + dash::SharechainConfig::coinbase_text_override = saved; + } +}; + +} // namespace + +// (11) MAINNET KAT — exact scriptSig bytes at the real height of the block the +// pool won on 2026-07-24 (2511303 = 0x2651c7 -> BIP34 push `03 c7 51 26`). +TEST(DashCoinbaseMarker, MainnetScriptSigKAT) { + ScopedCoinbaseText no_override(""); + auto s = dash::coinbase::build_coinbase_scriptsig( + /*height=*/2511303, /*coinbase_text=*/"", /*testnet=*/false); + + EXPECT_EQ(hex(s), + "03c75126" // BIP34 push3, height 2511303 LE + "2f5032506f6f6c2d444153482f6332706f6f6c2f"); // "/P2Pool-DASH/c2pool/" + EXPECT_EQ(s.size(), 24u); + EXPECT_EQ(ascii(s).substr(4), "/P2Pool-DASH/c2pool/"); + // The marker an explorer matches on is present verbatim. + EXPECT_NE(ascii(s).find(kOracleMarkerMain), std::string::npos); +} + +// (12) TESTNET KAT — the tDASH variant, never the mainnet string. (c2pool has +// no separate regtest sharechain profile: main_dash.cpp maps --regtest +// onto testnet=true, so regtest emits these same bytes.) +TEST(DashCoinbaseMarker, TestnetScriptSigKAT) { + ScopedCoinbaseText no_override(""); + auto s = dash::coinbase::build_coinbase_scriptsig( + /*height=*/950000, /*coinbase_text=*/"", /*testnet=*/true); + + EXPECT_EQ(hex(s), + "03f07e0e" // BIP34 push3, height 950000 LE + "2f5032506f6f6c2d74444153482f6332706f6f6c2f"); // "/P2Pool-tDASH/c2pool/" + EXPECT_EQ(s.size(), 25u); + EXPECT_EQ(ascii(s).substr(4), "/P2Pool-tDASH/c2pool/"); + EXPECT_NE(ascii(s).find(kOracleMarkerTest), std::string::npos); + // Testnet must NOT carry the mainnet marker. + EXPECT_EQ(ascii(s).find(kOracleMarkerMain), std::string::npos); +} + +// (13) NEGATIVE CONTROL — the bytes c2pool used to emit. This is the exact +// scriptSig observed on live mainnet block 2511303 before the change; it +// must satisfy none of the assertions above, proving they discriminate. +TEST(DashCoinbaseMarker, LegacyBareTagIsNotAttributable) { + ScopedCoinbaseText no_override(""); + + // Reconstruct the pre-change form: BIP34 height + raw "c2pool", no marker. + std::vector legacy = dash::coinbase::push_bip34_height(2511303); + for (char c : std::string("c2pool")) + legacy.push_back(static_cast(c)); + + EXPECT_EQ(hex(legacy), "03c751266332706f6f6c"); // as seen on-chain + EXPECT_EQ(ascii(legacy).find("P2Pool"), std::string::npos); + EXPECT_EQ(ascii(legacy).find(kOracleMarkerMain), std::string::npos); + + auto now = dash::coinbase::build_coinbase_scriptsig(2511303, "", false); + EXPECT_NE(hex(now), hex(legacy)); + // The BIP34 height push is byte-identical across old and new — the text is + // appended, never displacing dashd's ContextualCheckBlock prefix. + EXPECT_EQ(hex(now).substr(0, 8), hex(legacy).substr(0, 8)); +} + +// (14) SSOT wiring — config_pool.hpp carries the oracle hex verbatim (push +// opcode included) and coinbaseext_text() strips exactly that opcode. +TEST(DashCoinbaseMarker, ConfigPoolCarriesOracleConstant) { + EXPECT_EQ(dash::SharechainConfig::COINBASEEXT_HEX, + "0D2F5032506F6F6C2D444153482F"); + EXPECT_EQ(dash::SharechainConfig::TESTNET_COINBASEEXT_HEX, + "0E2F5032506F6F6C2D74444153482F"); + + // Raw oracle bytes: leading push opcode == payload length. + const std::string m = dash::SharechainConfig::coinbaseext_bytes(false); + ASSERT_EQ(m.size(), 14u); + EXPECT_EQ(static_cast(m[0]), 0x0D); + EXPECT_EQ(m.size() - 1, static_cast(static_cast(m[0]))); + const std::string t = dash::SharechainConfig::coinbaseext_bytes(true); + ASSERT_EQ(t.size(), 15u); + EXPECT_EQ(static_cast(t[0]), 0x0E); + EXPECT_EQ(t.size() - 1, static_cast(static_cast(t[0]))); + + // Text form == the ASCII an explorer reads. + EXPECT_EQ(dash::SharechainConfig::coinbaseext_text(false), kOracleMarkerMain); + EXPECT_EQ(dash::SharechainConfig::coinbaseext_text(true), kOracleMarkerTest); + + EXPECT_EQ(dash::SharechainConfig::default_coinbase_text(false), + "/P2Pool-DASH/c2pool/"); + EXPECT_EQ(dash::SharechainConfig::default_coinbase_text(true), + "/P2Pool-tDASH/c2pool/"); +} + +// (15) --coinbase-text override: the operator value replaces the default on +// BOTH the explicit-argument path and the SSOT-resolved path, and the +// default returns once the override is cleared. +TEST(DashCoinbaseMarker, OperatorOverrideReplacesDefault) { + { + ScopedCoinbaseText ov("/my-pool/"); + EXPECT_EQ(dash::SharechainConfig::coinbase_text(false), "/my-pool/"); + auto s = dash::coinbase::build_coinbase_scriptsig(2511303, "", false); + EXPECT_EQ(hex(s), "03c75126" "2f6d792d706f6f6c2f"); // "/my-pool/" + EXPECT_EQ(ascii(s).find(kOracleMarkerMain), std::string::npos); + + // An explicit non-empty argument still wins over the pool setting — + // that is what the E5/--mine-block call sites pass. + auto e = dash::coinbase::build_coinbase_scriptsig(2511303, "/other/", false); + EXPECT_EQ(ascii(e).substr(4), "/other/"); + } + // Override scope ended -> default restored. + ScopedCoinbaseText no_override(""); + EXPECT_EQ(dash::SharechainConfig::coinbase_text(false), "/P2Pool-DASH/c2pool/"); +} + +// (16) 100-byte bound + BIP34 integrity under an absurd text. The oracle rejects +// share_data['coinbase'] outside 2..100 bytes (data.py:315) and work.py +// slices [:100]; the height push must survive both. +TEST(DashCoinbaseMarker, TruncationRespectsBoundAndKeepsHeightPush) { + ScopedCoinbaseText no_override(""); + const std::string absurd(400, 'X'); + auto s = dash::coinbase::build_coinbase_scriptsig(2511303, absurd, false); + + EXPECT_EQ(s.size(), dash::coinbase::MAX_SCRIPTSIG_LEN); // exactly 100, never more + EXPECT_GE(s.size(), 2u); + // BIP34 height push intact and still FIRST. + auto h = dash::coinbase::push_bip34_height(2511303); + ASSERT_GE(s.size(), h.size()); + EXPECT_TRUE(std::equal(h.begin(), h.end(), s.begin())); + + // Worst-case BIP34 push is 5 bytes (4-byte CScriptNum + length); even then + // the default text is nowhere near the cap, and --coinbase-text is bounded + // at c2pool::MAX_OPERATOR_TEXT_SOLO (64) by main_dash.cpp. + auto tall = dash::coinbase::push_bip34_height(0x7FFFFFFFu); + EXPECT_EQ(tall.size(), 5u); + auto s2 = dash::coinbase::build_coinbase_scriptsig(0x7FFFFFFFu, "", false); + EXPECT_EQ(s2.size(), 5u + 20u); + EXPECT_LT(5u + 64u, dash::coinbase::MAX_SCRIPTSIG_LEN); + EXPECT_NE(ascii(s2).find(kOracleMarkerMain), std::string::npos); +} + +// (17) END-TO-END: the text actually lands in the coinbase TX that +// coinbase::build() serializes (this is the tx a winning miner submits). +// Fixed tx prefix: [version 4][vin count 1][prev_hash 32][prev_n 4] = 41 +// bytes, so the scriptSig VarStr length byte sits at offset 41. +TEST(DashCoinbaseMarker, BuildEmitsMarkerInSerializedCoinbase) { + ScopedCoinbaseText no_override(""); + auto params = dash::make_coin_params(/*testnet=*/false); + ASSERT_FALSE(params.is_testnet); + + dash::coin::DashWorkData work; + work.m_height = 2511303; + work.m_coinbase_value = 5000000000ull; + work.m_bits = 0x1b0404cb; + + std::vector finder_h(20, 0x07); + auto tx_outs = dash::coinbase::compute_dash_payouts( + work.m_coinbase_value, /*payments=*/{}, uint160(finder_h), + /*weights=*/{}, /*total_weight=*/0, params); + + auto layout = dash::coinbase::build( + work, tx_outs, + dash::SharechainConfig::coinbase_text(params.is_testnet), + params, uint256::ZERO); + + const auto expected = dash::coinbase::build_coinbase_scriptsig( + work.m_height, /*coinbase_text=*/"", params.is_testnet); + + ASSERT_GT(layout.bytes.size(), 41u + 1u + expected.size()); + EXPECT_EQ(layout.bytes[41], static_cast(expected.size())); + std::vector on_wire(layout.bytes.begin() + 42, + layout.bytes.begin() + 42 + expected.size()); + EXPECT_EQ(hex(on_wire), hex(expected)); + EXPECT_EQ(hex(on_wire), + "03c751262f5032506f6f6c2d444153482f6332706f6f6c2f"); + + // FRAMING: the split point is derived from the tx TAIL, so a longer + // scriptSig cannot move the 8-byte extranonce2 slot — coinb2 stays + // [locktime||payload] and the advertised extranonce2_size is unchanged. + auto split = dash::coinbase::split_coinb(layout); + EXPECT_EQ(dash::coinbase::EXTRANONCE2_SIZE, 8u); + EXPECT_EQ(split.coinb1_hex.size(), layout.nonce64_offset * 2); + EXPECT_EQ(split.coinb2_hex.size(), + (layout.bytes.size() - layout.nonce64_offset - 8) * 2); + EXPECT_EQ(split.coinb2_hex, "00000000"); // locktime only, no payload + EXPECT_EQ(layout.nonce64_offset, layout.ref_hash_offset + 32); +} + +// (18) Testnet params select the testnet text end-to-end (build() reads +// params.is_testnet, not the mutable process-global). +TEST(DashCoinbaseMarker, BuildHonoursTestnetParams) { + ScopedCoinbaseText no_override(""); + auto params = dash::make_coin_params(/*testnet=*/true); + ASSERT_TRUE(params.is_testnet); + + dash::coin::DashWorkData work; + work.m_height = 950000; + work.m_coinbase_value = 5000000000ull; + + std::vector finder_h(20, 0x07); + auto tx_outs = dash::coinbase::compute_dash_payouts( + work.m_coinbase_value, /*payments=*/{}, uint160(finder_h), + /*weights=*/{}, /*total_weight=*/0, params); + auto layout = dash::coinbase::build( + work, tx_outs, + dash::SharechainConfig::coinbase_text(params.is_testnet), + params, uint256::ZERO); + + const size_t len = layout.bytes[41]; + std::vector on_wire(layout.bytes.begin() + 42, + layout.bytes.begin() + 42 + len); + EXPECT_EQ(hex(on_wire), + "03f07e0e2f5032506f6f6c2d74444153482f6332706f6f6c2f"); + EXPECT_EQ(ascii(on_wire).find(kOracleMarkerMain), std::string::npos); +} + +// (19) The SHARE-MINT path and the BLOCK-COINBASE path derive their scriptSig +// from the same helper. If they ever disagree by one byte the minted +// share's gentx stops matching the block coinbase and the node +// self-rejects, so pin the equality explicitly: what mint_runloop.hpp +// writes into share_data['coinbase'] is exactly what build() serializes. +TEST(DashCoinbaseMarker, MintAndBlockCoinbaseScriptSigsAgree) { + ScopedCoinbaseText no_override(""); + for (bool testnet : {false, true}) { + auto params = dash::make_coin_params(testnet); + dash::coin::DashWorkData work; + work.m_height = testnet ? 950000u : 2511303u; + work.m_coinbase_value = 5000000000ull; + + const std::string resolved = + dash::SharechainConfig::coinbase_text(params.is_testnet); + + // mint_runloop.hpp::build_producer_job body, verbatim. + auto mint_sig = dash::coinbase::build_coinbase_scriptsig( + work.m_height, resolved, params.is_testnet); + ASSERT_GE(mint_sig.size(), 2u); + ASSERT_LE(mint_sig.size(), dash::coinbase::MAX_SCRIPTSIG_LEN); + + std::vector finder_h(20, 0x07); + auto tx_outs = dash::coinbase::compute_dash_payouts( + work.m_coinbase_value, /*payments=*/{}, uint160(finder_h), + /*weights=*/{}, /*total_weight=*/0, params); + auto layout = dash::coinbase::build( + work, tx_outs, resolved, params, uint256::ZERO); + + const size_t len = layout.bytes[41]; + std::vector block_sig( + layout.bytes.begin() + 42, layout.bytes.begin() + 42 + len); + EXPECT_EQ(hex(block_sig), hex(mint_sig)) << "testnet=" << testnet; + } +} diff --git a/test/test_dash_stratum_work_source.cpp b/test/test_dash_stratum_work_source.cpp index 96fc5f8da..4097621cf 100644 --- a/test/test_dash_stratum_work_source.cpp +++ b/test/test_dash_stratum_work_source.cpp @@ -385,8 +385,13 @@ TEST(DashStratumWorkSource, ConnectionCoinbaseIsByteIdenticalToVerifierSSOT) auto tx_outs = dash::coinbase::compute_dash_payouts( kCoinbaseValue, w.m_packed_payments, fixture_miner_pkh(), weights, /*total_weight=*/1, params); - auto layout = dash::coinbase::build(w, tx_outs, "c2pool", params, - /*ref_hash=*/uint256::ZERO); + // Coinbase text comes from the coin SSOT (config_pool.hpp), the same source + // build_connection_coinbase resolves — pinning the literal here would make + // this byte-identity test fail the moment the pool's coinbase text changes, + // which is exactly the knob --coinbase-text exposes. + auto layout = dash::coinbase::build( + w, tx_outs, dash::SharechainConfig::coinbase_text(params.is_testnet), + params, /*ref_hash=*/uint256::ZERO); EXPECT_EQ(reassembled, layout.bytes); // Value splits: worker payout = subsidy - masternode share; the single