feat: dMint V1 mint support + Python reference miner

Enhancement Summary

Deepened on: 2026-05-07 Reviewers consulted: security-sentinel, kieran-python-reviewer, performance-oracle, code-simplicity-reviewer, learnings-researcher (fuzzing strategy + local-CI-parity)

Key Changes Applied

  1. API surface tightened. mine_solution is keyword-only past target, nonce_width: Literal[4, 8], raises MaxAttemptsExhausted instead of returning None. MineResult is frozen=True, slots=True. Dropped pow_hash (recomputable) and progress_cb (no consumer).

  2. Speculative surface deferred to M2. Cut nonce_provider parameter and JSON-shim subprocess protocol. Cut resume JSON and automatic stale-state retry from the demo script. M1 ships the minimum that delivers one accepted live mint.

  3. Performance estimate corrected. Replaced “50,000 h/s / 95 hours” with measured ≈1.75M h/s on modern CPU and labeled ESTIMATED. Real-mainnet mining is single-digit hours single-threaded, not days.

  4. Default max_attempts = 600M. Bounded by default so a naïve call doesn’t wedge for hours; callers can override.

Reviewer Findings Not Adopted (and why)

  • Security: golden-vector cross-check between pyrxd and glyph-miner. Strong recommendation, would protect against silent preimage drift. Not adopted in this pass — owner declined. Worth revisiting if a preimage drift bug ever ships.

  • Security: three-key broadcast handshake, fee-budget cap, 0600 resume file. Demo-script hardening. Not adopted — owner declined, and resume-file scope was cut entirely.

  • Learnings: hypothesis property test for V1 round-trip. Fuzzing strategy explicitly motivated by the V1 classifier gap; one ~30 LOC property test would close the same hole for the new V1 builder. Not adopted in this pass.

  • Kieran: error-hierarchy placement (DmintError(RxdSdkError) parent). Adopted in spirit — new errors inherit from DmintError per the surface-area list — but the implementation detail of where DmintError lives (security/errors.py vs glyph/dmint_errors.py) is a coding-time decision, not a plan-time one.

Post-Review Hardening Pass (2026-05-08)

After the initial M1 implementation landed, security-sentinel and red-team review caught two structural show-stoppers that synthetic round-trip tests had missed because the parser is in the same module as the builder. Both fixes ship in this hardening pass:

  1. Wrong mint-tx output shape. The first implementation produced 2 outputs (contract recreate + plain P2PKH reward) with the contract value decremented by reward + fee per mint. The mainnet V1 covenant trace (docs/dmint-research-mainnet.md §4) shows the actual shape:

    • 4 outputs: contract recreate, 75-byte P2PKH-wrapped FT reward carrying the tokenRef, optional OP_RETURN msg, miner change

    • 2 inputs: contract UTXO + separate plain-RXD funding input that pays reward + fee

    • Contract output value is preserved across mints (V1 is a singleton, not a value pool — the live RBG contracts carry exactly 1 photon)

    Fix: new build_dmint_v1_ft_output_script(miner_pkh, token_ref) that produces the 75-byte FT shape byte-equal to mainnet vout[1]; build_dmint_mint_tx gains funding_utxo: DmintMinerFundingUtxo keyword arg required on the V1 path; rewritten _build_dmint_v1_mint_tx produces the correct 3- or 4-output tx with optional op_return_msg.

  2. DeprecationWarning is too soft. Python filters DeprecationWarning by default outside __main__. A library user calling prepare_dmint_deploy from their own script saw nothing and got a deployable V2 result — the footgun was wide open.

    Fix: prepare_dmint_deploy now raises DmintError unless the caller passes allow_v2_deploy=True. SDK-internal V2 self-tests pass the flag; consumer code must opt in explicitly.

