Skip to content

Commit 753832b

Browse files
committed
dash(embedded): E2d daemonless MN-set seed — pinned checkpoint + forward replay (#738)
c2pool could not mine DASH without an external dashd because it could not obtain the payout-bearing masternode set on its own. The P2P Simplified MN List omits scriptPayout and nLastPaidHeight, and neither is committed in merkleRootMNList, so no payout-bearing set can be header-authenticated. The only cold-start source was dashd RPC `protx list valid true` (E2c); the no-RPC arm printed "seed UNAVAILABLE" and every template kept routing to the dashd fallback. That was the last structurally daemon-dependent input on the daemonless path. This lands the settled design: a release-pinned masternode-set checkpoint compiled into the binary, replayed forward to the tip through the existing block-connect ingest. The fully trustless DIP-3-height replay (~1.5M block bodies) stays available as a later opt-in verify-mode and is NOT built here. THE COST IS A TRUST ANCHOR, AND IT IS DOCUMENTED WHERE USERS WILL SEE IT. A node cold-starting from the checkpoint trusts the release build for the set contents at the anchor height. That is stated in README ("DASH daemonless masternode-set checkpoint — trust anchor"), in KNOWN_ISSUES, in CHANGELOG, in src/impl/dash/coin/checkpoints/README.md, and at runtime on every load — not only in a source comment. What the node still verifies for itself, with no trust: * chain position — the anchor's blockhash must match our own X11-PoW + DGW validated header chain at the anchor height; * integrity — a SHA-256 digest over every non-comment line (an integrity check on the file, NOT a signature; it catches accident, not malice); * forward consistency — every replayed block re-derives the projected payee and compares it with that block's real coinbase, so a wrong anchor is falsified within a few blocks. FAIL CLOSED, LOUDLY. Missing, unpinned, corrupt, wrong-network, wrong chain position, staler than --embedded-mn-bridge-max (default 20000 blocks, ~34 days), a replay gap, or a replay payee-desync: log at ERROR, refuse to serve embedded templates, keep routing to the dashd fallback. A wrong payee is a coinbase the network rejects — a lost block. Refusing is always cheaper. Design note — why the bridge runs on a PRIVATE MnStateMachine rather than publishing the anchor and letting the maintainer fold forward: apply_block is forward-contiguous, and during a cold start the live feed is simultaneously delivering blocks at the CURRENT tip. Publishing at the anchor height would arm MN-readiness against a queue thousands of blocks stale, then trip gap_detected on the first live block — a served bad-cb-payee window followed by a latched-dead arm. The lane instead folds only the exact next height, ignores live tip blocks mid-bridge, and publishes ONCE, at the tip, through the same leg-4 event the RPC seed uses. Also closes an anti-mint hole that only becomes reachable now that the arm can run without an RPC: with no seed the payee set starts empty and the apply cursor is 0, so the first block carrying a ProRegTx would register one masternode, flip MN-readiness, and serve a template whose entire payment queue was that single accidental registration. CoinStateMaintainer now supports an opt-in latch requiring an authoritative height-stamped snapshot; main_dash enables it for the embedded arm. Default off, so no existing construction site changes behaviour. Re-pinning is a scripted release step, not manual surgery: tools/dash/gen_mn_checkpoint.py pin (height-stable protx fetch bracketed by getblockcount, chain cross-check, deterministic ordering, prints the provenance block for the release notes) and ... verify PATH for the release checklist. The shipped mainnet and testnet anchors are UNPINNED in this change — pinning needs RPC access to a synced dashd, which is a release-time step. Until pinned, daemonless DASH fails closed, which is the intended safe default. Lock trace: the lane holds no lock and is io-thread confined. Its pump() calls self-locking HeaderChain reads and is driven from HeaderChain's on_tip_changed callback, which is dispatched with m_mutex RELEASED (add_header/add_headers copy the pending tip change out inside the lock scope, close it, then invoke) — so the callee is reachable, not dead code. The pre-existing tip_advance_from_chain(*hc, ...) call in the same lambda already depends on that property and is proven live. Tests fold into the EXISTING allowlisted test_dash_node_reception_wire target (a new add_executable would silently report "Not Run"). No #ifdef guards in the new TU, so every registered case actually executes.
1 parent 1e66d7f commit 753832b

13 files changed

Lines changed: 2353 additions & 16 deletions

CHANGELOG.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,35 @@
11
# Changelog
22

3+
## [Unreleased]
4+
5+
### DASH — daemonless masternode-set seed (E2d)
6+
- **Release-pinned masternode-set checkpoint + forward replay**`--embedded-mainnet`
7+
can now cold-start the payout-bearing masternode set without a dashd RPC. This was
8+
the last structurally daemon-dependent input on the DASH daemonless path.
9+
-**This introduces a documented TRUST ANCHOR.** The pinned masternode set is data
10+
the release build asserts; it cannot be header-authenticated, because the P2P
11+
Simplified MN List omits `scriptPayout`/`nLastPaidHeight` and neither is committed
12+
in `merkleRootMNList`. The node independently verifies the anchor's **chain
13+
position** against its own PoW-validated header chain, its **SHA-256 integrity
14+
digest**, and **every replayed block's coinbase** against the projected payee — but
15+
the set contents at the anchor height are trusted. Fully trustless DIP-3-height
16+
replay remains planned as a later opt-in verify-mode. See the README section
17+
"DASH daemonless masternode-set checkpoint — trust anchor" and
18+
`src/impl/dash/coin/checkpoints/README.md`.
19+
- **Fails closed, loudly.** A missing, unpinned, corrupt, wrong-network,
20+
wrong-chain-position, stale or contradicted anchor refuses to serve embedded
21+
templates rather than guessing a payee. New `--embedded-mn-bridge-max N` bounds how
22+
far back an anchor may be (default 20000 blocks, ≈34 days).
23+
- **Anchors are pinned at release time**, not by hand:
24+
`tools/dash/gen_mn_checkpoint.py pin --network mainnet --rpc-url ... ` rewrites the
25+
checkpoint and prints the provenance block for the release notes;
26+
`... verify PATH` re-validates one already in the tree.
27+
- **Anti-mint latch** — on the embedded arm, masternode-readiness now requires an
28+
authoritative height-stamped snapshot. Previously a cold daemonless start could arm
29+
itself off a single ProRegTx observed in a live block and serve a template whose
30+
entire payment queue was that one accidental registration.
31+
32+
333
## [0.14.0-v36] - 2026-07-10
434

535
### Licensing

KNOWN_ISSUES.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,44 @@ documented in the [postmortem archive](https://github.com/frstrtr/the/tree/maste
55

66
---
77

8+
## DASH daemonless masternode set is seeded from a TRUST ANCHOR
9+
10+
**Severity**: Medium (security-model disclosure, not a defect)
11+
**Affects**: DASH `--embedded-mainnet` run WITHOUT a dashd RPC
12+
13+
c2pool cannot derive the payout-bearing DASH masternode set from the P2P
14+
network: the Simplified MN List omits `scriptPayout` and `nLastPaidHeight`,
15+
and neither is committed in `merkleRootMNList`. Daemonless DASH therefore
16+
cold-starts from a **release-pinned masternode set compiled into the binary**
17+
and replays blocks forward from it.
18+
19+
**You are trusting the release build** for the set contents at the anchor
20+
height. The node does independently verify the anchor's chain position against
21+
its own PoW-validated header chain, its SHA-256 integrity digest, and every
22+
replayed block's coinbase against the projected payee — a wrong anchor is
23+
falsified within a few blocks and fails closed — but nothing available to the
24+
node can prove the set itself.
25+
26+
A fully trustless DIP-3-height replay (~1.5M block bodies) is planned as a
27+
later **opt-in verify-mode**; it is not implemented.
28+
29+
Two further limitations today:
30+
31+
- **The shipped mainnet and testnet anchors are UNPINNED.** Pinning requires
32+
RPC access to a synced dashd and is a release-time step
33+
(`tools/dash/gen_mn_checkpoint.py pin`). Until an anchor is pinned,
34+
daemonless DASH **fails closed** — it logs at `ERROR` and refuses to serve
35+
embedded templates rather than guessing a payee.
36+
- **A stale anchor is refused, not worked around.** An anchor more than
37+
`--embedded-mn-bridge-max` blocks behind the tip (default 20000, ≈34 days)
38+
is rejected. A release cut long ago will not daemonlessly mine DASH without
39+
either a fresher build or `--coin-rpc-*`.
40+
41+
Background: README section "DASH daemonless masternode-set checkpoint — trust
42+
anchor" and `src/impl/dash/coin/checkpoints/README.md`.
43+
44+
---
45+
846
## Remaining IO-thread tracker callbacks without shared_lock
947

1048
**Severity**: Medium

README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,56 @@ c2pool has four operating modes. The default is a full P2P pool — no flags req
154154

155155
Legacy `--standalone` mode (minimal stratum + RPC daemon, no embedded SPV) is available for backwards compatibility.
156156

157+
### DASH daemonless masternode-set checkpoint — trust anchor
158+
159+
**If you run DASH with `--embedded-mainnet` and no dashd, you are trusting the
160+
c2pool release build for one specific piece of data. This section says exactly
161+
which, and why.**
162+
163+
To build a DASH block, c2pool must know which masternode is next in the DIP-3
164+
payment queue. Paying the wrong one produces a coinbase the network rejects
165+
(`bad-cb-payee`) — a mined block thrown away. Ranking the queue needs each
166+
masternode's `scriptPayout` and `nLastPaidHeight`, and **neither is available
167+
from the DASH P2P network**: the Simplified MN List (`mnlistdiff`) omits both,
168+
and neither is committed in `merkleRootMNList`, so there is no header
169+
commitment to check them against.
170+
171+
So c2pool ships a **release-pinned masternode set** — a trust anchor of the
172+
same class as Bitcoin Core's `assumeutxo` — and replays blocks forward from it
173+
to the current tip.
174+
175+
**What the node verifies for itself, with no trust:**
176+
177+
- **chain position** — the anchor names a block hash, and is rejected unless
178+
c2pool's own X11-PoW + DGW-validated header chain holds exactly that hash at
179+
exactly that height;
180+
- **integrity** — a SHA-256 digest over the anchor's contents (an integrity
181+
check on the file, *not* a signature: whoever can change the source can
182+
recompute the digest — it catches accidents, not malice);
183+
- **forward consistency** — every block replayed from the anchor re-derives
184+
the projected payee and compares it against that block's real coinbase, so a
185+
wrong anchor is falsified within a few blocks.
186+
187+
**What you are trusting:** the membership and payout state of the masternode
188+
set *at the anchor height*. Nothing available to the node can prove it.
189+
190+
A fully trustless alternative exists — replaying every block from DIP-3
191+
activation (~1.5M blocks) — and is **planned as a later opt-in verify-mode**.
192+
It is not implemented today.
193+
194+
**Fail-closed by design.** If the anchor is missing, corrupt, for the wrong
195+
network, in the wrong chain position, further behind the tip than
196+
`--embedded-mn-bridge-max` (default 20000 blocks, ≈34 days), or contradicted
197+
by a replayed block, c2pool logs the refusal at `ERROR` and **refuses to serve
198+
embedded DASH templates**. It falls back to a configured dashd, or serves
199+
nothing. It never guesses a masternode payee.
200+
201+
Running DASH with a dashd RPC configured does **not** use the anchor at all —
202+
`protx list valid true` is authoritative and is used instead.
203+
204+
Details, provenance of the shipped anchor, and the release-time re-pinning
205+
procedure: [`src/impl/dash/coin/checkpoints/README.md`](src/impl/dash/coin/checkpoints/README.md).
206+
157207
### Startup examples
158208

159209
```bash

0 commit comments

Comments
 (0)