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¶
API surface tightened.
mine_solutionis keyword-only pasttarget,nonce_width: Literal[4, 8], raisesMaxAttemptsExhaustedinstead of returningNone.MineResultisfrozen=True, slots=True. Droppedpow_hash(recomputable) andprogress_cb(no consumer).Speculative surface deferred to M2. Cut
nonce_providerparameter 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.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.
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 fromDmintErrorper the surface-area list — but the implementation detail of whereDmintErrorlives (security/errors.pyvsglyph/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:
Wrong mint-tx output shape. The first implementation produced 2 outputs (contract recreate + plain P2PKH reward) with the contract value decremented by
reward + feeper 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_txgainsfunding_utxo: DmintMinerFundingUtxokeyword arg required on the V1 path; rewritten_build_dmint_v1_mint_txproduces the correct 3- or 4-output tx with optionalop_return_msg.DeprecationWarningis too soft. Python filtersDeprecationWarningby default outside__main__. A library user callingprepare_dmint_deployfrom their own script saw nothing and got a deployable V2 result — the footgun was wide open.Fix:
prepare_dmint_deploynow raisesDmintErrorunless the caller passesallow_v2_deploy=True. SDK-internal V2 self-tests pass the flag; consumer code must opt in explicitly.
Other hardening from same review pass¶
Token-burn defense (security-sentinel C1, red-team A). V1 mint refuses
funding_utxo.scriptcontaining 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. RaisesInvalidFundingUtxoError.Golden-vector cross-check (security-sentinel C3). Added
TestBuildDmintV1FtOutputScript::test_byte_equal_to_mainnet_vout1which asserts byte-for-byte equality with the live mainnet146a4d68…f3cvout[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).Sentinel placeholder preimage (security-sentinel H1). The placeholder preimage in unsigned mint txs is now
0xff * 64instead 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.Validation tightening (red-team #3, #4, #12, #15):
fee_rate < 1raisesValidationErrorcurrent_time != 0on 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)
Subprocess shim hardening (security-sentinel C2):
stderr=subprocess.DEVNULLto bound parent memory if the miner misbehavesUTF-8 decode errors wrapped as
ValidationErrorrather 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 overNone).pow_hashfield onMineResult. Both Kieran and Simplicity argued for removal. Removed.max_attemptsdefault. Kieran saidNoneis 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:
Funding-UTXO acceptance contradiction resolved. The original plan said
build_dmint_mint_txraisesInvalidFundingUtxoError, but the function only takes the contract input — funding UTXOs are assembled by callers. Moved the check (and the error) to theexamples/dmint_claim_demo.pyfunding-input scan. Library raises onlyContractExhaustedErrorandPoolTooSmallError.Naming drift fixed. All new errors get the
...Errorsuffix matching the codebase’s universal convention (MaxAttemptsExhausted→MaxAttemptsError, etc.).MineResult→DmintMineResultmatches theDomain...Resultpattern.DmintErrorplacement specified. Lives insrc/pyrxd/security/errors.py(notglyph/dmint.py) per the established layering rule that allRxdSdkErrorsubclasses live insecurity/errors.py.V2-deploy mitigation upgraded from docstring-only to docstring
runtime
DeprecationWarning. Same scope, materially stronger — surfaces in CI logs, lets tests opt intowarnings.simplefilter("error").
slots=Truedropped fromDmintMineResult. Zero existing dataclasses insrc/use it; single-instance precedent isn’t worth the inconsistency.Implementation note added:
mine_solutioncallsverify_sha256d_solutionper 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.Runtime
nonce_widthguard 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_utxolibrary 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_utxoandbuild_dmint_v1_mint_preimage.find_dmint_contract_utxoitself still M2 (the demo accepts the contract outpoint via env var).Naming choice
build_dmint_v1_code_scriptvsbuild_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:
is_token_bearing_script(script: bytes) -> bool— was_funding_script_is_token_bearing(private). Used bybuild_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.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 typedInvalidFundingUtxoErrorenforces, (c) the library is the right home for “scan a wallet” logic that touchesElectrumXClient.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.Fee estimation simplified.
_build_dmint_v1_mint_txpreviously hand-rolled ~30 lines of varint accounting; replaced withlen(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:
No V1 mint builder.
build_dmint_mint_txaccepts 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_SUFFIXat dmint.py:562).No nonce-grinding loop.
verify_sha256d_solutionexists 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.No live-network proof. Tests broadcast
testmempoolacceptbut 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 asbuild_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:
Synthetic V1 mint test in
tests/test_dmint_v1_mint.py(new): aTestBuildDmintMintTxV1class mirroringTestBuildDmintMintTxat 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 asis_v1=True, the resulting contract output chains correctly to a second mint. OptionalRADIANT_INTEGRATIONpath pushes the tx hex to the existing VPS fortestmempoolaccept.Manual
examples/dmint_claim_demo.py: env-var-driven, modeled onexamples/ft_deploy_premine.py.DRY_RUN=1default. 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; raisesMaxAttemptsError)DmintMineResultfrozen dataclass (new public type)build_dmint_v1_code_script(algo)(new public helper, sibling ofbuild_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 bybuild_dmint_mint_txV1 branchPoolTooSmallError(DmintError)— raised bybuild_dmint_mint_txV1 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:
Fetches the source tx
Classifies the locking script:
is_ft_script(script.hex())andDmintState.from_script(script)must both return falsySkips with logged warning if either matches
Raises
InvalidFundingUtxoErrorif no clean funding UTXOs remain
Contract-exhaustion / pool-size validation¶
Before the miner loop, validate:
state.height < state.max_height(otherwise raiseContractExhausted)contract_pool >= state.reward + min_fee + dust_floor(otherwise raisePoolTooSmall)
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_txaccepts 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=TrueviaDmintState.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 aDmintMineResultwhose nonce passesverify_sha256d_solution. Tested viahashlib.sha256monkey-patch — same pattern astest_clamp_invariant_via_constructionin the existing V2 module tests. Discovered during implementation: dMint has a hard 32-bit leading-zero floor (hash[0..4] == 0x00000000is required regardless oftarget), 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_solutionraisesMaxAttemptsError(withattemptsandelapsed_sattributes) whenmax_attemptsis reached without a solution[x]
mine_solutionraisesValidationErrorat runtime whennonce_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_validshape) confirms search loop integration with realhashlib[x]
examples/dmint_claim_demo.pyraisesInvalidFundingUtxoErrorwhen funding-UTXO scan finds no plain-RXD candidates (FT/dMint UTXOs are filtered out via_funding_script_is_token_bearing)[x]
build_dmint_mint_txraisesContractExhaustedErrorwhenheight >= max_height[x]
build_dmint_mint_txraisesPoolTooSmallErrorwhen 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 raisesValidationErroron 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.pywithTestBuildDmintMintTxV1class (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 integrationpath pushes V1 mint tx via SSH to VPStestmempoolaccept(gated byRADIANT_INTEGRATIONenv var, same pattern as test_dmint_deploy_integration.py:488) — deferred to Session C/D[x] No regressions in V2 mint path — existing
TestBuildDmintMintTxcontinues to pass; updatedtest_exhausted_contract_raisesandtest_pool_too_small_raisesto match the new typed-error class names (the V2 path now raisesContractExhaustedError/PoolTooSmallErrorfor 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_solutiondocstring includes a worked hex example (preimage in → nonce out → verifier passes)[x]
examples/dmint_claim_demo.pyexists, env-var driven,DRY_RUN=1default. Includes:Funding-UTXO scan that excludes token-bearing UTXOs via the library’s opcode-stream walker
Three-key handshake on broadcast (
DRY_RUN=0requiresI_UNDERSTAND_THIS_IS_REAL=yes)Per-attempt support for an external miner via the
EXTERNAL_MINERenv var (delegates to glyph-miner viamine_solution_external)OP_RETURN_MSG=NONEescape hatch for users who want to test without the Photonic msg markerStale-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.mdgets 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_deploycarries both a docstring warning AND a runtimeDeprecationWarningfor the V2-deploy footgun (see “Deploy-footgun mitigation in M1” below)[x] Test confirms the
DeprecationWarningfires onprepare_dmint_deploycalls
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-minerproject 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.mdopen question on input/output hash construction — RESOLVED by glyph-miner reference: each isSHA256d(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.testmempoolacceptdoesn’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 positivetestmempoolacceptis 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 viaget_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_deployV1 path)Closing the deploy footgun (
prepare_dmint_deploycurrently 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.mdExample
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:
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.Runtime
DeprecationWarningraised at the entry ofprepare_dmint_deploy. Surfaces in CI logs and lets tests opt intowarnings.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¶
docs/brainstorms/2026-05-07-dmint-integration-brainstorm.md— feature scope decisionssrc/pyrxd/glyph/dmint.py(1268 L) — V1 fingerprint at L562,_from_v1_scriptat L781,build_pow_preimageat L326,build_mint_scriptsigat L365,verify_sha256d_solutionat L466,build_dmint_mint_txat L1019, V1 reject at L1091src/pyrxd/glyph/builder.py:291—prepare_dmint_deploy(already ships)tests/test_dmint_end_to_end.py:554—TestBuildDmintMintTx(V2 template to mirror for V1)tests/test_dmint_module.py:339— low-difficulty mining templatetests/test_dmint_deploy_integration.py:488— VPS testmempoolaccept patternexamples/ft_deploy_premine.py— env-var/DRY_RUN/resume pattern for the demo scriptexamples/ft_transfer_demo.py—is_ft_scriptprecondition patterndocs/dmint-research-mainnet.md— live V1 contract decode + mint tracedocs/dmint-research-photonic.md— Photonic Wallet TS referencedocs/solutions/logic-errors/dmint-v1-classifier-gap.md— prior incident: V1 classifier gap exposed by RBG live test (drives the synthetic-then-real testing approach)
External references¶
glyph-miner(MIT) — authoritative V1 mining algorithmsrc/pow.tsL11–18 — preimage constructionsrc/miner.tsL283–311 — midstate precomputesrc/miner.tsL494–508 — target check (BE)src/nonce.tsL7–13 — V1=4B/V2=8B widths
Files to be created¶
tests/test_dmint_v1_mint.py— synthetic V1 mint test classexamples/dmint_claim_demo.py— manual real-mint script
Files to be modified¶
src/pyrxd/glyph/dmint.py— V1 branch inbuild_dmint_mint_tx,mine_solution, parameterized verifier, V1 code-script helperdocs/dmint-followup.md— top-of-file “stale” warning