pyrxd.glyph.dmint — dMint permissionless PoW issuance

dMint protocol implementation — subpackage split from the original dmint.py per the plan at docs/plans/2026-05-18-refactor-dmint-py-subpackage-split-plan.md.

The subpackage layers as types builders chain miner (one-way dependency). The __init__.py uses PEP 562 lazy __getattr__ to re-export every public symbol — plus 10 underscore-private symbols that existing tests + pyrxd.glyph.builder import directly — at their original pyrxd.glyph.dmint path. This matches the convention used by pyrxd/__init__.py, pyrxd/glyph/__init__.py, and pyrxd/script/__init__.py.

A handful of symbols were relocated during execution to keep the dependency graph one-way:

  • _OP_STATESEPARATOR lives in types (not chain) because builders needs it and builders chain would be a cycle.

  • _V1_EPILOGUE_PREFIX/_ALGO_OFFSET/_SUFFIX/_LEN live in builders (not chain) for the same reason; chain re-exports them under their original names.

Mock targeting note: mock.patch patches the namespace where the test names the target, not where the calling code resolves it via PEP 562 __getattr__. Test sites that previously used patch("pyrxd.glyph.dmint.hashlib") and similar were updated to target the submodule where the imported name lives (typically pyrxd.glyph.dmint.miner.hashlib).

class pyrxd.glyph.dmint.DaaMode[source]

Bases: IntEnum

__new__(value)
FIXED = 0
EPOCH = 1
ASERT = 2
LWMA = 3
SCHEDULE = 4
class pyrxd.glyph.dmint.DmintAlgo[source]

Bases: IntEnum

__new__(value)
SHA256D = 0
BLAKE3 = 1
K12 = 2
class pyrxd.glyph.dmint.DmintCborPayload[source]

Bases: object

The dmint object embedded in Glyph V2 token metadata CBOR.

Indexers read this to discover dMint contracts and display mining parameters in wallets/explorers without parsing the contract script.

Field names mirror Photonic Wallet DmintPayload type in types.ts.

__init__(algo, num_contracts, max_height, reward, premine, diff, daa_mode=DaaMode.FIXED, target_block_time=60, half_life=0, window_size=0)
Parameters:
Return type:

None

daa_mode: DaaMode = 0
classmethod from_cbor_dict(d)[source]

Parse the dmint CBOR value from an on-chain payload.

Parameters:

d (dict)

Return type:

DmintCborPayload

half_life: int = 0
target_block_time: int = 60
to_cbor_dict()[source]

Encode to the dict that becomes the dmint CBOR value.

Return type:

dict

window_size: int = 0
algo: DmintAlgo
num_contracts: int
max_height: int
reward: int
premine: int
diff: int
class pyrxd.glyph.dmint.DmintContractUtxo[source]

Bases: object

Describes a live dMint contract UTXO to be spent in a mint transaction.

Parameters:
  • txid – txid of the UTXO (hex, not reversed)

  • vout – output index

  • value – photon value locked in the UTXO. For V1 contracts this is the singleton carrier (1 photon on the live RBG-class deploys). For V2 it is the running reward pool that decrements per mint.

  • script – full output script bytes (state + OP_STATESEPARATOR + code)

  • state – parsed DmintState — caller can obtain via DmintState.from_script(script)

__init__(txid, vout, value, script, state)
Parameters:
Return type:

None

txid: str
vout: int
value: int
script: bytes
state: DmintState
class pyrxd.glyph.dmint.DmintDeployParams[source]

Bases: object

Parameters for deploying a V2 dMint contract.

__init__(contract_ref, token_ref, max_height, reward, difficulty, algo=DmintAlgo.SHA256D, daa_mode=DaaMode.FIXED, target_time=60, half_life=3600, height=0, last_time=0, epoch_length=2016, max_adjustment_log2=2, schedule=())
Parameters:
Return type:

None

algo: DmintAlgo = 0
daa_mode: DaaMode = 0
epoch_length: int = 2016
half_life: int = 3600
height: int = 0
property initial_target: int

Compute initial target from difficulty using the SHA256d formula.

last_time: int = 0
max_adjustment_log2: int = 2
schedule: tuple[tuple[int, int], ...] = ()
target_time: int = 60
contract_ref: GlyphRef
token_ref: GlyphRef
max_height: int
reward: int
difficulty: int
class pyrxd.glyph.dmint.DmintMineResult[source]

Bases: object

The output of a successful mine_solution() call.

Parameters:
  • nonce – The nonce bytes (4B for V1, 8B for V2) that satisfy the target.

  • attempts – Number of nonce candidates tried before finding the solution.

  • elapsed_s – Wall-clock seconds spent searching.

__init__(nonce, attempts, elapsed_s)
Parameters:
Return type:

None

nonce: bytes
attempts: int
elapsed_s: float
class pyrxd.glyph.dmint.DmintMinerFundingUtxo[source]

Bases: object

A plain RXD UTXO supplied by the miner to fund a V1 mint.

The V1 covenant takes its FT output value (reward photons) and the miner’s tx fee from a separate plain-RXD input — the contract output is a singleton and never funds the mint. This dataclass describes that funding input.

The locking script must be a plain script with NO Glyph/FT/dMint ref pushes (OP_PUSHINPUTREF*, opcodes 0xd0–0xd8). Spending a token-bearing UTXO as fee silently destroys the token; the V1 mint builder validates this and raises InvalidFundingUtxoError if the funding script carries any ref envelope.

Parameters:
  • txid – txid of the UTXO (hex, not reversed)

  • vout – output index

  • value – photons locked in the UTXO

  • script – full locking script bytes (typically 25-byte P2PKH)

__init__(txid, vout, value, script)
Parameters:
Return type:

None

txid: str
vout: int
value: int
script: bytes
class pyrxd.glyph.dmint.DmintMintResult[source]

Bases: object

Output of build_dmint_mint_tx().

Parameters:
  • tx – Unsigned transaction (caller must sign).

  • updated_state – New DmintState written into the contract output (height incremented, target updated if DAA is active).

  • contract_script – New contract output script (state + separator + code).

  • reward_script – P2PKH locking script of the miner reward output.

  • fee – Transaction fee in photons.

Note

The transaction returned here is unsigned — it uses raw script bytes for the contract input’s unlocking script (nonce + preimage halves) built by build_mint_scriptsig(). The contract script is a covenant, not a P2PKH, so standard Transaction.sign() is not appropriate. The caller must either set the unlocking script directly or use a custom signing path. See docstring of build_dmint_mint_tx() for details.

__init__(tx, updated_state, contract_script, reward_script, fee)
Parameters:
Return type:

None

tx: Any
updated_state: Any
contract_script: bytes
reward_script: bytes
fee: int
class pyrxd.glyph.dmint.DmintState[source]

Bases: object

Parsed dMint contract state (from on-chain UTXO script).

Supports both V1 (the current Radiant mainnet format) and V2 (Photonic Wallet’s HEAD spec, not yet seen on mainnet). V1 has 6 state items; V2 has 10. is_v1 is True iff this state was parsed from V1 layout — in which case target_time and last_time are not meaningful on-chain values and are set to 0; daa_mode is always FIXED for V1 (the V1 contract template has no DAA bytecode).

__init__(height, contract_ref, token_ref, max_height, reward, algo, daa_mode, target_time, last_time, target, is_v1=False)
Parameters:
Return type:

None

classmethod from_script(script_bytes)[source]

Parse a dMint contract UTXO script into a DmintState.

Tries V2 layout first (10 state items), falls back to V1 (6 items + fingerprinted code epilogue). Raises ValidationError if the script matches neither.

Parameters:

script_bytes (bytes) – Raw script bytes from a dMint contract UTXO output.

Raises:

ValidationError – Script is malformed or matches neither V1 nor V2 layout.

Return type:

DmintState

property is_exhausted: bool
is_v1: bool = False
height: int
contract_ref: GlyphRef
token_ref: GlyphRef
max_height: int
reward: int
algo: DmintAlgo
daa_mode: DaaMode
target_time: int
last_time: int
target: int
class pyrxd.glyph.dmint.DmintV1ContractInitialState[source]

Bases: object

Just-deployed state of a V1 dMint contract template.

Carries exactly the parameters needed to reconstruct the initial (height=0) contract codescript for every contract of a given deploy. Used by find_dmint_contract_utxos()’s fast path, where the caller already knows the deploy params.

Parameters:
  • num_contracts – Count of parallel contracts the deploy created (1..255 for V1; mainnet GLYPH used 32).

  • reward_sats – Photons emitted per successful mint (must fit in 3 bytes — V1 protocol constant).

  • max_height – Maximum mints per contract (3-byte ceiling).

  • target – 8-byte SHA256d PoW target.

  • algo – PoW algorithm. Defaults to DmintAlgo.SHA256D, which is the only algorithm seen on V1 mainnet.

__init__(num_contracts, reward_sats, max_height, target, algo=DmintAlgo.SHA256D)
Parameters:
Return type:

None

algo: DmintAlgo = 0
num_contracts: int
reward_sats: int
max_height: int
target: int
class pyrxd.glyph.dmint.PowPreimageResult[source]

Bases: object

The 64-byte PoW preimage plus the two script hashes a miner must push.

The covenant binds the PoW hash AND the scriptSig pushes together: it recomputes H2 = SHA256(scriptSig_inputHash || scriptSig_outputHash) and folds that into the same hash the miner solved. Diverging the preimage from the scriptSig pushes is a silent on-chain rejection — see docs/solutions/runtime-errors/dmint-v1-mint-scriptsig-shape.md for the prior incident that motivated returning all three values from a single helper.

Parameters:
  • preimage – 64-byte SHA256d PoW preimage; feeds mine_solution.

  • input_hashSHA256d(input_script) — push as scriptSig_inputHash.

  • output_hashSHA256d(output_script) — push as scriptSig_outputHash.

__init__(preimage, input_hash, output_hash)
Parameters:
Return type:

None

preimage: bytes
input_hash: bytes
output_hash: bytes
exception pyrxd.glyph.dmint.V2UnvalidatedWarning[source]

Bases: UserWarning

Retained warning category for V2 dMint code paths.

