Skip to content

dash(tools): gen_mn_checkpoint.py — the missing daemonless MN-set anchor generator (#955) - #956

Open
frstrtr wants to merge 1 commit into
masterfrom
dash/955-gen-mn-checkpoint
Open

dash(tools): gen_mn_checkpoint.py — the missing daemonless MN-set anchor generator (#955)#956
frstrtr wants to merge 1 commit into
masterfrom
dash/955-gen-mn-checkpoint

Conversation

@frstrtr

@frstrtr frstrtr commented Jul 29, 2026

Copy link
Copy Markdown
Owner

What / why

#899 (E2d, merged 827d758d) ships a release-pinned masternode-set checkpoint that the daemonless embedded arm cold-starts from, and fails closed when it is unpinned/corrupt. The generator its README, its .inc headers, and its runtime ERROR all name — tools/dash/gen_mn_checkpoint.pydid not exist, and neither did tools/dash/. So the fail-closed state was permanent: no supported path could produce an anchor and --embedded-mainnet could never build a template (#955). This lands the remedy.

What it does

  • pin — fetch protx list valid true <height> from a dashd (RPC or an offline --protx-json capture), convert it to the checkpoint payload, and overwrite the target .inc in place; print a provenance block for the release notes. Brackets the fetch with getblockcount and refetches if the tip moved (auto-height), asserts getblockchaininfo.chain matches --network, converts payoutAddress → scriptPayout once, sorts by proTxHash for reproducible bytes, and re-parses what it wrote so it never ships a file the runtime would refuse.
  • verify — re-derive the digest and re-run every fail-closed rule mn_checkpoint.hpp enforces.

Stdlib only (urllib/hashlib/json/base58check) — a release box needs nothing but python3.

Byte-parity proof (this is why it's correct)

The field conversions mirror mn_seed.hpp::parse_protx_list_seed exactly (scriptPayout via base58check→P2PKH/P2SH, the dashd -1 "never" height clamps, Evo/Regular typing, operatorReward pct→fixed-point), so a checkpoint seed and an RPC seed produce byte-identical MNState — the coinbase-correctness contract.

tools/dash/test_gen_mn_checkpoint.py proves this offline, against the SAME committed testnet fixture (test/dash_mn_checkpoint_testnet_1519543.inc) that test_dash_mn_checkpoint.cpp::CheckpointSetIsFieldIdenticalToRpcSeed certifies equals the RPC seed:

  1. the fixture re-derives its own digest eddc3386…;
  2. pinning from the payee-relevant protx JSON subset reproduces the fixture's payee-critical columns byte-for-byte (6 MN × 13 columns);
  3. every defect class (digest tamper, count/network mismatch, unpinned) refuses the WHOLE checkpoint;
  4. round-trip: pin a synthetic set → verify accepts → one-byte edit → refused.
$ python3 tools/dash/test_gen_mn_checkpoint.py
...
ALL PASS

.gitignore

tools/ was wholly ignored (build artifacts). This un-ignores only the tracked generator source, so the path the checkpoints README instructs operators to run actually exists in the tree.

Not in this PR — the runtime acceptance

#955's acceptance bar ("pin an anchor, start --embedded-<net> with no coin-RPC, observe the node LEAVE the CHECKPOINT REFUSED state and build a template") is the live end-to-end I am driving next (fresh testnet anchor from a live node → build → run), and I'll attach that log as the merge-gating evidence. This PR is the generator + its byte-parity proof — the offline half — and unblocks pinning the mainnet anchor right now.

… anchor generator (#955)

E2d (#899) ships a release-pinned masternode-set checkpoint that the
daemonless embedded arm cold-starts from, and FAILS CLOSED when it is
unpinned/corrupt. But the generator its README, its .inc headers, and its
runtime ERROR all name — tools/dash/gen_mn_checkpoint.py — did not exist, and
neither did tools/dash/. So the fail-closed state was PERMANENT: no supported
path could produce an anchor, and --embedded-mainnet could never build a
template. This lands the remedy.

  pin    fetch `protx list valid true <height>` from a dashd (RPC or an
         offline --protx-json capture), convert it to the checkpoint payload,
         and OVERWRITE the target .inc in place; print a provenance block for
         the release notes. Brackets the fetch with getblockcount and refetches
         if the tip moved (auto-height), verifies getblockchaininfo.chain
         matches --network, converts payoutAddress -> scriptPayout once, sorts
         by proTxHash for reproducible bytes, and re-parses what it wrote so it
         never ships a file the runtime would refuse.
  verify re-derive the digest and re-run every fail-closed rule the parser
         enforces (mn_checkpoint.hpp).

Stdlib only (urllib/hashlib/json/base58check) — a release box needs nothing
but python3.

The field conversions MIRROR mn_seed.hpp::parse_protx_list_seed exactly
(scriptPayout via base58check->P2PKH/P2SH, the dashd -1 "never" height clamps,
Evo/Regular typing, operatorReward pct->fixed-point), so a checkpoint seed and
an RPC seed produce byte-identical MNState — the coinbase-correctness contract.

tools/dash/test_gen_mn_checkpoint.py proves this OFFLINE against the SAME
committed testnet fixture (test/dash_mn_checkpoint_testnet_1519543.inc) that
CheckpointSetIsFieldIdenticalToRpcSeed certifies equals the RPC seed:
  - the fixture re-derives its own digest eddc3386…;
  - pinning from the payee-relevant protx JSON subset reproduces the fixtures


def main():
fixture_text = open(FIXTURE).read()
g.main(["pin", "--network", "testnet", "--protx-json", jsrc,
"--height", str(FIXTURE_HEIGHT), "--blockhash", FIXTURE_BLOCKHASH,
"--generated", "2026-07-26T13:23:51Z", "--source", "kat", "--output", out, "--quiet"])
R, G = mn_map(g.unwrap_inc(fixture_text)), mn_map(g.unwrap_inc(open(out).read()))
so = tf.name
g.main(["pin", "--network", "testnet", "--protx-json", sj, "--height", "12345",
"--blockhash", "ab" * 32, "--generated", "2026-01-01T00:00:00Z", "--output", so, "--quiet"])
st = open(so).read()
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.

2 participants