Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
57 changes: 51 additions & 6 deletions src/c2pool/main_dash.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
#include <impl/dash/coin/subsidy.hpp>

#include <core/coin_params.hpp>
#include <core/coinbase_builder.hpp> // c2pool::MAX_OPERATOR_TEXT_SOLO (--coinbase-text budget SSOT)
#include <core/core_util.hpp> // raise_nofile_limit (hotel interim fix #4)
#include <core/uint256.hpp>
#include <core/netaddress.hpp> // NetService (dashd RPC endpoint)
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<uint32_t>(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;
Expand Down Expand Up @@ -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";

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
100 changes: 90 additions & 10 deletions src/impl/dash/coinbase_builder.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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)

Expand Down Expand Up @@ -253,6 +254,84 @@ inline std::vector<unsigned char> 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<unsigned char> build_coinbase_scriptsig(
uint32_t height, const std::string& coinbase_text, bool testnet)
{
std::vector<unsigned char> 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)
{
Expand All @@ -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<MinerPayout>& 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;
Expand All @@ -295,10 +374,11 @@ inline CoinbaseLayout build(const dash::coin::DashWorkData& work,
in.prevout.index = 0xFFFFFFFFu;
in.sequence = 0xFFFFFFFFu;
{
std::vector<unsigned char> 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));
Expand Down
Loading
Loading