HISTORY: V2 dMint was once quarantined behind this warning because it had never been exercised against live consensus. That is no longer true — the canonical-Photonic V2 redesign is byte-matched to upstream and consensus- validated on radiant-core v3.1.1 regtest AND Radiant mainnet (3.1.2): the first V2 dMint deploy + PoW mint confirmed on mainnet (deploy 95335028…bb16fb09, mint 1239f64a…e0cd6c67; #219). The per-call warning is therefore no longer emitted.

The class is kept (not deleted) so any downstream warnings.simplefilter(…, V2UnvalidatedWarning) filters remain importable. V2 is still pre-external- audit — that caveat lives in the README / threat-model, the same level as V1, not in a per-call warning.

pyrxd.glyph.dmint.build_dmint_code_script(params)[source]

Build the V2 dMint code bytecode (Part A + powHashOp + Part B + Part C).

Parameters:

params (DmintDeployParams)

Return type:

bytes

pyrxd.glyph.dmint.build_dmint_contract_script(params)[source]

Build the full V2 dMint output script: state + OP_STATESEPARATOR + code.

Byte-identical to the canonical Photonic dMintScript for the same parameters (validated against golden vectors in tests/test_dmint_v2_canonical.py, and consensus-proven on regtest + mainnet).

Parameters:

params (DmintDeployParams)

Return type:

bytes

pyrxd.glyph.dmint.build_dmint_mint_tx(contract_utxo, nonce, miner_pkh, current_time, fee_rate=10000, *, funding_utxo=None, op_return_msg=None, half_life=3600, epoch_length=None, max_adjustment_log2=None, schedule=None)[source]

Build an unsigned dMint mint transaction.

Spends the live dMint contract UTXO, recreates the 1-photon contract singleton at height + 1, and pays the FT reward to miner_pkh from a separate plain-RXD funding input. V1 and V2 use the same consensus shape (the V2 covenant’s output-validation block is byte-identical to V1’s); they differ only in the nonce width (4B V1 / 8B V2) and the 10-item V2 state.

Transaction structure (both V1 and V2)

Inputs
  • Input 0: contract UTXO — covenant scriptSig build_mint_scriptsig(nonce, pow.input_hash, pow.output_hash, nonce_width=4|8).

  • Input 1: funding_utxo — a plain-RXD P2PKH input that pays the reward photons + tx fee + change (the contract is a 1-photon singleton).

Outputs
  • Output 0: recreated contract (current script with only height bumped), value 1.

  • Output 1: FT-wrapped reward output (value = state.reward).

  • Output 2: OP_RETURN (when op_return_msg is set) — the output the PoW preimage binds (see build_dmint_v2_mint_preimage() / V1 analog).

  • Output 3: change back to miner_pkh.

Note

V2 supports all five DAA modes — FIXED, ASERT, LWMA, EPOCH, SCHEDULE (the canonical Photonic redesign). The covenant rebuilds the next state’s last_time from OP_TXLOCKTIME and its target from the alt-stack DAA result on every mint, so current_time IS the block locktime: it is written into the recreated state’s last_time AND set as the tx nLockTime (the two must agree), and for DAA modes it drives the target retarget. current_time must be in [last_time, 0x7FFFFFFF] for DAA modes (a backwards or post-2038 locktime is rejected on-chain). EPOCH/SCHEDULE bake their parameters into the contract code (not the parsed state), so the caller passes epoch_length/max_adjustment_log2 or schedule (and half_life for ASERT) matching the deployed contract — a mismatch is caught before the PoW grind.

Note

The preimage is a function of the transaction itself (txid of the input being spent and the content of both the input and output locking scripts), which creates a circular dependency that cannot be resolved without a real node. The nonce + preimage in the returned tx’s unlocking script are therefore placeholder bytes derived from the inputs as known at build time. A production miner loop must:

  1. Build the unsigned tx shell via this function.

  2. Compute the real preimage AND scriptSig hashes via build_pow_preimage once the tx’s txid and script hashes are stable (they are stable once outputs are finalised — the txid doesn’t depend on the unlocking script in Radiant/Bitcoin sighash).

  3. Mine for a valid nonce via verify_sha256d_solution (or the relevant algo).

  4. Replace input 0’s unlocking script with build_mint_scriptsig(nonce, pow.input_hash, pow.output_hash). The two hashes MUST come from the same build_pow_preimage call that produced the preimage the miner solved — splitting the sources is a silent on-chain rejection.

  5. Broadcast.

Steps 2–5 are deliberately out of scope here — they require a live node connection or deterministic txid from a fully-built tx.

param contract_utxo:

The live dMint contract UTXO to spend.

param nonce:

8-byte PoW nonce (use b'\x00' * 8 as placeholder when building the tx shell; replace after mining).

param miner_pkh:

20-byte P2PKH hash of the miner’s reward address.

param current_time:

Unix timestamp of the block (used for DAA target computation). Caller is responsible for supplying a value consistent with the transaction’s locktime.

param fee_rate:

Photons per byte for fee calculation (default 10_000, the Radiant post-V2 relay minimum).

raises ValidationError:

contract_utxo.state.is_exhausted is True; nonce is not 8 bytes; miner_pkh is not 20 bytes.

returns:

DmintMintResult with the unsigned tx and updated state.

Parameters:
Return type:

DmintMintResult

pyrxd.glyph.dmint.build_dmint_state_script(params)[source]

Build the 10-item V2 dMint state script (before OP_STATESEPARATOR).

Layout (canonical redesign §4.2):

height(minimal) | d8:contractRef(36B) | d0:tokenRef(36B) |
maxHeight | reward | algoId | daaMode | targetTime |
lastTime(4B LE) | target(minimal)

height and target use minimal pushes (variable width) so the state script is MINIMALDATA-compliant from height 0 / target MAX onward — the old fixed 04 [LE4] height push was rejected by radiantd’s MINIMALDATA mempool policy on mainnet. lastTime stays a 4-byte push (Unix timestamps are always 4-byte minimal), which simplifies Part C’s 04 || NUM2BIN(4, locktime) reconstruction.

Parameters:

params (DmintDeployParams)

Return type:

bytes

pyrxd.glyph.dmint.build_dmint_v1_code_script(algo)[source]

Build the V1 dMint code epilogue (the 145 bytes after OP_STATESEPARATOR).

Returns _V1_EPILOGUE_PREFIX + <algo_byte> + _V1_EPILOGUE_SUFFIX where algo_byte is the on-chain hash opcode for the requested algorithm (0xaa SHA256D, 0xee BLAKE3, 0xef K12). The byte sequence matches every V1 contract decoded from mainnet; _match_v1_epilogue is the inverse.

Raises:

ValidationErroralgo is not a recognized DmintAlgo value (which would be a programming bug — the enum class enforces membership).

Parameters:

algo (DmintAlgo)

Return type:

bytes

pyrxd.glyph.dmint.build_dmint_v1_contract_script(height, contract_ref, token_ref, max_height, reward, target, algo=DmintAlgo.SHA256D)[source]

Build a full V1 dMint output script: state followed by V1 code epilogue.

Note: V1’s code epilogue begins with the OP_STATESEPARATOR byte (0xbd) — see _V1_EPILOGUE_PREFIX. Unlike the V2 builder (which interpolates a separate _OP_STATESEPARATOR), this function concatenates state and epilogue directly. Total length is 241 bytes for typical mainnet parameters (96-byte state + 145-byte epilogue), matching the byte-by-byte decode in docs/dmint-research-mainnet.md §2.2.

The output of this function round-trips through DmintState.from_script() with is_v1=True.

Parameters:
Return type:

bytes

pyrxd.glyph.dmint.build_dmint_v1_ft_output_script(miner_pkh, token_ref)[source]

Build the 75-byte P2PKH-wrapped FT output that a V1 mint produces.

Layout (docs/dmint-research-mainnet.md §4 vout[1]):

76 a9 14 <pkh:20>     OP_DUP OP_HASH160 PUSH20 pkh
88 ac                 OP_EQUALVERIFY OP_CHECKSIG    (25-byte P2PKH prologue)
bd                    OP_STATESEPARATOR
d0 <tokenRef:36>      OP_PUSHINPUTREF tokenRef       (37 bytes)
de c0 e9 aa 76 e3     12-byte covenant fingerprint   (`_V1_FT_OUTPUT_EPILOGUE`)
78 e4 a2 69 e6 9d
──────────────────────
Total: 75 bytes

This is the FT-bearing reward output — the V1 contract’s OP_CODESCRIPTHASHVALUESUM_OUTPUTS OP_NUMEQUALVERIFY at epilogue offset 168 sums photons under this codescript and requires the total to equal the contract’s reward field. Producing a plain P2PKH instead breaks FT conservation and the network rejects the mint.

Raises:

ValidationErrorminer_pkh is not 20 bytes.

Parameters:
Return type:

bytes

pyrxd.glyph.dmint.build_dmint_v1_mint_preimage(contract_utxo, funding_utxo, unsigned_tx)[source]

Build the V1 mining preimage AND scriptSig hashes for an unsigned mint tx.

The V1 covenant binds the PoW preimage to:

  1. The contract input’s outpoint txid + the contract ref (so a nonce mined for one contract slot can’t be replayed against another)

  2. The miner’s funding-input locking script (so the miner cannot substitute a different funding source after finding a nonce)

  3. The OP_RETURN msg output script at vout[2] (Photonic’s mainnet-canonical layout; the covenant computes outputHash = SHA256d(this script))

Layout (matches build_pow_preimage()):

preimage    = SHA256(txid_LE || contractRef) ||
              SHA256(SHA256d(input_script) || SHA256d(output_script))
input_hash  = SHA256d(input_script)    ← scriptSig push
output_hash = SHA256d(output_script)   ← scriptSig push

Callers feed preimage to mine_solution() and pass input_hash + output_hash to build_mint_scriptsig().

Parameters:
  • contract_utxo (DmintContractUtxo) – The V1 contract UTXO being spent.

  • funding_utxo (DmintMinerFundingUtxo) – The plain-RXD UTXO providing reward + fee.

  • unsigned_tx (Any) – The unsigned Transaction from build_dmint_mint_tx() — vout[2] is required to be the OP_RETURN msg output (mainnet-canonical 4-output shape).

Returns:

PowPreimageResult carrying the preimage and the two script hashes that the scriptSig must push for the covenant to accept the mint.

Raises:

ValidationErrorunsigned_tx has fewer than 4 outputs (no OP_RETURN at vout[2]) OR vout[2] is not actually an OP_RETURN script. Build the tx via build_dmint_mint_tx() with a non-empty op_return_msg; skipping that produces a 3-output tx, and hand-building a 4-output tx with a different vout[2] would silently bind the preimage to wrong bytes (the on-chain covenant would then reject after a successful mine — wasting the mining work).

Return type:

PowPreimageResult

pyrxd.glyph.dmint.build_dmint_v1_state_script(height, contract_ref, token_ref, max_height, reward, target)[source]

Build the 6-item V1 dMint state script (before OP_STATESEPARATOR).

Layout (docs/dmint-research-mainnet.md §2.2 offsets 0–94):

height(4B LE) | d8 contractRef(36B) | d0 tokenRef(36B) |
maxHeight | reward | target(0x08 + 8B LE)

The target is always pushed as a fixed 8-byte little-endian value (push opcode 0x08, then 8 bytes of payload). This is what distinguishes V1 from V2 in the state-script discriminator at parse time: V2’s item 5 is algoId via _push_minimal, never an 8-byte push.

Raises:

ValidationErrorheight < 0; max_height < 1; height >= max_height (born-exhausted contract); reward < 1; target not in [1, MAX_SHA256D_TARGET]. The upper target bound is MAX_SHA256D_TARGET = 0x7fff...ff rather than 2**64 because Bitcoin script integers are signed: pushing a value with the high bit set produces a negative number on the stack, and the on-chain target comparison would behave wrongly. Photonic Wallet’s dMintDiffToTarget formula always produces a value in this signed-positive range.

Parameters:
Return type:

bytes

pyrxd.glyph.dmint.build_dmint_v2_mint_preimage(contract_utxo, funding_utxo, output_script)[source]

Build the V2 mining preimage AND scriptSig hashes.

V2 analog of build_dmint_v1_mint_preimage(). The preimage shape (and the on-chain covenant’s H1/H2 binding logic) is identical to V1 — V2 inherits the output-validation block via _PART_C = _V1_EPILOGUE_SUFFIX[18:]. The only V1/V2 differences at the mint-tx level are the nonce width (8 bytes for V2 vs 4 for V1, a parameter of build_mint_scriptsig()) and the absence of the Photonic-Wallet op_return_msg convention in V2.

Layout (matches build_pow_preimage()):

preimage    = SHA256(txid_LE || contractRef) ||
              SHA256(SHA256d(input_script) || SHA256d(output_script))
input_hash  = SHA256d(input_script)    ← scriptSig push
output_hash = SHA256d(output_script)   ← scriptSig push

Unlike the V1 helper, this function takes output_script as an explicit argument. V2 has no canonical “OP_RETURN msg at vout[2]” convention (that’s Photonic-Wallet’s V1 layout); the V2 covenant binds outputHash to whatever bytes the caller chooses to push. Callers selecting output_script should pick one of the actual transaction outputs and document the binding in their own code.

Note

This helper closes the audit’s security-H1 finding (no V2 analog of build_dmint_v1_mint_preimage left V2 callers one careless script-mismatch away from reproducing the M1 bug pattern). V2 is consensus-proven on regtest + mainnet (#219).

Parameters:
  • contract_utxo (DmintContractUtxo) – The V2 contract UTXO being spent. Its state.is_v1 MUST be False — passing a V1 UTXO is a bug.

  • funding_utxo (DmintMinerFundingUtxo) – The plain-RXD UTXO providing reward + fee.

  • output_script (bytes) – The output-script bytes to bind into the preimage. V2 has no canonical convention; pick a transaction output the caller cares about (e.g. an OP_RETURN identifier, or the reward output’s locking script).

Returns:

PowPreimageResult with the preimage and the two script hashes the scriptSig must push.

Raises:

ValidationError – V1 contract UTXO passed by mistake, or an empty output_script.

Return type:

PowPreimageResult

pyrxd.glyph.dmint.build_mint_scriptsig(nonce, input_hash, output_hash, *, nonce_width=8)[source]

Build the scriptSig a miner includes in the contract-spend input.

Format (SHA256d):

V2 (nonce_width=8): <0x08> <nonce:8B> <0x20> <inputHash:32B> <0x20> <outputHash:32B> <0x00> → 76 bytes V1 (nonce_width=4): <0x04> <nonce:4B> <0x20> <inputHash:32B> <0x20> <outputHash:32B> <0x00> → 72 bytes

The V1 layout is documented in docs/dmint-research-mainnet.md §4 (vin[0] of the mainnet mint trace at 146a4d68…f3c). Same shape as V2, differing only in nonce width and corresponding push opcode.

The hashes pushed here MUST equal PowPreimageResult.input_hash and output_hash from the same build_pow_preimage() call that produced the preimage the miner solved. The on-chain covenant recomputes SHA256(input_hash || output_hash) from these pushes and folds that into the PoW hash — diverging them silently produces a mandatory-script-verify-flag-failed rejection after a successful mine.

Parameters:
  • nonce (bytes) – nonce_width-bytes nonce (found during mining).

  • input_hash (bytes) – 32-byte SHA256d(input_script) from PowPreimageResult.

  • output_hash (bytes) – 32-byte SHA256d(output_script) from PowPreimageResult.

  • nonce_width (Literal[4, 8]) – 4 for V1 contracts, 8 for V2. Keyword-only and Literal[4, 8] so a stray positional value is a type error rather than a silent V1/V2 confusion. Default 8 preserves pre-V1-support behavior.

Return type:

bytes

pyrxd.glyph.dmint.build_pow_preimage(txid_le, contract_ref_bytes, input_script, output_script)[source]

Build the PoW preimage AND the two script hashes the scriptSig must push.

preimage[0..32] = SHA256(txid_LE || contractRef) preimage[32..64] = SHA256(SHA256d(inputScript) || SHA256d(outputScript))

The covenant pulls inputHash and outputHash from the scriptSig pushes (not from the preimage halves) and recomputes the second SHA256 on-chain. Returning all three values here forces callers to feed both sites from the same source — splitting the helper into “preimage builder” and “scriptSig builder” with independently-recomputed hashes is what produced the M1 covenant-rejection bug.

Parameters:
  • txid_le (bytes) – 32-byte txid in little-endian (internal byte order)

  • contract_ref_bytes (bytes) – 36-byte contract ref (wire format)

  • input_script (bytes) – miner’s input locking script (e.g. P2PKH)

  • output_script (bytes) – miner’s output script (e.g. OP_RETURN message)

Returns:

PowPreimageResult with preimage, input_hash, output_hash.

Return type:

PowPreimageResult

pyrxd.glyph.dmint.compute_next_target_asert(current_target, last_time, current_time, target_time, half_life)[source]

Compute next ASERT-lite target (mirrors the redesigned on-chain bytecode).

The redesign replaced OP_LSHIFT/OP_RSHIFT (which Radiant evaluates as a big-endian bit-string shift — wrong on the LE target encoding) with an unrolled 4-step OP_2MUL/OP_2DIV loop with a per-step overflow cap:

drift = trunc((current_time - last_time - target_time) / half_life)  # clamp [-4,+4]
drift > 0:  repeat |drift|x:  target = MAX_TARGET if target > MAX/2 else target*2
drift < 0:  repeat |drift|x:  target = target // 2
minimum target is 1

The per-step cap matches the miner’s newTarget = min(MAX, oldTarget<<drift) clamp-at-MAX semantics (a naive target << drift would overshoot MAX).

Note

V2-only DAA. V1 has no DAA (fixed difficulty).

Parameters:
  • current_target (int)

  • last_time (int)

  • current_time (int)

  • target_time (int)

  • half_life (int)

Return type:

int

pyrxd.glyph.dmint.compute_next_target_epoch(current_target, last_time, current_time, target_time, height, epoch_length, max_adjustment_log2)[source]

Compute next EPOCH target (mirrors the on-chain buildEpochDaaBytecode).

Periodic retarget — only at epoch boundaries. height is the CURRENT (spent) contract’s height (the covenant gates on the state’s own height, OP_9 PICK):

if height > 0 and height % epoch_length == 0:
    delta        = current_time - last_time
    clampedDelta = max(target_time >> N, min(target_time << N, delta))
    new          = max(1, min(2^48, (min(target, 2^48) // target_time) * clampedDelta))
else: target unchanged

N = max_adjustment_log2 (1..4). The clamp keeps clampedDelta ≥ target_time>>N > 0, so the division has positive operands (floor == OP_DIV’s truncate-toward-zero). The target is clamped to EPOCH_MAX_SAFE_TARGET (2^48) on BOTH sides of the multiply and the divide runs first, so the on-chain int64 multiply never overflows (Radiant-Core/Photonic-Wallet#2). Capping the output at 2^48 keeps target there for the next epoch (difficulty floor 32768).

Note

V2-only DAA.

Parameters:
  • current_target (int)

  • last_time (int)

  • current_time (int)

  • target_time (int)

  • height (int)

  • epoch_length (int)

  • max_adjustment_log2 (int)

Return type:

int

pyrxd.glyph.dmint.compute_next_target_linear(current_target, last_time, current_time, target_time)[source]

Compute next linear/LWMA target (mirrors the redesigned on-chain bytecode).

Divide-first with caps so the on-chain OP_MUL never overflows int64:

timeDelta_capped = max(0, min(current_time - last_time, 4 * target_time))
target_capped    = min(current_target, MAX_TARGET // 4)
new_target       = min(MAX_TARGET, (target_capped // target_time) * timeDelta_capped)
minimum target is 1

The MAX/4 target cap means LWMA contracts cannot have a difficulty floor below 4 (target <= MAX_TARGET/4). The 0-floor on timeDelta mirrors the on-chain OP_0 OP_MAX (Radiant-Core/Photonic-Wallet#2): a backwards-clock block (locktime earlier than the previous mint) gives a negative delta that would otherwise underflow the on-chain int64 multiply.

Note

V2-only DAA.

Parameters:
  • current_target (int)

  • last_time (int)

  • current_time (int)

  • target_time (int)

Return type:

int

pyrxd.glyph.dmint.compute_next_target_schedule(current_target, height, schedule)[source]

Compute next SCHEDULE target (mirrors the on-chain buildScheduleDaaBytecode).

The target of the highest boundary height reached; unchanged if height is below the lowest boundary. height is the CURRENT (spent) contract’s height. schedule is ascending (height, target) pairs.

Note

V2-only DAA.

Parameters:
Return type:

int

pyrxd.glyph.dmint.difficulty_to_target(difficulty, algo=DmintAlgo.SHA256D)[source]

Convert difficulty to PoW target.

Parameters:
Return type:

int

async pyrxd.glyph.dmint.find_dmint_contract_utxos(client, *, token_ref, initial_state=None, limit=None, min_confirmations=1)[source]

Discover live V1 dMint contract UTXOs for a given token_ref.

Two call shapes:

  • Fast path — pass initial_state. The function rebuilds each contract’s expected initial codescript locally (contractRef[i] = (commit_txid, i+1), tokenRef = token_ref), computes its scripthash inline, and asks the server for the UTXO at that scripthash. One get_utxos call per contract. Use this shape immediately after deploy to verify all N contracts went live, or any time the caller has the deploy params handy.

  • Walk-from-reveal fallback — omit initial_state. The function fetches the deploy commit, derives the FT-commit hashlock’s scripthash, queries history for the reveal txid, then fetches the reveal and extracts every fresh V1 contract output whose tokenRef matches. Slower (3+ extra round-trips) but works on any live token where you only know the token_ref.

Both shapes apply the same security S2 cross-check: for each candidate UTXO returned, the source transaction is fetched and verified to have txid() matching the server’s tx_hash, and its output script byte-equal to the script the server claimed. Defends against a malicious or buggy ElectrumX serving altered bytes (mirrors find_dmint_funding_utxo()’s round-4 defense).

The fallback path returns fresh contracts only — UTXOs that have been mined from at least once are skipped (their state advanced and their scripthash drifted; following the spend chain forward to locate the current head is filed as deferred work).

Parameters:
  • client (Any) – An open pyrxd.network.electrumx.ElectrumXClient.

  • token_ref (GlyphRef) – The token’s permanent 36-byte ref (the deploy commit’s vout-0 outpoint, LE-reversed). Equivalently: GlyphRef(txid=commit_txid, vout=0).

  • initial_state (DmintV1ContractInitialState | None) – If supplied, fast-path. If None, walk from the deploy reveal.

  • limit (int | None) – If supplied, cap the result list at this many contracts. None returns all available.

  • min_confirmations (int) – Skip UTXOs younger than this many blocks. Default 1 (require at least 1 confirmation).

Returns:

A list of DmintContractUtxo for each currently-unspent contract whose script verified S2.

Raises:
  • ValidationError – Inputs malformed (token_ref must point at vout=0); or initial_state has out-of-range fields.

  • NetworkError – Propagated from the ElectrumX client.

Return type:

list[DmintContractUtxo]

async pyrxd.glyph.dmint.find_dmint_funding_utxo(client, miner_address, needed, *, require_confirmed=True)[source]

Scan miner_address for a plain-RXD UTXO that funds a V1 mint.

Excludes token-bearing UTXOs (FT, NFT, dMint covenant scripts) using is_token_bearing_script() — the same opcode-aware walker the V1 mint builder enforces. Returns the largest qualifying candidate to minimise change-output dust risk.

A plain-RXD funding input is what the V1 covenant requires (V1 contracts are singletons; reward + fee come from a separate input). Spending an FT/NFT/dMint UTXO as fee silently destroys the token — this scan is the load-bearing defense.

Parameters:
  • client (Any) – An already-connected pyrxd.network.electrumx.ElectrumXClient.

  • miner_address (str) – Radiant address (R…) of the wallet to scan.

  • needed (int) – Minimum photons the candidate must hold.

  • require_confirmed (bool) – Default True. Skip UTXOs with height == 0 (unconfirmed). Picking an unconfirmed UTXO can cause “missing inputs” rejection when the parent tx hasn’t propagated to all relays, or leave a dangling tx if the parent gets evicted from mempool. Set False only if you’re deliberately funding from a same-tx chain.

Returns:

The largest qualifying funding UTXO.

Raises:

InvalidFundingUtxoError – No plain-RXD UTXO at miner_address covers needed. The error message reports counts of (a) token-bearing skipped, (b) too-small skipped, (c) unconfirmed skipped (when require_confirmed=True), and (d) network-error skipped, so the caller can diagnose why the wallet failed the scan.

Return type:

DmintMinerFundingUtxo

pyrxd.glyph.dmint.is_token_bearing_script(script)[source]

Return True if script uses any OP_PUSHINPUTREF-family opcode.

Walks the script as an opcode stream so that only opcode position bytes are checked against the ref-opcode range — a naive bare-byte scan would falsely flag any P2PKH whose 20-byte hash contains a 0xd0–0xd8 byte (~51% of random addresses), denying about half of honest miners.

Truncated push fields are treated as token-bearing — a malformed script of ambiguous length should not be accepted as funding.

Built on the shared opcode walker pyrxd.glyph.script.iter_input_refs() (single source of truth for ref detection; see also count_input_refs for the exactly-which-refs covenant guard).

Parameters:

script (bytes)

Return type:

bool

pyrxd.glyph.dmint.mine_solution(preimage, target, *, algo=DmintAlgo.SHA256D, nonce_width=4, max_attempts=600000000)[source]

Search for a nonce satisfying the V1/V2 dMint PoW target.

Sequential nonce sweep starting at 0. The nonce is encoded as a little-endian unsigned integer of the requested width (4 bytes for V1, 8 bytes for V2 — matches glyph-miner’s nonceBytesForContracts).

Calls verify_sha256d_solution() per candidate; that’s the single source of truth for “does this hash satisfy the target.” Drift between the mining check and the verifier check would let pyrxd produce a nonce that passes locally but fails on-chain (or vice versa).

Parameters:
  • preimage (bytes) – 64-byte preimage from build_pow_preimage().

  • target (int) – 8-byte 64-bit target (the V1/V2 contract’s target state field).

  • algo (DmintAlgo) – Hash algorithm. Only SHA256D is implemented; BLAKE3 and K12 raise NotImplementedError.

  • nonce_width (Literal[4, 8]) – 4 for V1, 8 for V2. Keyword-only and Literal[4, 8] so a stray positional value is a type error rather than a silent V1/V2 confusion.

  • max_attempts (int) – Upper bound on iterations before raising MaxAttemptsError. Defaults to ≈minutes single-core at typical CPython hashlib speeds.

Raises:
  • ValidationErrorpreimage is not 64 bytes, target is not positive, nonce_width is not 4 or 8, or max_attempts is < 1.

  • NotImplementedErroralgo is BLAKE3 or K12.

  • MaxAttemptsError – No solution found within max_attempts iterations. The exception’s attempts and elapsed_s attributes carry telemetry.

Return type:

DmintMineResult

Worked example (small target chosen so the loop completes in ms):

>>> from pyrxd.glyph.dmint import (
...     mine_solution, verify_sha256d_solution, MAX_SHA256D_TARGET,
... )
>>> preimage = b"\x00" * 64
>>> target = MAX_SHA256D_TARGET >> 8  # easy: ~1 in 256 expected
>>> result = mine_solution(preimage, target, nonce_width=4)
>>> verify_sha256d_solution(preimage, result.nonce, target, nonce_width=4)
True
pyrxd.glyph.dmint.mine_solution_dispatch(preimage, target, *, nonce_width=4, algo=DmintAlgo.SHA256D, miner_argv=None, max_attempts=600000000, timeout_s=600.0)[source]

Mine a nonce — picks the in-process or subprocess miner from one entrypoint.

Most callers want this helper rather than calling mine_solution() or mine_solution_external() directly. The two paths share semantics — both return a DmintMineResult with a nonce that satisfies the target — but have disjoint parameter sets (max_attempts vs timeout_s, no-argv vs argv). Picking between them was a 30-line wrapper that every demo and operator script ended up rewriting; this function is that wrapper, with the branch in one place.

Dispatch rule:

  • miner_argv is None (default): run mine_solution() in this process. Slow but correct. Use for tests, small examples, and contracts where mining takes < a minute.

  • miner_argv is not None: invoke mine_solution_external() with the supplied argv. The external miner (e.g. pyrxd.contrib.miner, a custom binary, or glyph-miner) runs as a subprocess and returns a verified nonce via the JSON-over-stdio protocol. The local re-verification in mine_solution_external is the load-bearing safety check against a buggy or malicious miner.

Parameters:
  • preimage (bytes) – 64-byte preimage from build_pow_preimage().

  • target (int) – The PoW target.

  • nonce_width (Literal[4, 8]) – 4 for V1 contracts, 8 for V2.

  • algo (DmintAlgo) – Hash algorithm. Currently only SHA256D is implemented; BLAKE3 and K12 raise from mine_solution(). Ignored on the external-miner path (the protocol doesn’t carry an algo field; external miners are assumed SHA256D until the protocol is extended).

  • miner_argv (list[str] | None) – None → in-process; otherwise an argv list passed to subprocess.run() for the external miner. Use [sys.executable, "-m", "pyrxd.contrib.miner"] for the bundled parallel miner.

  • max_attempts (int) – Iteration cap on the in-process path. Ignored on the external-miner path (the external miner caps via timeout_s instead).

  • timeout_s (float) – Subprocess timeout on the external-miner path. Ignored in-process (use max_attempts there).

Returns:

DmintMineResult with the verified nonce.

Raises:
  • MaxAttemptsError – in-process exhausted max_attempts, or external miner exceeded timeout_s / explicitly signalled exhaustion.

  • ValidationError – external miner returned a malformed response or a nonce that fails local verification.

Return type:

DmintMineResult

pyrxd.glyph.dmint.mine_solution_external(preimage, target, *, miner_argv, nonce_width=4, timeout_s=600.0)[source]

Delegate nonce search to an external miner via JSON-over-subprocess.

Spawns miner_argv as a subprocess, writes one JSON line to its stdin, reads one JSON line from its stdout, and re-verifies the returned nonce locally. The local re-verification is the load-bearing safety check — a wrong nonce from the external process raises rather than getting silently embedded in a transaction.

The miner is expected to:

  1. Read one JSON object from stdin: {"preimage_hex", "target_hex", "nonce_width"}.

  2. Search for a valid nonce.

  3. Write one JSON object to stdout — on a hit (exit 0): {"nonce_hex", "attempts", "elapsed_s"}; on nonce-space exhaustion (exit 2, added in 0.5.1): {"exhausted": true} (pyrxd then raises MaxAttemptsError immediately rather than waiting for the parent timeout to fire).

A bundled reference implementation ships at pyrxd.contrib.miner (added in 0.5.1) — see Parallel mining and the external-miner protocol for the full protocol spec and operational notes. Invoke it via:

miner_argv=[sys.executable, "-m", "pyrxd.contrib.miner"]

Warning

Supply-chain risk: pyrxd does NOT pin or verify the miner binary. miner_argv[0] is resolved by the OS at exec time, so a malicious binary earlier in $PATH can intercept calls. The local nonce re-verification (below) defends against the miner returning a wrong nonce, but cannot detect side-channel exfiltration: a malicious miner sees the preimage (which encodes the contract ref + miner binding) and can leak it out-of-band over the network.

Mitigations the caller should consider:

  • Invoke with an absolute path (["/usr/local/bin/glyph-miner", ...]) rather than a bare name to bypass $PATH resolution.

  • Verify the binary’s checksum against the upstream release before first use.

  • Run pyrxd in an environment where $PATH is controlled (e.g. a dedicated user account, sandbox, or container).

For testing and trusted environments the bare-name form is fine.

Parameters:
  • preimage (bytes) – 64-byte preimage from build_pow_preimage().

  • target (int) – The PoW target.

  • miner_argv (list[str]) – argv passed to subprocess.run() (e.g. ["glyph-miner", "--stdin"]). The first element must be a binary or shell-resolvable name; pyrxd does not pin a specific miner. See the supply-chain warning above.

  • nonce_width (Literal[4, 8]) – 4 for V1, 8 for V2.

  • timeout_s (float) – Hard timeout. The subprocess is killed and MaxAttemptsError raised on expiry.

Raises:
  • ValidationError – The miner returned a malformed JSON response, a nonce of wrong width, or a nonce that fails local verification.

  • MaxAttemptsError – The miner exceeded timeout_s.

  • FileNotFoundErrorminer_argv[0] is not on PATH.

Return type:

DmintMineResult

pyrxd.glyph.dmint.target_to_difficulty(target, algo=DmintAlgo.SHA256D)[source]

Convert PoW target to difficulty (approximate).

Parameters:
Return type:

int

pyrxd.glyph.dmint.verify_sha256d_solution(preimage, nonce, target, *, nonce_width=8)[source]

Verify a SHA256d PoW solution.

Valid if: hash[0..4] == 0x00000000 AND int.from_bytes(hash[4..12], ‘big’) < target

target is clamped to MAX_SHA256D_TARGET before comparison — a caller-supplied target above the maximum would make the check trivially pass for any hash that starts with four zero bytes.

Parameters:
  • nonce_width (Literal[4, 8]) – 4 for V1 contracts, 8 for V2. Default 8 preserves the pre-V1-support behavior. Passed as keyword-only so a stray positional 4 vs 8 is a type error rather than a silent V1/V2 confusion.

  • preimage (bytes)

  • nonce (bytes)

  • target (int)

Return type:

bool

The subpackage layers as types builders chain miner (a one-way dependency graph). Every public symbol is re-exported at the pyrxd.glyph.dmint path via PEP 562 lazy __getattr__; the reference below documents each symbol at its defining submodule.

Types & parameters

Type definitions for the dMint subpackage.

Pure data types consumed by ≥2 sibling submodules, plus the V2UnvalidatedWarning warning class and shared module-level byte constants. Depends on nothing within the subpackage; siblings import from here, not the reverse.

Symbols (15):

V2UnvalidatedWarning, MAX_SHA256D_TARGET, MAX_V2_TARGET_256, DmintAlgo, DaaMode, _PART_B1, _PART_B2, _PART_B4, DmintDeployParams, DmintCborPayload, DmintMintResult, DmintV1ContractInitialState

exception pyrxd.glyph.dmint.types.V2UnvalidatedWarning[source]

Bases: UserWarning

Retained warning category for V2 dMint code paths.

HISTORY: V2 dMint was once quarantined behind this warning because it had never been exercised against live consensus. That is no longer true — the canonical-Photonic V2 redesign is byte-matched to upstream and consensus- validated on radiant-core v3.1.1 regtest AND Radiant mainnet (3.1.2): the first V2 dMint deploy + PoW mint confirmed on mainnet (deploy 95335028…bb16fb09, mint 1239f64a…e0cd6c67; #219). The per-call warning is therefore no longer emitted.

The class is kept (not deleted) so any downstream warnings.simplefilter(…, V2UnvalidatedWarning) filters remain importable. V2 is still pre-external- audit — that caveat lives in the README / threat-model, the same level as V1, not in a per-call warning.

class pyrxd.glyph.dmint.types.DmintAlgo[source]

Bases: IntEnum

SHA256D = 0
BLAKE3 = 1
K12 = 2
__new__(value)
class pyrxd.glyph.dmint.types.DaaMode[source]

Bases: IntEnum

FIXED = 0
EPOCH = 1
ASERT = 2
LWMA = 3
SCHEDULE = 4
__new__(value)
class pyrxd.glyph.dmint.types.DmintDeployParams[source]

Bases: object

Parameters for deploying a V2 dMint contract.

contract_ref: GlyphRef
token_ref: GlyphRef
max_height: int
reward: int
difficulty: int
algo: DmintAlgo = 0
daa_mode: DaaMode = 0
target_time: int = 60
half_life: int = 3600
height: int = 0
last_time: int = 0
epoch_length: int = 2016
max_adjustment_log2: int = 2
schedule: tuple[tuple[int, int], ...] = ()
property initial_target: int

Compute initial target from difficulty using the SHA256d formula.

__init__(contract_ref, token_ref, max_height, reward, difficulty, algo=DmintAlgo.SHA256D, daa_mode=DaaMode.FIXED, target_time=60, half_life=3600, height=0, last_time=0, epoch_length=2016, max_adjustment_log2=2, schedule=())
Parameters:
Return type:

None

class pyrxd.glyph.dmint.types.DmintCborPayload[source]

Bases: object

The dmint object embedded in Glyph V2 token metadata CBOR.

Indexers read this to discover dMint contracts and display mining parameters in wallets/explorers without parsing the contract script.

Field names mirror Photonic Wallet DmintPayload type in types.ts.

algo: DmintAlgo
num_contracts: int
max_height: int
reward: int
premine: int
diff: int
daa_mode: DaaMode = 0
target_block_time: int = 60
half_life: int = 0
window_size: int = 0
to_cbor_dict()[source]

Encode to the dict that becomes the dmint CBOR value.

Return type:

dict

classmethod from_cbor_dict(d)[source]

Parse the dmint CBOR value from an on-chain payload.

Parameters:

d (dict)

Return type:

DmintCborPayload

__init__(algo, num_contracts, max_height, reward, premine, diff, daa_mode=DaaMode.FIXED, target_block_time=60, half_life=0, window_size=0)
Parameters:
Return type:

None

class pyrxd.glyph.dmint.types.DmintMintResult[source]

Bases: object

Output of build_dmint_mint_tx().

Parameters:
  • tx – Unsigned transaction (caller must sign).

  • updated_state – New DmintState written into the contract output (height incremented, target updated if DAA is active).

  • contract_script – New contract output script (state + separator + code).

  • reward_script – P2PKH locking script of the miner reward output.

  • fee – Transaction fee in photons.

Note

The transaction returned here is unsigned — it uses raw script bytes for the contract input’s unlocking script (nonce + preimage halves) built by build_mint_scriptsig(). The contract script is a covenant, not a P2PKH, so standard Transaction.sign() is not appropriate. The caller must either set the unlocking script directly or use a custom signing path. See docstring of build_dmint_mint_tx() for details.

tx: Any
updated_state: Any
contract_script: bytes
reward_script: bytes
fee: int
__init__(tx, updated_state, contract_script, reward_script, fee)
Parameters:
Return type:

None

class pyrxd.glyph.dmint.types.DmintV1ContractInitialState[source]

Bases: object

Just-deployed state of a V1 dMint contract template.

Carries exactly the parameters needed to reconstruct the initial (height=0) contract codescript for every contract of a given deploy. Used by find_dmint_contract_utxos()’s fast path, where the caller already knows the deploy params.

Parameters:
  • num_contracts – Count of parallel contracts the deploy created (1..255 for V1; mainnet GLYPH used 32).

  • reward_sats – Photons emitted per successful mint (must fit in 3 bytes — V1 protocol constant).

  • max_height – Maximum mints per contract (3-byte ceiling).

  • target – 8-byte SHA256d PoW target.

  • algo – PoW algorithm. Defaults to DmintAlgo.SHA256D, which is the only algorithm seen on V1 mainnet.

num_contracts: int
reward_sats: int
max_height: int
target: int
algo: DmintAlgo = 0
__init__(num_contracts, reward_sats, max_height, target, algo=DmintAlgo.SHA256D)
Parameters:
Return type:

None

Covenant & transaction builders

Script-construction primitives for the dMint subpackage.

Locking-script builders (V1 + V2), DAA helpers (ASERT, linear), and the low-level push helpers. Depends on .types only.

build_mint_scriptsig lives in .miner despite its name — its sole callers are the mint-tx assembly functions, so the call cluster stays local to that module.

NOTE ON PLAN DEVIATION: The plan placed _V1_EPILOGUE_PREFIX, _V1_EPILOGUE_ALGO_OFFSET, _V1_EPILOGUE_SUFFIX, and _V1_EPILOGUE_LEN in chain.py. However, build_dmint_v1_code_script (assigned to builders.py) uses those constants directly, which would require a builders chain import and violate the one-way dependency graph. Moving these four constants here resolves the cycle: chain.py imports them from builders.py via the allowed chain builders edge. The _match_v1_epilogue function (which also uses them) is in chain.py and imports them from here.

Symbols (17 + 4 epilogue constants shared with chain):

_push_minimal, _push_4bytes_le, _PART_A, _POW_HASH_OP, _build_asert_daa, _build_linear_daa, _build_part_b, build_dmint_state_script, build_dmint_code_script, build_dmint_contract_script, _V1_ALGO_BYTE_TO_ENUM, _V1_ENUM_TO_ALGO_BYTE, build_dmint_v1_state_script, build_dmint_v1_code_script, _V1_FT_OUTPUT_EPILOGUE, build_dmint_v1_ft_output_script, build_dmint_v1_contract_script, _V1_EPILOGUE_PREFIX, _V1_EPILOGUE_ALGO_OFFSET, _V1_EPILOGUE_SUFFIX, _V1_EPILOGUE_LEN

pyrxd.glyph.dmint.builders.build_dmint_state_script(params)[source]

Build the 10-item V2 dMint state script (before OP_STATESEPARATOR).

Layout (canonical redesign §4.2):

height(minimal) | d8:contractRef(36B) | d0:tokenRef(36B) |
maxHeight | reward | algoId | daaMode | targetTime |
lastTime(4B LE) | target(minimal)

height and target use minimal pushes (variable width) so the state script is MINIMALDATA-compliant from height 0 / target MAX onward — the old fixed 04 [LE4] height push was rejected by radiantd’s MINIMALDATA mempool policy on mainnet. lastTime stays a 4-byte push (Unix timestamps are always 4-byte minimal), which simplifies Part C’s 04 || NUM2BIN(4, locktime) reconstruction.

Parameters:

params (DmintDeployParams)

Return type:

bytes

pyrxd.glyph.dmint.builders.build_dmint_code_script(params)[source]

Build the V2 dMint code bytecode (Part A + powHashOp + Part B + Part C).

Parameters:

params (DmintDeployParams)

Return type:

bytes

pyrxd.glyph.dmint.builders.build_dmint_contract_script(params)[source]

Build the full V2 dMint output script: state + OP_STATESEPARATOR + code.

Byte-identical to the canonical Photonic dMintScript for the same parameters (validated against golden vectors in tests/test_dmint_v2_canonical.py, and consensus-proven on regtest + mainnet).

Parameters:

params (DmintDeployParams)

Return type:

bytes

pyrxd.glyph.dmint.builders.build_dmint_v1_state_script(height, contract_ref, token_ref, max_height, reward, target)[source]

Build the 6-item V1 dMint state script (before OP_STATESEPARATOR).

Layout (docs/dmint-research-mainnet.md §2.2 offsets 0–94):

height(4B LE) | d8 contractRef(36B) | d0 tokenRef(36B) |
maxHeight | reward | target(0x08 + 8B LE)

The target is always pushed as a fixed 8-byte little-endian value (push opcode 0x08, then 8 bytes of payload). This is what distinguishes V1 from V2 in the state-script discriminator at parse time: V2’s item 5 is algoId via _push_minimal, never an 8-byte push.

Raises:

ValidationErrorheight < 0; max_height < 1; height >= max_height (born-exhausted contract); reward < 1; target not in [1, MAX_SHA256D_TARGET]. The upper target bound is MAX_SHA256D_TARGET = 0x7fff...ff rather than 2**64 because Bitcoin script integers are signed: pushing a value with the high bit set produces a negative number on the stack, and the on-chain target comparison would behave wrongly. Photonic Wallet’s dMintDiffToTarget formula always produces a value in this signed-positive range.

Parameters:
Return type:

bytes

pyrxd.glyph.dmint.builders.build_dmint_v1_code_script(algo)[source]

Build the V1 dMint code epilogue (the 145 bytes after OP_STATESEPARATOR).

Returns _V1_EPILOGUE_PREFIX + <algo_byte> + _V1_EPILOGUE_SUFFIX where algo_byte is the on-chain hash opcode for the requested algorithm (0xaa SHA256D, 0xee BLAKE3, 0xef K12). The byte sequence matches every V1 contract decoded from mainnet; _match_v1_epilogue is the inverse.

Raises:

ValidationErroralgo is not a recognized DmintAlgo value (which would be a programming bug — the enum class enforces membership).

Parameters:

algo (DmintAlgo)

Return type:

bytes

pyrxd.glyph.dmint.builders.build_dmint_v1_ft_output_script(miner_pkh, token_ref)[source]

Build the 75-byte P2PKH-wrapped FT output that a V1 mint produces.

Layout (docs/dmint-research-mainnet.md §4 vout[1]):

76 a9 14 <pkh:20>     OP_DUP OP_HASH160 PUSH20 pkh
88 ac                 OP_EQUALVERIFY OP_CHECKSIG    (25-byte P2PKH prologue)
bd                    OP_STATESEPARATOR
d0 <tokenRef:36>      OP_PUSHINPUTREF tokenRef       (37 bytes)
de c0 e9 aa 76 e3     12-byte covenant fingerprint   (`_V1_FT_OUTPUT_EPILOGUE`)
78 e4 a2 69 e6 9d
──────────────────────
Total: 75 bytes

This is the FT-bearing reward output — the V1 contract’s OP_CODESCRIPTHASHVALUESUM_OUTPUTS OP_NUMEQUALVERIFY at epilogue offset 168 sums photons under this codescript and requires the total to equal the contract’s reward field. Producing a plain P2PKH instead breaks FT conservation and the network rejects the mint.

Raises:

ValidationErrorminer_pkh is not 20 bytes.

Parameters:
Return type:

bytes

pyrxd.glyph.dmint.builders.build_dmint_v1_contract_script(height, contract_ref, token_ref, max_height, reward, target, algo=DmintAlgo.SHA256D)[source]

Build a full V1 dMint output script: state followed by V1 code epilogue.

Note: V1’s code epilogue begins with the OP_STATESEPARATOR byte (0xbd) — see _V1_EPILOGUE_PREFIX. Unlike the V2 builder (which interpolates a separate _OP_STATESEPARATOR), this function concatenates state and epilogue directly. Total length is 241 bytes for typical mainnet parameters (96-byte state + 145-byte epilogue), matching the byte-by-byte decode in docs/dmint-research-mainnet.md §2.2.

The output of this function round-trips through DmintState.from_script() with is_v1=True.

Parameters:
Return type:

bytes

Contract & chain state

Chain-walking + on-chain-byte parsing for the dMint subpackage.

Covers find_dmint_*_utxo* helpers, the is_token_bearing_script classifier, opcode-walker primitives (_parse_script_int, _decode_script_le_int, _match_v1_epilogue), and the DmintState/DmintContractUtxo/DmintMinerFundingUtxo dataclasses whose construction depends on parser logic. Depends on .types and .builders (the latter via _find_v1_contract_utxos_fast which uses build_dmint_v1_contract_script for shape validation, and via _match_v1_epilogue which uses _V1_EPILOGUE_* constants).

NOTE ON PLAN DEVIATION: The plan listed _V1_EPILOGUE_PREFIX, _V1_EPILOGUE_ALGO_OFFSET, _V1_EPILOGUE_SUFFIX, and _V1_EPILOGUE_LEN in this module, and _OP_STATESEPARATOR here too. Both were moved: the epilogue constants to builders.py (because build_dmint_v1_code_script there uses them, and placing them here would require a builders chain cycle), and _OP_STATESEPARATOR to types.py (because builders.py’s build_dmint_v1_ft_output_script and build_dmint_contract_script also need it). Both moves preserve the one-way types builders chain miner dependency graph. This module re-exports _OP_STATESEPARATOR and the epilogue constants so they remain importable from their plan-specified locations for any downstream that imports from chain directly.

Symbols (19):

_OP_STATESEPARATOR (re-export from types), _parse_script_int, _decode_script_le_int, _V1_EPILOGUE_PREFIX (re-export from builders), _V1_EPILOGUE_ALGO_OFFSET (re-export from builders), _V1_EPILOGUE_SUFFIX (re-export from builders), _V1_EPILOGUE_LEN (re-export from builders), _match_v1_epilogue, DmintState, DmintContractUtxo, DmintMinerFundingUtxo, is_token_bearing_script, find_dmint_funding_utxo, _scripthash_for_script, find_dmint_contract_utxos, _find_v1_contract_utxos_fast, _find_v1_contract_utxos_walk, _s2_verify_contract_utxos

Proof-of-work miner

Mining + mint-tx assembly for the dMint subpackage.

Mining loop (in-process + external + dispatch), PoW preimage construction, difficulty/target math, scriptSig assembly, and the complete build_dmint_mint_tx pipeline. Carries the miner-domain result dataclasses (PowPreimageResult, DmintMineResult) and timeout constants (DEFAULT_MAX_ATTEMPTS, EXTERNAL_MINER_TIMEOUT_S). Depends on .types, .builders, .chain.

Symbols (19):

PowPreimageResult, build_pow_preimage, build_mint_scriptsig, compute_next_target_asert, compute_next_target_linear, difficulty_to_target, target_to_difficulty, verify_sha256d_solution, DEFAULT_MAX_ATTEMPTS, EXTERNAL_MINER_TIMEOUT_S, DmintMineResult, mine_solution, mine_solution_external, mine_solution_dispatch, build_dmint_mint_tx, _build_dmint_v1_mint_tx, _varint_size, build_dmint_v1_mint_preimage, build_dmint_v2_mint_preimage

class pyrxd.glyph.dmint.miner.PowPreimageResult[source]

Bases: object

The 64-byte PoW preimage plus the two script hashes a miner must push.

The covenant binds the PoW hash AND the scriptSig pushes together: it recomputes H2 = SHA256(scriptSig_inputHash || scriptSig_outputHash) and folds that into the same hash the miner solved. Diverging the preimage from the scriptSig pushes is a silent on-chain rejection — see docs/solutions/runtime-errors/dmint-v1-mint-scriptsig-shape.md for the prior incident that motivated returning all three values from a single helper.

Parameters:
  • preimage – 64-byte SHA256d PoW preimage; feeds mine_solution.

  • input_hashSHA256d(input_script) — push as scriptSig_inputHash.

  • output_hashSHA256d(output_script) — push as scriptSig_outputHash.

preimage: bytes
input_hash: bytes
output_hash: bytes
__init__(preimage, input_hash, output_hash)
Parameters:
Return type:

None

pyrxd.glyph.dmint.miner.build_pow_preimage(txid_le, contract_ref_bytes, input_script, output_script)[source]

Build the PoW preimage AND the two script hashes the scriptSig must push.

preimage[0..32] = SHA256(txid_LE || contractRef) preimage[32..64] = SHA256(SHA256d(inputScript) || SHA256d(outputScript))

The covenant pulls inputHash and outputHash from the scriptSig pushes (not from the preimage halves) and recomputes the second SHA256 on-chain. Returning all three values here forces callers to feed both sites from the same source — splitting the helper into “preimage builder” and “scriptSig builder” with independently-recomputed hashes is what produced the M1 covenant-rejection bug.

Parameters:
  • txid_le (bytes) – 32-byte txid in little-endian (internal byte order)

  • contract_ref_bytes (bytes) – 36-byte contract ref (wire format)

  • input_script (bytes) – miner’s input locking script (e.g. P2PKH)

  • output_script (bytes) – miner’s output script (e.g. OP_RETURN message)

Returns:

PowPreimageResult with preimage, input_hash, output_hash.

Return type:

PowPreimageResult

pyrxd.glyph.dmint.miner.build_mint_scriptsig(nonce, input_hash, output_hash, *, nonce_width=8)[source]

Build the scriptSig a miner includes in the contract-spend input.

Format (SHA256d):

V2 (nonce_width=8): <0x08> <nonce:8B> <0x20> <inputHash:32B> <0x20> <outputHash:32B> <0x00> → 76 bytes V1 (nonce_width=4): <0x04> <nonce:4B> <0x20> <inputHash:32B> <0x20> <outputHash:32B> <0x00> → 72 bytes

The V1 layout is documented in docs/dmint-research-mainnet.md §4 (vin[0] of the mainnet mint trace at 146a4d68…f3c). Same shape as V2, differing only in nonce width and corresponding push opcode.

The hashes pushed here MUST equal PowPreimageResult.input_hash and output_hash from the same build_pow_preimage() call that produced the preimage the miner solved. The on-chain covenant recomputes SHA256(input_hash || output_hash) from these pushes and folds that into the PoW hash — diverging them silently produces a mandatory-script-verify-flag-failed rejection after a successful mine.

Parameters:
  • nonce (bytes) – nonce_width-bytes nonce (found during mining).

  • input_hash (bytes) – 32-byte SHA256d(input_script) from PowPreimageResult.

  • output_hash (bytes) – 32-byte SHA256d(output_script) from PowPreimageResult.

  • nonce_width (Literal[4, 8]) – 4 for V1 contracts, 8 for V2. Keyword-only and Literal[4, 8] so a stray positional value is a type error rather than a silent V1/V2 confusion. Default 8 preserves pre-V1-support behavior.

Return type:

bytes

pyrxd.glyph.dmint.miner.compute_next_target_asert(current_target, last_time, current_time, target_time, half_life)[source]

Compute next ASERT-lite target (mirrors the redesigned on-chain bytecode).

The redesign replaced OP_LSHIFT/OP_RSHIFT (which Radiant evaluates as a big-endian bit-string shift — wrong on the LE target encoding) with an unrolled 4-step OP_2MUL/OP_2DIV loop with a per-step overflow cap:

drift = trunc((current_time - last_time - target_time) / half_life)  # clamp [-4,+4]
drift > 0:  repeat |drift|x:  target = MAX_TARGET if target > MAX/2 else target*2
drift < 0:  repeat |drift|x:  target = target // 2
minimum target is 1

The per-step cap matches the miner’s newTarget = min(MAX, oldTarget<<drift) clamp-at-MAX semantics (a naive target << drift would overshoot MAX).

Note

V2-only DAA. V1 has no DAA (fixed difficulty).

Parameters:
  • current_target (int)

  • last_time (int)

  • current_time (int)

  • target_time (int)

  • half_life (int)

Return type:

int

pyrxd.glyph.dmint.miner.compute_next_target_linear(current_target, last_time, current_time, target_time)[source]

Compute next linear/LWMA target (mirrors the redesigned on-chain bytecode).

Divide-first with caps so the on-chain OP_MUL never overflows int64:

timeDelta_capped = max(0, min(current_time - last_time, 4 * target_time))
target_capped    = min(current_target, MAX_TARGET // 4)
new_target       = min(MAX_TARGET, (target_capped // target_time) * timeDelta_capped)
minimum target is 1

The MAX/4 target cap means LWMA contracts cannot have a difficulty floor below 4 (target <= MAX_TARGET/4). The 0-floor on timeDelta mirrors the on-chain OP_0 OP_MAX (Radiant-Core/Photonic-Wallet#2): a backwards-clock block (locktime earlier than the previous mint) gives a negative delta that would otherwise underflow the on-chain int64 multiply.

Note

V2-only DAA.

Parameters:
  • current_target (int)

  • last_time (int)

  • current_time (int)

  • target_time (int)

Return type:

int

pyrxd.glyph.dmint.miner.compute_next_target_epoch(current_target, last_time, current_time, target_time, height, epoch_length, max_adjustment_log2)[source]

Compute next EPOCH target (mirrors the on-chain buildEpochDaaBytecode).

Periodic retarget — only at epoch boundaries. height is the CURRENT (spent) contract’s height (the covenant gates on the state’s own height, OP_9 PICK):

if height > 0 and height % epoch_length == 0:
    delta        = current_time - last_time
    clampedDelta = max(target_time >> N, min(target_time << N, delta))
    new          = max(1, min(2^48, (min(target, 2^48) // target_time) * clampedDelta))
else: target unchanged

N = max_adjustment_log2 (1..4). The clamp keeps clampedDelta ≥ target_time>>N > 0, so the division has positive operands (floor == OP_DIV’s truncate-toward-zero). The target is clamped to EPOCH_MAX_SAFE_TARGET (2^48) on BOTH sides of the multiply and the divide runs first, so the on-chain int64 multiply never overflows (Radiant-Core/Photonic-Wallet#2). Capping the output at 2^48 keeps target there for the next epoch (difficulty floor 32768).

Note

V2-only DAA.

Parameters:
  • current_target (int)

  • last_time (int)

  • current_time (int)

  • target_time (int)

  • height (int)

  • epoch_length (int)

  • max_adjustment_log2 (int)

Return type:

int

pyrxd.glyph.dmint.miner.compute_next_target_schedule(current_target, height, schedule)[source]

Compute next SCHEDULE target (mirrors the on-chain buildScheduleDaaBytecode).

The target of the highest boundary height reached; unchanged if height is below the lowest boundary. height is the CURRENT (spent) contract’s height. schedule is ascending (height, target) pairs.

Note

V2-only DAA.

Parameters:
Return type:

int

pyrxd.glyph.dmint.miner.difficulty_to_target(difficulty, algo=DmintAlgo.SHA256D)[source]

Convert difficulty to PoW target.

Parameters:
Return type:

int

pyrxd.glyph.dmint.miner.target_to_difficulty(target, algo=DmintAlgo.SHA256D)[source]

Convert PoW target to difficulty (approximate).

Parameters:
Return type:

int

pyrxd.glyph.dmint.miner.verify_sha256d_solution(preimage, nonce, target, *, nonce_width=8)[source]

Verify a SHA256d PoW solution.

Valid if: hash[0..4] == 0x00000000 AND int.from_bytes(hash[4..12], ‘big’) < target

target is clamped to MAX_SHA256D_TARGET before comparison — a caller-supplied target above the maximum would make the check trivially pass for any hash that starts with four zero bytes.

Parameters:
  • nonce_width (Literal[4, 8]) – 4 for V1 contracts, 8 for V2. Default 8 preserves the pre-V1-support behavior. Passed as keyword-only so a stray positional 4 vs 8 is a type error rather than a silent V1/V2 confusion.

  • preimage (bytes)

  • nonce (bytes)

  • target (int)

Return type:

bool

class pyrxd.glyph.dmint.miner.DmintMineResult[source]

Bases: object

The output of a successful mine_solution() call.

Parameters:
  • nonce – The nonce bytes (4B for V1, 8B for V2) that satisfy the target.

  • attempts – Number of nonce candidates tried before finding the solution.

  • elapsed_s – Wall-clock seconds spent searching.

nonce: bytes
attempts: int
elapsed_s: float
__init__(nonce, attempts, elapsed_s)
Parameters:
Return type:

None

pyrxd.glyph.dmint.miner.mine_solution(preimage, target, *, algo=DmintAlgo.SHA256D, nonce_width=4, max_attempts=600000000)[source]

Search for a nonce satisfying the V1/V2 dMint PoW target.

Sequential nonce sweep starting at 0. The nonce is encoded as a little-endian unsigned integer of the requested width (4 bytes for V1, 8 bytes for V2 — matches glyph-miner’s nonceBytesForContracts).

Calls verify_sha256d_solution() per candidate; that’s the single source of truth for “does this hash satisfy the target.” Drift between the mining check and the verifier check would let pyrxd produce a nonce that passes locally but fails on-chain (or vice versa).

Parameters:
  • preimage (bytes) – 64-byte preimage from build_pow_preimage().

  • target (int) – 8-byte 64-bit target (the V1/V2 contract’s target state field).

  • algo (DmintAlgo) – Hash algorithm. Only SHA256D is implemented; BLAKE3 and K12 raise NotImplementedError.

  • nonce_width (Literal[4, 8]) – 4 for V1, 8 for V2. Keyword-only and Literal[4, 8] so a stray positional value is a type error rather than a silent V1/V2 confusion.

  • max_attempts (int) – Upper bound on iterations before raising MaxAttemptsError. Defaults to ≈minutes single-core at typical CPython hashlib speeds.

Raises:
  • ValidationErrorpreimage is not 64 bytes, target is not positive, nonce_width is not 4 or 8, or max_attempts is < 1.

  • NotImplementedErroralgo is BLAKE3 or K12.

  • MaxAttemptsError – No solution found within max_attempts iterations. The exception’s attempts and elapsed_s attributes carry telemetry.

Return type:

DmintMineResult

Worked example (small target chosen so the loop completes in ms):

>>> from pyrxd.glyph.dmint import (
...     mine_solution, verify_sha256d_solution, MAX_SHA256D_TARGET,
... )
>>> preimage = b"\x00" * 64
>>> target = MAX_SHA256D_TARGET >> 8  # easy: ~1 in 256 expected
>>> result = mine_solution(preimage, target, nonce_width=4)
>>> verify_sha256d_solution(preimage, result.nonce, target, nonce_width=4)
True
pyrxd.glyph.dmint.miner.mine_solution_external(preimage, target, *, miner_argv, nonce_width=4, timeout_s=600.0)[source]

Delegate nonce search to an external miner via JSON-over-subprocess.

Spawns miner_argv as a subprocess, writes one JSON line to its stdin, reads one JSON line from its stdout, and re-verifies the returned nonce locally. The local re-verification is the load-bearing safety check — a wrong nonce from the external process raises rather than getting silently embedded in a transaction.

The miner is expected to:

  1. Read one JSON object from stdin: {"preimage_hex", "target_hex", "nonce_width"}.

  2. Search for a valid nonce.

  3. Write one JSON object to stdout — on a hit (exit 0): {"nonce_hex", "attempts", "elapsed_s"}; on nonce-space exhaustion (exit 2, added in 0.5.1): {"exhausted": true} (pyrxd then raises MaxAttemptsError immediately rather than waiting for the parent timeout to fire).

A bundled reference implementation ships at pyrxd.contrib.miner (added in 0.5.1) — see Parallel mining and the external-miner protocol for the full protocol spec and operational notes. Invoke it via:

miner_argv=[sys.executable, "-m", "pyrxd.contrib.miner"]

Warning

Supply-chain risk: pyrxd does NOT pin or verify the miner binary. miner_argv[0] is resolved by the OS at exec time, so a malicious binary earlier in $PATH can intercept calls. The local nonce re-verification (below) defends against the miner returning a wrong nonce, but cannot detect side-channel exfiltration: a malicious miner sees the preimage (which encodes the contract ref + miner binding) and can leak it out-of-band over the network.

Mitigations the caller should consider:

  • Invoke with an absolute path (["/usr/local/bin/glyph-miner", ...]) rather than a bare name to bypass $PATH resolution.

  • Verify the binary’s checksum against the upstream release before first use.

  • Run pyrxd in an environment where $PATH is controlled (e.g. a dedicated user account, sandbox, or container).

For testing and trusted environments the bare-name form is fine.

Parameters:
  • preimage (bytes) – 64-byte preimage from build_pow_preimage().

  • target (int) – The PoW target.

  • miner_argv (list[str]) – argv passed to subprocess.run() (e.g. ["glyph-miner", "--stdin"]). The first element must be a binary or shell-resolvable name; pyrxd does not pin a specific miner. See the supply-chain warning above.

  • nonce_width (Literal[4, 8]) – 4 for V1, 8 for V2.

  • timeout_s (float) – Hard timeout. The subprocess is killed and MaxAttemptsError raised on expiry.

Raises:
  • ValidationError – The miner returned a malformed JSON response, a nonce of wrong width, or a nonce that fails local verification.

  • MaxAttemptsError – The miner exceeded timeout_s.

  • FileNotFoundErrorminer_argv[0] is not on PATH.

Return type:

DmintMineResult

pyrxd.glyph.dmint.miner.mine_solution_dispatch(preimage, target, *, nonce_width=4, algo=DmintAlgo.SHA256D, miner_argv=None, max_attempts=600000000, timeout_s=600.0)[source]

Mine a nonce — picks the in-process or subprocess miner from one entrypoint.

Most callers want this helper rather than calling mine_solution() or mine_solution_external() directly. The two paths share semantics — both return a DmintMineResult with a nonce that satisfies the target — but have disjoint parameter sets (max_attempts vs timeout_s, no-argv vs argv). Picking between them was a 30-line wrapper that every demo and operator script ended up rewriting; this function is that wrapper, with the branch in one place.

Dispatch rule:

  • miner_argv is None (default): run mine_solution() in this process. Slow but correct. Use for tests, small examples, and contracts where mining takes < a minute.

  • miner_argv is not None: invoke mine_solution_external() with the supplied argv. The external miner (e.g. pyrxd.contrib.miner, a custom binary, or glyph-miner) runs as a subprocess and returns a verified nonce via the JSON-over-stdio protocol. The local re-verification in mine_solution_external is the load-bearing safety check against a buggy or malicious miner.

Parameters:
  • preimage (bytes) – 64-byte preimage from build_pow_preimage().

  • target (int) – The PoW target.

  • nonce_width (Literal[4, 8]) – 4 for V1 contracts, 8 for V2.

  • algo (DmintAlgo) – Hash algorithm. Currently only SHA256D is implemented; BLAKE3 and K12 raise from mine_solution(). Ignored on the external-miner path (the protocol doesn’t carry an algo field; external miners are assumed SHA256D until the protocol is extended).

  • miner_argv (list[str] | None) – None → in-process; otherwise an argv list passed to subprocess.run() for the external miner. Use [sys.executable, "-m", "pyrxd.contrib.miner"] for the bundled parallel miner.

  • max_attempts (int) – Iteration cap on the in-process path. Ignored on the external-miner path (the external miner caps via timeout_s instead).

  • timeout_s (float) – Subprocess timeout on the external-miner path. Ignored in-process (use max_attempts there).

Returns:

DmintMineResult with the verified nonce.

Raises:
  • MaxAttemptsError – in-process exhausted max_attempts, or external miner exceeded timeout_s / explicitly signalled exhaustion.

  • ValidationError – external miner returned a malformed response or a nonce that fails local verification.

Return type:

DmintMineResult

pyrxd.glyph.dmint.miner.build_dmint_mint_tx(contract_utxo, nonce, miner_pkh, current_time, fee_rate=10000, *, funding_utxo=None, op_return_msg=None, half_life=3600, epoch_length=None, max_adjustment_log2=None, schedule=None)[source]

Build an unsigned dMint mint transaction.

Spends the live dMint contract UTXO, recreates the 1-photon contract singleton at height + 1, and pays the FT reward to miner_pkh from a separate plain-RXD funding input. V1 and V2 use the same consensus shape (the V2 covenant’s output-validation block is byte-identical to V1’s); they differ only in the nonce width (4B V1 / 8B V2) and the 10-item V2 state.

Transaction structure (both V1 and V2)

Inputs
  • Input 0: contract UTXO — covenant scriptSig build_mint_scriptsig(nonce, pow.input_hash, pow.output_hash, nonce_width=4|8).

  • Input 1: funding_utxo — a plain-RXD P2PKH input that pays the reward photons + tx fee + change (the contract is a 1-photon singleton).

Outputs
  • Output 0: recreated contract (current script with only height bumped), value 1.

  • Output 1: FT-wrapped reward output (value = state.reward).

  • Output 2: OP_RETURN (when op_return_msg is set) — the output the PoW preimage binds (see build_dmint_v2_mint_preimage() / V1 analog).

  • Output 3: change back to miner_pkh.

Note

V2 supports all five DAA modes — FIXED, ASERT, LWMA, EPOCH, SCHEDULE (the canonical Photonic redesign). The covenant rebuilds the next state’s last_time from OP_TXLOCKTIME and its target from the alt-stack DAA result on every mint, so current_time IS the block locktime: it is written into the recreated state’s last_time AND set as the tx nLockTime (the two must agree), and for DAA modes it drives the target retarget. current_time must be in [last_time, 0x7FFFFFFF] for DAA modes (a backwards or post-2038 locktime is rejected on-chain). EPOCH/SCHEDULE bake their parameters into the contract code (not the parsed state), so the caller passes epoch_length/max_adjustment_log2 or schedule (and half_life for ASERT) matching the deployed contract — a mismatch is caught before the PoW grind.

Note

The preimage is a function of the transaction itself (txid of the input being spent and the content of both the input and output locking scripts), which creates a circular dependency that cannot be resolved without a real node. The nonce + preimage in the returned tx’s unlocking script are therefore placeholder bytes derived from the inputs as known at build time. A production miner loop must:

  1. Build the unsigned tx shell via this function.

  2. Compute the real preimage AND scriptSig hashes via build_pow_preimage once the tx’s txid and script hashes are stable (they are stable once outputs are finalised — the txid doesn’t depend on the unlocking script in Radiant/Bitcoin sighash).

  3. Mine for a valid nonce via verify_sha256d_solution (or the relevant algo).

  4. Replace input 0’s unlocking script with build_mint_scriptsig(nonce, pow.input_hash, pow.output_hash). The two hashes MUST come from the same build_pow_preimage call that produced the preimage the miner solved — splitting the sources is a silent on-chain rejection.

  5. Broadcast.

Steps 2–5 are deliberately out of scope here — they require a live node connection or deterministic txid from a fully-built tx.

param contract_utxo:

The live dMint contract UTXO to spend.

param nonce:

8-byte PoW nonce (use b'\x00' * 8 as placeholder when building the tx shell; replace after mining).

param miner_pkh:

20-byte P2PKH hash of the miner’s reward address.

param current_time:

Unix timestamp of the block (used for DAA target computation). Caller is responsible for supplying a value consistent with the transaction’s locktime.

param fee_rate:

Photons per byte for fee calculation (default 10_000, the Radiant post-V2 relay minimum).

raises ValidationError:

contract_utxo.state.is_exhausted is True; nonce is not 8 bytes; miner_pkh is not 20 bytes.

returns:

DmintMintResult with the unsigned tx and updated state.

Parameters:
Return type:

DmintMintResult

pyrxd.glyph.dmint.miner.build_dmint_v1_mint_preimage(contract_utxo, funding_utxo, unsigned_tx)[source]

Build the V1 mining preimage AND scriptSig hashes for an unsigned mint tx.

The V1 covenant binds the PoW preimage to:

  1. The contract input’s outpoint txid + the contract ref (so a nonce mined for one contract slot can’t be replayed against another)

  2. The miner’s funding-input locking script (so the miner cannot substitute a different funding source after finding a nonce)

  3. The OP_RETURN msg output script at vout[2] (Photonic’s mainnet-canonical layout; the covenant computes outputHash = SHA256d(this script))

Layout (matches build_pow_preimage()):

preimage    = SHA256(txid_LE || contractRef) ||
              SHA256(SHA256d(input_script) || SHA256d(output_script))
input_hash  = SHA256d(input_script)    ← scriptSig push
output_hash = SHA256d(output_script)   ← scriptSig push

Callers feed preimage to mine_solution() and pass input_hash + output_hash to build_mint_scriptsig().

Parameters:
  • contract_utxo (DmintContractUtxo) – The V1 contract UTXO being spent.

  • funding_utxo (DmintMinerFundingUtxo) – The plain-RXD UTXO providing reward + fee.

  • unsigned_tx (Any) – The unsigned Transaction from build_dmint_mint_tx() — vout[2] is required to be the OP_RETURN msg output (mainnet-canonical 4-output shape).

Returns:

PowPreimageResult carrying the preimage and the two script hashes that the scriptSig must push for the covenant to accept the mint.

Raises:

ValidationErrorunsigned_tx has fewer than 4 outputs (no OP_RETURN at vout[2]) OR vout[2] is not actually an OP_RETURN script. Build the tx via build_dmint_mint_tx() with a non-empty op_return_msg; skipping that produces a 3-output tx, and hand-building a 4-output tx with a different vout[2] would silently bind the preimage to wrong bytes (the on-chain covenant would then reject after a successful mine — wasting the mining work).

Return type:

PowPreimageResult

pyrxd.glyph.dmint.miner.build_dmint_v2_mint_preimage(contract_utxo, funding_utxo, output_script)[source]

Build the V2 mining preimage AND scriptSig hashes.

V2 analog of build_dmint_v1_mint_preimage(). The preimage shape (and the on-chain covenant’s H1/H2 binding logic) is identical to V1 — V2 inherits the output-validation block via _PART_C = _V1_EPILOGUE_SUFFIX[18:]. The only V1/V2 differences at the mint-tx level are the nonce width (8 bytes for V2 vs 4 for V1, a parameter of build_mint_scriptsig()) and the absence of the Photonic-Wallet op_return_msg convention in V2.

Layout (matches build_pow_preimage()):

preimage    = SHA256(txid_LE || contractRef) ||
              SHA256(SHA256d(input_script) || SHA256d(output_script))
input_hash  = SHA256d(input_script)    ← scriptSig push
output_hash = SHA256d(output_script)   ← scriptSig push

Unlike the V1 helper, this function takes output_script as an explicit argument. V2 has no canonical “OP_RETURN msg at vout[2]” convention (that’s Photonic-Wallet’s V1 layout); the V2 covenant binds outputHash to whatever bytes the caller chooses to push. Callers selecting output_script should pick one of the actual transaction outputs and document the binding in their own code.

Note

This helper closes the audit’s security-H1 finding (no V2 analog of build_dmint_v1_mint_preimage left V2 callers one careless script-mismatch away from reproducing the M1 bug pattern). V2 is consensus-proven on regtest + mainnet (#219).

Parameters:
  • contract_utxo (DmintContractUtxo) – The V2 contract UTXO being spent. Its state.is_v1 MUST be False — passing a V1 UTXO is a bug.

  • funding_utxo (DmintMinerFundingUtxo) – The plain-RXD UTXO providing reward + fee.

  • output_script (bytes) – The output-script bytes to bind into the preimage. V2 has no canonical convention; pick a transaction output the caller cares about (e.g. an OP_RETURN identifier, or the reward output’s locking script).

Returns:

PowPreimageResult with the preimage and the two script hashes the scriptSig must push.

Raises:

ValidationError – V1 contract UTXO passed by mistake, or an empty output_script.

Return type:

PowPreimageResult