Skip to content

dash: emit the canonical p2pool coinbase marker so explorers attribute our blocks (+ wire --coinbase-text) - #901

Merged
frstrtr merged 1 commit into
masterfrom
dash/coinbase-p2pool-marker
Jul 26, 2026
Merged

dash: emit the canonical p2pool coinbase marker so explorers attribute our blocks (+ wire --coinbase-text)#901
frstrtr merged 1 commit into
masterfrom
dash/coinbase-p2pool-marker

Conversation

@frstrtr

@frstrtr frstrtr commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Problem

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.

c2pool was writing scriptSig 03c751266332706f6f6c = BIP34 height push + bare c2pool
(verified on our own live mainnet block 2511303). We are ~87% of the p2pool-dash pool's
hashrate and the two blocks won on 2026-07-24 (2511241, 2511303) are credited to nobody.

Second, smaller problem found on the way: --coinbase-text is an existing, documented
feature (README.md "Coinbase structure", src/core/coinbase_builder.hpp) that
main_ltc.cpp wires and main_dash.cpp never did — it did not appear in the DASH
binary's --help at all, and the README claimed a /c2pool/ default that the DASH lane
did not even use.

What this changes (DASH only)

Default coinbase text, sourced from the coin SSOT:

network scriptSig rendered
mainnet (h=2511303) 03c75126 2f5032506f6f6c2d444153482f6332706f6f6c2f push3 height + /P2Pool-DASH/c2pool/ (24 B)
testnet / regtest (h=950000) 03f07e0e 2f5032506f6f6c2d74444153482f6332706f6f6c2f push3 height + /P2Pool-tDASH/c2pool/ (25 B)

src/impl/dash/config_pool.hpp now carries COINBASEEXT_HEX /
TESTNET_COINBASEEXT_HEX transcribed verbatim from the oracle
(p2pool-dash networks/dash.py:11, dash_testnet.py:11), push opcode included, so the
SSOT pins the oracle constant exactly. coinbaseext_text() strips that opcode; what we
emit is plain text, because the slot is operator-overridable and a bare 0x0D control
byte would vanish the moment anyone set --coinbase-text. Attribution is unaffected —
explorers key on the ASCII, and /P2Pool-DASH/ is byte-identical to what a canonical
p2pool-dash node renders.

--coinbase-text wired into main_dash.cpp, mirroring the LTC pattern: parsed,
bounded by c2pool::MAX_OPERATOR_TEXT_SOLO (64 B — DASH has no merged-mining commitment
sharing the 100-byte budget), resolved once before any coinbase is built, echoed at
startup, and present in --help.

One scriptSig SSOT. DASH built the scriptSig in two places — coinbase::build()
(stratum job / block coinbase) and mint::build_producer_job() (share_data['coinbase']).
A one-byte divergence between them makes the node self-reject its own shares. Both now
call dash::coinbase::build_coinbase_scriptsig(), and a KAT pins their equality. This is
also why the override lives on the SSOT rather than being threaded per-call.

Consensus safety

Verified before touching bytes, not assumed:

  1. Not re-derived by peers. The scriptSig travels on the wire as
    share_info.share_data.coinbase (VarStrType, 2 <= len <= 100, data.py:315).
    Share.check() calls generate_transaction(tracker, SELF.share_info['share_data'], ...)
    — it feeds the received share's own coinbase field back in, so the re-derived gentx
    contains whatever bytes the sender wrote. COINBASEEXT appears nowhere in the
    oracle's data.py; grep puts it only in networks/*.py and the stratum assembly at
    work.py:339. c2pool's mirror share_check.hpp::generate_share_transaction likewise
    does tx << share.m_coinbase.
  2. share_info != self.share_info is satisfied for the same reason — share_data is
    passed through, not rebuilt.
  3. hash_link is length-agnostic. prefix_to_hash_link() takes a SHA-256 midstate over
    the gentx prefix plus extra_data = trailing partial 64-byte block minus the
    const_ending; check_hash_link() reconstructs it from hash_link.length % 64. Both
    sides derive the 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 (the
    upstream assert not extra_data is commented out in the oracle for this reason).
  4. Empirically decisive. 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.

Framing (not consensus, but breaking it breaks mining)

  • BIP34 height push stays first and is never reached by the cap. dashd's
    ContextualCheckBlock compares the scriptSig prefix to CScript() << nHeight.
    Worst-case push is 5 bytes; --coinbase-text is capped at 64; 5 + 64 = 69 < 100.
    Tested with a 400-byte text: result is exactly 100 bytes with the height push intact.
  • Extranonce offsets do not move. DASH's stratum extranonce2 is the 8-byte nonce64
    inside the OP_RETURN output, not the scriptSig. coinb1/coinb2 split at
    nonce64_offset, which build() derives from the tx tail
    (total - payload - 4 - 8). A longer scriptSig lengthens coinb1 only; the advertised
    extranonce2_size (8) and coinb2 are unchanged. Asserted in the KAT.

Other lanes — reported, deliberately not changed