Other hardening from same review pass

  1. Token-burn defense (security-sentinel C1, red-team A). V1 mint refuses funding_utxo.script containing any OP_PUSHINPUTREF-family opcode (0xd0–0xd8). Spending an FT/dMint UTXO as fee silently destroys the token; the deny-list-by-opcode filter is the load-bearing defense. Raises InvalidFundingUtxoError.

  2. Golden-vector cross-check (security-sentinel C3). Added TestBuildDmintV1FtOutputScript::test_byte_equal_to_mainnet_vout1 which asserts byte-for-byte equality with the live mainnet 146a4d68…f3c vout[1] decoded in §4. This is the first test in pyrxd that compares output bytes against captured mainnet data (rather than round-tripping pyrxd’s own builder through pyrxd’s own parser).

  3. Sentinel placeholder preimage (security-sentinel H1). The placeholder preimage in unsigned mint txs is now 0xff * 64 instead of zeros. A user who broadcasts before the miner-loop patches in the real preimage gets fast network rejection rather than a covenant-fail silent bug.

  4. Validation tightening (red-team #3, #4, #12, #15):

    • fee_rate < 1 raises ValidationError

    • current_time != 0 on V1 path raises (V1 has no DAA)

    • V1 target range tightened to [1, MAX_SHA256D_TARGET] (top-bit-set values decode as negative under Bitcoin script signed-int semantics)

    • V1 state-script builder rejects height >= max_height (born-exhausted contracts)

  5. Subprocess shim hardening (security-sentinel C2):

    • stderr=subprocess.DEVNULL to bound parent memory if the miner misbehaves

    • UTF-8 decode errors wrapped as ValidationError rather than escaping uncaught

Reviewer Conflicts Resolved

  • Kieran (richer API) vs Simplicity (cut surface). Resolved by taking Simplicity’s cuts on speculative extension points (nonce_provider, shim, retry, resume JSON) and Kieran’s structure on what remains (Literal, frozen dataclass, exception over None).

  • pow_hash field on MineResult. Both Kieran and Simplicity argued for removal. Removed.

  • max_attempts default. Kieran said None is fine; Performance said default to a finite sentinel; Simplicity didn’t care. Took Performance’s recommendation (600M attempts ≈ minutes single-threaded).

Technical-Review Pass (2026-05-07)

After the deepen-plan synthesis, ran architecture-strategist and pattern-recognition-specialist reviewers. Findings applied:

  1. Funding-UTXO acceptance contradiction resolved. The original plan said build_dmint_mint_tx raises InvalidFundingUtxoError, but the function only takes the contract input — funding UTXOs are assembled by callers. Moved the check (and the error) to the examples/dmint_claim_demo.py funding-input scan. Library raises only ContractExhaustedError and PoolTooSmallError.

  2. Naming drift fixed. All new errors get the ...Error suffix matching the codebase’s universal convention (MaxAttemptsExhausted → MaxAttemptsError, etc.). MineResult → DmintMineResult matches the Domain...Result pattern.

  3. DmintError placement specified. Lives in src/pyrxd/security/errors.py (not glyph/dmint.py) per the established layering rule that all RxdSdkError subclasses live in security/errors.py.

  4. V2-deploy mitigation upgraded from docstring-only to docstring

    • runtime DeprecationWarning. Same scope, materially stronger — surfaces in CI logs, lets tests opt into warnings.simplefilter("error").

  5. slots=True dropped from DmintMineResult. Zero existing dataclasses in src/ use it; single-instance precedent isn’t worth the inconsistency.

  6. Implementation note added: mine_solution calls verify_sha256d_solution per candidate rather than inlining its own hash check. Single source of truth — drift between mining-check and verifier-check was the V1 classifier-gap failure mode.

  7. Runtime nonce_width guard added as acceptance criterion. Literal[4, 8] is type-checker-only; trust-boundary rule requires runtime validation regardless.

Findings deferred to coding-time (noted but not blocking):

  • V1 branch should early-return rather than fall through V2’s DAA update path — done in _build_dmint_v1_mint_tx

  • ~~find_dmint_contract_utxo library helper deferred to M2~~ — Pulled forward into M1 closeout (post-technical-review round 3, see “Architectural promotions” below). Two helpers now public: find_dmint_funding_utxo and build_dmint_v1_mint_preimage. find_dmint_contract_utxo itself still M2 (the demo accepts the contract outpoint via env var).

  • Naming choice build_dmint_v1_code_script vs build_dmint_code_script_v1 — both have precedent; pick at coding time

Architectural Promotions (post-technical-review round 3)

After the demo and supporting infrastructure landed, a third technical-review pass (kieran-python-reviewer + code-simplicity-reviewer

  • architecture-strategist) found one cluster of issues all three flagged: the demo was importing a _-prefixed library symbol, and key protocol logic (preimage construction, funding-UTXO scanning) lived in the demo rather than the library. Both signals indicated missing public API. Fixed by promoting:

  1. is_token_bearing_script(script: bytes) -> bool — was _funding_script_is_token_bearing (private). Used by build_dmint_mint_tx, find_dmint_funding_utxo, and any future “is this UTXO safe to spend as fee?” caller. Public name reflects that it’s a generic Glyph-protocol classifier, not a V1-mint-specific helper.

  2. find_dmint_funding_utxo(client, miner_address, needed) — library helper that scans a wallet for plain-RXD UTXOs covering a minimum value, excluding token-bearing UTXOs. Was a private helper in the demo. Promoted because (a) M2’s V1-deploy code will need it, (b) it implements the library invariant that the typed InvalidFundingUtxoError enforces, (c) the library is the right home for “scan a wallet” logic that touches ElectrumXClient.

  3. build_dmint_v1_mint_preimage(contract_utxo, funding_utxo, unsigned_tx) — library helper that computes the V1 mint PoW preimage. Was a private helper in the demo with 40 lines of comments explaining the V1 covenant binding. Promoted because the V1 covenant binding is protocol logic, not example glue: it documents which input/output the covenant hashes, which output binding (vout[2] OP_RETURN msg) the mainnet shape uses, and the SHA256d structure. Examples should glue typed primitives, not encode protocol.

  4. Fee estimation simplified. _build_dmint_v1_mint_tx previously hand-rolled ~30 lines of varint accounting; replaced with len(tx.serialize()) against a trial-assembled tx. The trial includes same-size placeholder scriptSigs on both inputs (sentinel 0xff*64 preimage on the contract input; 107 zero-bytes on the funding input matching the post-signing P2PKH size), so the measured size matches the final on-wire size. Eliminates drift between fee estimate and actual tx weight.

10 new public-API tests landed (TestIsTokenBearingScript, TestBuildDmintV1MintPreimage). Demo dropped ~80 lines (private helper functions removed) and now uses only the public library API. Full suite: 2629 passed, 10 skipped.

Overview

Make pyrxd capable of claiming tokens from existing mainnet V1 dMint contracts (e.g. RBG), including a slow but correct Python reference miner. After this milestone, a developer with a funded wallet can broadcast a real V1 mint tx against a live contract and have it accepted by the network.

Current state: pyrxd parses V1 contracts but build_dmint_mint_tx explicitly refuses them at dmint.py:1091, because spending V1 through the V2 builder produces an output the V1 covenant rejects. All seven live mainnet contracts are V1 per docs/dmint-research-mainnet.md §2.3, so the V2-only mint path is unusable on mainnet today.

Problem Statement

Three concrete gaps block live-network use:

  1. No V1 mint builder. build_dmint_mint_tx accepts only V2 state shapes. V1 needs a 4-byte nonce (vs V2’s 8), a 6-item state script (vs V2’s 10), and the V1 code epilogue (_V1_EPILOGUE_PREFIX + algo + _V1_EPILOGUE_SUFFIX at dmint.py:562).

  2. No nonce-grinding loop. verify_sha256d_solution exists but only verifies; the only “find a nonce” loop in the codebase is the test-internal one at tests/test_dmint_module.py:339. Library users have nothing to call.

  3. No live-network proof. Tests broadcast testmempoolaccept but never confirm a tx clears mempool and gets mined. A bug in the covenant-spend path stays invisible until someone tries it for real.

Proposed Solution

Three coordinated changes:

A. Make build_dmint_mint_tx V1-aware

Branch on state.is_v1 before the rejection at dmint.py:1091. The V1 branch:

  • Builds the 6-item V1 state script: height(4LE), contractRef, tokenRef, maxHeight, reward, target(8B fixed) — no DAA fields.

  • Reuses the V1 code epilogue verbatim from _V1_EPILOGUE_PREFIX + bytes([algo_byte]) + _V1_EPILOGUE_SUFFIX. Wrap as build_dmint_v1_code_script(algo).

  • Skips DAA target update (V1 is FIXED-only): new_target = state.target.

  • Calls a parameterized scriptSig builder with nonce_width=4.

B. Add mine_solution and parameterize the verifier

New library function in src/pyrxd/glyph/dmint.py:

@dataclass(frozen=True)
class DmintMineResult:
    nonce: bytes
    attempts: int
    elapsed_s: float

# Lives in src/pyrxd/security/errors.py per existing layering rule
# (every RxdSdkError subclass lives there). DmintError is a new parent.
class DmintError(RxdSdkError): ...
class MaxAttemptsError(DmintError):
    """Raised by mine_solution when max_attempts is reached without a solution."""
    attempts: int
    elapsed_s: float

def mine_solution(
    preimage: bytes,                              # 64 B from build_pow_preimage
    target: int,
    *,
    algo: DmintAlgo = DmintAlgo.SHA256D,
    nonce_width: Literal[4, 8] = 4,               # 4 = V1, 8 = V2
    max_attempts: int = 600_000_000,              # ≈ minutes single-core
) -> DmintMineResult:
    ...

Sequential nonce sweep starting at 0. Algos other than SHA256D raise NotImplementedError. MaxAttemptsError carries attempts and elapsed_s for telemetry. The nonce_width parameter is keyword-only and Literal[4, 8] for type-checker enforcement, plus a defensive runtime if nonce_width not in (4, 8): raise ValidationError(...) at the function entry — Literal is type-checker-only and pyrxd’s trust-boundary convention requires runtime validation regardless.

Implementation note: mine_solution calls verify_sha256d_solution once per candidate nonce rather than inlining its own hash-check. Single source of truth — drift between the mining check and the verifier check was the failure mode in docs/solutions/logic-errors/dmint-v1-classifier-gap.md. Performance is “slow but correct” anyway; one extra Python call per attempt is irrelevant compared to the hash itself.

Naming convention: Errors follow the codebase’s universal ...Error suffix convention (KeyMaterialError, CovenantError, etc.). All M1 new errors: MaxAttemptsError, InvalidFundingUtxoError, ContractExhaustedError, PoolTooSmallError. Result types follow the domain-prefixed Domain...Result pattern (DmintMintResult, FtTransferResult); the new DmintMineResult matches.

Deferred to M2: a nonce_provider parameter for external-miner plug-in and the JSON-over-stdin shim. Users wanting fast mining today run glyph-miner standalone, take the hex nonce, and pass it to a separate finalize call. No real-world caller needs the iterator hook yet — adding it before someone asks is YAGNI.

Generalize verify_sha256d_solution at dmint.py:466 to take a keyword-only nonce_width: Literal[4, 8] = 8 (preserves V2 default; new V1 callers pass nonce_width=4). Confirmed equivalence with the glyph-miner reference at glyph-miner src/miner.ts:494-508: target check is hash[0..4] == 0x00000000 AND be_u64(hash[4..12]) < target.

C. Synthetic-then-real acceptance proof

Two test surfaces:

  1. Synthetic V1 mint test in tests/test_dmint_v1_mint.py (new): a TestBuildDmintMintTxV1 class mirroring TestBuildDmintMintTx at test_dmint_end_to_end.py:554, using a low-difficulty target so brute-force in pure Python finds a nonce in seconds. Asserts: scriptSig is 72 bytes, parses back as is_v1=True, the resulting contract output chains correctly to a second mint. Optional RADIANT_INTEGRATION path pushes the tx hex to the existing VPS for testmempoolaccept.

  2. Manual examples/dmint_claim_demo.py: env-var-driven, modeled on examples/ft_deploy_premine.py. DRY_RUN=1 default. On broadcast failure: print the failure and exit (the developer re-runs by hand). No resume JSON — the manual one-off nature of the script doesn’t justify it, and a re-mine from a fresh contract-state query is correct anyway because the preimage is contractRef-bound and goes stale on chain advance. Polls confirmations after successful broadcast. Manual acceptance gate: at least one mint against a live RBG contract confirmed on-chain.

Technical Considerations

Architecture impacts

Surface area added to pyrxd.glyph.dmint:

  • mine_solution(preimage, target, *, algo, nonce_width, max_attempts) -> DmintMineResult (new public API; raises MaxAttemptsError)

  • DmintMineResult frozen dataclass (new public type)

  • build_dmint_v1_code_script(algo) (new public helper, sibling of build_dmint_code_script)

  • verify_sha256d_solution(preimage, nonce, target, *, nonce_width=8) (signature change — additive, default preserves V2 behavior)

Surface area added to pyrxd.security.errors:

  • DmintError(RxdSdkError) (new parent class for dMint-domain errors)

  • MaxAttemptsError(DmintError)

  • InvalidFundingUtxoError(DmintError) — raised by the example demo when assembling funding inputs (see “Funding-UTXO sanity check” below)

  • ContractExhaustedError(DmintError) — raised by build_dmint_mint_tx V1 branch

  • PoolTooSmallError(DmintError) — raised by build_dmint_mint_tx V1 branch

build_dmint_mint_tx keeps its signature; the V1 branch is internal and early-returns rather than falling through V2’s DAA-target update path.

Funding-UTXO sanity check

Spending a token-bearing UTXO as fee silently destroys the token. The funding-UTXO check must reject any input that carries an FT or dMint ref envelope.

Where the check lives: build_dmint_mint_tx only takes the contract input — funding UTXOs are assembled by callers (the example script, future wallet integrations). The check therefore lives in examples/dmint_claim_demo.py’s funding-UTXO selection loop, not in the library function. This matches the pattern at examples/ft_transfer_demo.py:163-167 where is_ft_script is the example-side filter.

The library raises InvalidFundingUtxoError for callers who pass an already-classified bad UTXO via a future expanded signature — but in M1 the function signature is unchanged, so the only caller that exercises the error path is the demo.

For each candidate funding UTXO, the demo:

  1. Fetches the source tx

  2. Classifies the locking script: is_ft_script(script.hex()) and DmintState.from_script(script) must both return falsy

  3. Skips with logged warning if either matches

  4. Raises InvalidFundingUtxoError if no clean funding UTXOs remain

Contract-exhaustion / pool-size validation

Before the miner loop, validate:

  • state.height < state.max_height (otherwise raise ContractExhausted)

  • contract_pool >= state.reward + min_fee + dust_floor (otherwise raise PoolTooSmall)

Both are deterministic from parsed state and ~430-byte tx size estimate. Failing fast saves the developer minutes of mining only to discover the contract can’t be claimed. tests/test_dmint_end_to_end.py:638 (test_pool_too_small_raises) already covers the V2 shape; add the V1 sibling case.

Stale-state race in flow C

Between query-contract-state and broadcast, another miner can claim height N first. The script’s broadcast then fails because the spent input is gone. examples/dmint_claim_demo.py handles this minimally: print the broadcast failure (with the rejection reason if available) and exit non-zero. The developer re-runs the script, which re-queries state and re-mines from scratch. Re-using a stale preimage would be wrong (contractRef-bound), so an automatic retry loop wouldn’t help even if it were worth the complexity.

External miner integration (M2)

mine_solution is intentionally minimal in M1 — sequential nonce sweep, no plug-in points. Users wanting fast mining today run glyph-miner standalone, take the resulting hex nonce, and pass it to the tx-finalize path manually. A typed nonce_provider parameter and JSON shim protocol are M2 work, to be added when a real external-miner caller exists.

Performance implications

ESTIMATED: pure-Python sha256d via hashlib runs at roughly 1–2 million hashes/sec on a modern CPU core (measured ≈1.75M h/s on i9-14900K by performance-oracle review; not yet measured on the project test machine). At RBG’s target 0x00da740da740da74 (~2^34 expected attempts), one mainnet claim is on the order of single-digit hours single-threaded, not days. The “slow but correct” framing still applies — anyone wanting to mine routinely uses glyph-miner. The acceptance test will record the actual measured rate on its host.

The max_attempts default (600M ≈ minutes single-threaded) prevents a naïve mine_solution() call from wedging for hours on real-mainnet difficulty without explicit opt-in.

Security considerations

  • Funding-UTXO check (above) is a security control, not just ergonomics — silently spending FT UTXOs as fee is a token-burn bug.

  • No private-key handling changes. The signing surface is the existing P2PKH path used by every other broadcaster. No new attack surface.

  • Mining is offline. No network calls inside mine_solution. The preimage-target shim protocol is local-only (subprocess stdin/stdout), not a network endpoint.

  • License attribution. glyph-miner is MIT (see its LICENSE). pyrxd is Apache 2.0. Compatible. If specific algorithm code is ported (e.g. midstate-precompute pattern), preserve the MIT header per file or add to NOTICE. Not a legal opinion.

Acceptance Criteria

Functional

  • [x] build_dmint_mint_tx accepts V1 contract states without raising

  • [x] V1 path produces a 72-byte scriptSig (4B nonce + 32B inputHash + 32B outputHash + OP_0)

  • [x] V1 mint tx parses back as is_v1=True via DmintState.from_script

  • [x] Two consecutive V1 mints chain correctly (contract output of mint 1 is the contract input of mint 2)

  • [x] mine_solution(preimage, target, nonce_width=4) returns a DmintMineResult whose nonce passes verify_sha256d_solution. Tested via hashlib.sha256 monkey-patch — same pattern as test_clamp_invariant_via_construction in the existing V2 module tests. Discovered during implementation: dMint has a hard 32-bit leading-zero floor (hash[0..4] == 0x00000000 is required regardless of target), so even the easiest possible dMint contract requires ~4B hash attempts to mine. End-to-end search in unit tests is impractical — would either skip or take ≈30 min single-core pure Python.

  • [x] mine_solution raises MaxAttemptsError (with attempts and elapsed_s attributes) when max_attempts is reached without a solution

  • [x] mine_solution raises ValidationError at runtime when nonce_width not in (4, 8) (Literal is type-checker-only)

  • [x] An optional slow brute-force test (skipped on no-find, mirrors existing test_brute_force_finds_valid shape) confirms search loop integration with real hashlib

  • [x] examples/dmint_claim_demo.py raises InvalidFundingUtxoError when funding-UTXO scan finds no plain-RXD candidates (FT/dMint UTXOs are filtered out via _funding_script_is_token_bearing)

  • [x] build_dmint_mint_tx raises ContractExhaustedError when height >= max_height

  • [x] build_dmint_mint_tx raises PoolTooSmallError when contract pool can’t cover reward + fee + dust

  • [x] NEW: mine_solution_external(preimage, target, miner_argv, nonce_width) delegates nonce search to a subprocess (e.g. glyph-miner), re-verifies the returned nonce locally, and raises ValidationError on miner-returned bad nonces. Added during implementation when user surfaced the GPU-mining use case as a real near-term need.

Test requirements

  • [x] New file tests/test_dmint_v1_mint.py with TestBuildDmintMintTxV1 class (49 tests covering V1 builders, V1 mint dispatch, mine_solution, mine_solution_external, deploy DeprecationWarning)

  • [x] All synthetic V1 mint tests pass under pytest -m unit — full suite 2592 passed, 10 skipped, 0 failed

  • [ ] Optional pytest -m integration path pushes V1 mint tx via SSH to VPS testmempoolaccept (gated by RADIANT_INTEGRATION env var, same pattern as test_dmint_deploy_integration.py:488) — deferred to Session C/D

  • [x] No regressions in V2 mint path — existing TestBuildDmintMintTx continues to pass; updated test_exhausted_contract_raises and test_pool_too_small_raises to match the new typed-error class names (the V2 path now raises ContractExhaustedError/PoolTooSmallError for parity with V1)

Manual acceptance (gate before declaring milestone shipped)

  • [ ] One mint against a live V1 contract on mainnet (RBG target — contract at maxHeight 628,328, currently ~14% mined per docs/dmint-research-mainnet.md §2.3) confirmed on-chain. Tx hash recorded in milestone close-out note.

Documentation

  • [x] mine_solution docstring includes a worked hex example (preimage in → nonce out → verifier passes)

  • [x] examples/dmint_claim_demo.py exists, env-var driven, DRY_RUN=1 default. Includes:

    • Funding-UTXO scan that excludes token-bearing UTXOs via the library’s opcode-stream walker

    • Three-key handshake on broadcast (DRY_RUN=0 requires I_UNDERSTAND_THIS_IS_REAL=yes)

    • Per-attempt support for an external miner via the EXTERNAL_MINER env var (delegates to glyph-miner via mine_solution_external)

    • OP_RETURN_MSG=NONE escape hatch for users who want to test without the Photonic msg marker

    • Stale-state recovery: print failure + reason on broadcast rejection, exit non-zero so the user re-runs (no automatic retry — mining a new preimage is required because the contractRef-bound preimage goes stale on chain advance)

  • [x] docs/dmint-followup.md gets an “out of date — see code” warning at the top (full rewrite lands in Milestone 2). Banner cites the authoritative current sources (dmint.py, builder.py, examples/dmint_claim_demo.py, the plan itself) and lists what’s still genuinely future work.

  • [x] prepare_dmint_deploy carries both a docstring warning AND a runtime DeprecationWarning for the V2-deploy footgun (see “Deploy-footgun mitigation in M1” below)

  • [x] Test confirms the DeprecationWarning fires on prepare_dmint_deploy calls

Success Metrics

  • Primary: one confirmed live V1 mint tx (binary outcome).

  • Secondary: synthetic V1 mint test stable on CI for 2+ weeks without flake.

  • Tertiary: at least one external user (or the developer themselves via glyph-miner) plugs in the external-miner shim and produces a valid mint, confirming the JSON protocol is usable.

Dependencies & Risks

Dependencies

  • A self-hosted Radiant full node for testmempoolaccept (existing — already used by deploy-integration tests).

  • the glyph-miner project for the optional fast-mining path. Not a hard dependency for shipping M1, but a cross-check during real-mint testing.

Risks

  • Stale dmint-research-mainnet.md open question on input/output hash construction — RESOLVED by glyph-miner reference: each is SHA256d(serialized_script) of the miner’s chosen funding-input script and OP_RETURN output script. Encoded in code now, not just the doc.

  • OP_RETURN “msg” output may or may not be covenant-required. Photonic convention pushes <6d7367 ("msg")> <message> as vout[2] in the example mint trace at docs/dmint-research-mainnet.md §4. The V1 covenant bytecode walk does not appear to enforce a specific OP_RETURN format, but this is unconfirmed. Mitigation: include the canonical “msg” OP_RETURN in our V1 mint to match what every observed mainnet mint does. If a future user wants to omit it, that’s a separate experiment.

  • testmempoolaccept doesn’t actually verify covenant satisfaction at mempool-acceptance time — it verifies the script signature evaluates to true, which is exactly the covenant. So this concern is largely moot, but worth noting: a positive testmempoolaccept is strong evidence, not proof. Only a confirmed-on-chain tx is proof.

  • Race in flow C — addressed by stale-state recovery in the demo script (above). Worst case: developer pays ~0.043 RXD in fees for a few attempts before the contract advances past them. Bounded.

  • ElectrumX has no get_outpoint(txid, vout) primitive. Workaround: fetch raw tx via get_transaction, parse outputs. Adequate for M1. Add a typed primitive in a separate cleanup PR if it becomes painful.

Out of Scope

Punted to Milestone 2 (V1 deploy):

  • V1 deploy builders (prepare_dmint_deploy V1 path)

  • Closing the deploy footgun (prepare_dmint_deploy currently always emits V2 with no opt-out — see “Deploy-footgun mitigation in M1” below for the minimal stop-gap M1 should ship)

  • Cross-tool verification (glyph-miner mines the pyrxd-deployed token)

  • Full rewrite of docs/dmint-followup.md

  • Example examples/dmint_deploy_demo.py

Deferred to M3 (indefinite — only if/when someone wants V2’s DAA features):

  • V2 deploy live-network proof

  • BLAKE3 and K12 algorithms

  • EPOCH and SCHEDULE DAA modes

Out of scope, period (always — that’s glyph-miner’s job):

  • Fast (C++/GPU) miner

Separate concern:

  • A typed get_outpoint(txid, vout) ElectrumX primitive (separate PR if needed)

Deploy-footgun mitigation in M1

Closing the V1 deploy gap is an M2 milestone, but M1 should not let the gap get worse. Two layered guards on prepare_dmint_deploy before M1 ships:

  1. Docstring warning at the top of prepare_dmint_deploy:

    ⚠️ This currently emits a V2 dMint contract. No live mainnet contracts are V2, no external miner (e.g. glyph-miner) targets V2, and indexer behavior on V2 deploys is empirically unknown. If you issue a token with this function today, nobody will be able to mine it without bespoke tooling. V1 deploy support is M2; this warning will be removed when version="v1" is the default.

  2. Runtime DeprecationWarning raised at the entry of prepare_dmint_deploy. Surfaces in CI logs and lets tests opt into warnings.simplefilter("error") to catch accidental V2 issuance:

    warnings.warn(
        "prepare_dmint_deploy currently emits V2 dMint contracts; "
        "no ecosystem miner targets V2. V1 deploy lands in M2. "
        "See docs/plans/2026-05-07-feat-dmint-v1-mint-and-reference-miner-plan.md",
        DeprecationWarning,
        stacklevel=2,
    )
    

The runtime warning is the load-bearing guard — docstrings get skipped, warnings show up in logs. ~3 lines. Both removed when M2 ships version: Literal["v1", "v2"] = "v1".

Acceptance: both guards in place. Test confirms the DeprecationWarning fires on prepare_dmint_deploy calls.

References & Research

Internal references

External references

  • glyph-miner (MIT) — authoritative V1 mining algorithm

    • src/pow.ts L11–18 — preimage construction

    • src/miner.ts L283–311 — midstate precompute

    • src/miner.ts L494–508 — target check (BE)

    • src/nonce.ts L7–13 — V1=4B/V2=8B widths

Files to be created

  • tests/test_dmint_v1_mint.py — synthetic V1 mint test class

  • examples/dmint_claim_demo.py — manual real-mint script

Files to be modified

  • src/pyrxd/glyph/dmint.py — V1 branch in build_dmint_mint_tx, mine_solution, parameterized verifier, V1 code-script helper

  • docs/dmint-followup.md — top-of-file “stale” warning