lane emitted today source
LTC /c2pool/ hardcoded twice in src/core/web_server.cpp (756, 1103) + twice in merged/merged_mining.cpp
BTC /c2pool-btc/ src/impl/btc/stratum/work_source.cpp:430
BCH /c2pool-bch/ src/impl/bch/stratum/work_source.cpp:415
DGB nothing — empty scriptSig, no BIP34 height either main_dgb.cpp:861-868 never sets ain.coinbase_script
DASH c2pool -> /P2Pool-DASH/c2pool/ this PR

Adjacent findings, filed here rather than fixed:

  • DGB emits an empty coinbase scriptSig on the stratum path — no pool tag and no
    BIP34 height push. That looks like a real bug, not a cosmetic gap.
  • BTC/BCH writers emit tags their own readers cannot match (block_json.hpp:99 and
    bch/coinbase_commitment.hpp:198 both look for /c2pool/).
  • Only p2pool-dash defines a COINBASEEXT among the oracles available on this box;
    jtoomim's networks/bitcoin.py has none (checked directly). No p2pool-ltc / -dgb / -bch
    checkout exists locally, so their canonical markers are unverified.

Tests

Folded into the existing allowlisted test_dash_coinbase_parity target — no new
add_executable
. 9 new cases; per #895, each was confirmed to actually execute (gtest
[ RUN ]/[ OK ] lines, not a bare ctest tick):

[  PASSED  ] 19 tests.   (10 pre-existing + 9 new)
  DashCoinbaseMarker.MainnetScriptSigKAT
  DashCoinbaseMarker.TestnetScriptSigKAT
  DashCoinbaseMarker.LegacyBareTagIsNotAttributable        <- negative control
  DashCoinbaseMarker.ConfigPoolCarriesOracleConstant
  DashCoinbaseMarker.OperatorOverrideReplacesDefault
  DashCoinbaseMarker.TruncationRespectsBoundAndKeepsHeightPush
  DashCoinbaseMarker.BuildEmitsMarkerInSerializedCoinbase
  DashCoinbaseMarker.BuildHonoursTestnetParams
  DashCoinbaseMarker.MintAndBlockCoinbaseScriptSigsAgree

Negative control run: flipping one nibble of COINBASEEXT_HEX
(...444153482F -> ...444153483F) and rebuilding fails 5 of the 9 with the expected
byte diff ("...444153483f..." vs "...444153482f...") and find(...) == npos on the
marker — so the KATs discriminate rather than tautologically pass. Reverted.

Regression sweep: all 41 test_dash_* binaries built and run — 0 failures.
test_dash_stratum_work_source.cpp:389 had hardcoded "c2pool" in its
stratum-vs-SSOT byte-identity comparison and was updated to read the SSOT (pinning the
literal there would break the test the moment an operator sets --coinbase-text, which is
the whole point of the flag).

--help on the built binary:

       c2pool-dash --run ...
           [--message-blob-hex HEX] [--coinbase-text TEXT]
...
        --coinbase-text TEXT sets the coinbase scriptSig text written
        after the BIP34 height push (README "Coinbase structure"; max
        64 bytes, no merged mining on the DASH lane). Default
        "/P2Pool-DASH/c2pool/" (testnet "/P2Pool-tDASH/c2pool/") --
        the /P2Pool-DASH/ marker is what block explorers match on to
        attribute a block to this pool; the c2pool suffix says which
        implementation mined it. ...

Bound enforced: --coinbase-text with 65 bytes prints
[args] --coinbase-text too long: 65 bytes (max 64) and exits 2; 64 bytes is accepted.

Could not verify / deliberately out of scope

  • regtest gets the tDASH string, not an rDASH one. main_dash.cpp maps --regtest
    onto testnet = true — DASH has no third sharechain profile — so a separate
    /P2Pool-rDASH/ would need a three-way network selector across the whole lane. Noted in
    the SSOT comment. Also: the oracle's own dash_regtest.py:11 constant
    0F2F5032506F6F6C2D724441534828 is malformed — it declares push-15 but carries 14
    bytes, and ends 0x28 ( where / was clearly meant (/P2Pool-rDASH(). Not
    reproduced. (dash_custom_example.py:25 has the same class of bug: push-13 declared,
    10 bytes present.)
  • No live-explorer confirmation. The cryptoid extraction page currently renders
    "Coming soon... :)" for its block table, so I could confirm the pool is registered as
    P2Pool-DASH but not observe its matching rule directly. The argument for
    /P2Pool-DASH/ is that it is byte-identical to what canonical p2pool-dash nodes write.
  • Canonical markers for LTC/DGB/BCH are unverified — no local p2pool checkouts for
    those chains.
  • pool.yaml coinbase_text is not wired on DASHmain_dash.cpp has no YAML config
    loader at all (0 hits for cfg[/Fileconfig). CLI only. The README table row keeps the
    YAML key because it is accurate for LTC.
  • No soak. Nothing here was exercised against a live sharechain or a real dashd; the
    evidence is KATs plus the structural argument above.

Conflict surface

DRAFT — do not merge.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant