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_STATESEPARATORlives intypes(notchain) becausebuildersneeds it andbuilders → chainwould be a cycle._V1_EPILOGUE_PREFIX/_ALGO_OFFSET/_SUFFIX/_LENlive inbuilders(notchain) for the same reason;chainre-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:
objectThe
dmintobject 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
DmintPayloadtype 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)¶
- class pyrxd.glyph.dmint.DmintContractUtxo[source]¶
Bases:
objectDescribes 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 viaDmintState.from_script(script)
- __init__(txid, vout, value, script, state)¶
- Parameters:
txid (str)
vout (int)
value (int)
script (bytes)
state (DmintState)
- Return type:
None
- state: DmintState¶
- class pyrxd.glyph.dmint.DmintDeployParams[source]¶
Bases:
objectParameters 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
- class pyrxd.glyph.dmint.DmintMineResult[source]¶
Bases:
objectThe 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)¶
- class pyrxd.glyph.dmint.DmintMinerFundingUtxo[source]¶
Bases:
objectA plain RXD UTXO supplied by the miner to fund a V1 mint.
The V1 covenant takes its FT output value (
rewardphotons) 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 raisesInvalidFundingUtxoErrorif 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)¶
- class pyrxd.glyph.dmint.DmintMintResult[source]¶
Bases:
objectOutput of
build_dmint_mint_tx().- Parameters:
tx – Unsigned transaction (caller must sign).
updated_state – New
DmintStatewritten 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 standardTransaction.sign()is not appropriate. The caller must either set the unlocking script directly or use a custom signing path. See docstring ofbuild_dmint_mint_tx()for details.- __init__(tx, updated_state, contract_script, reward_script, fee)¶
- class pyrxd.glyph.dmint.DmintState[source]¶
Bases:
objectParsed 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_v1is True iff this state was parsed from V1 layout — in which casetarget_timeandlast_timeare not meaningful on-chain values and are set to 0;daa_modeis alwaysFIXEDfor 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)¶
- 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
ValidationErrorif 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:
- class pyrxd.glyph.dmint.DmintV1ContractInitialState[source]¶
Bases:
objectJust-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)¶
- class pyrxd.glyph.dmint.PowPreimageResult[source]¶
Bases:
objectThe 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 — seedocs/solutions/runtime-errors/dmint-v1-mint-scriptsig-shape.mdfor the prior incident that motivated returning all three values from a single helper.- Parameters:
preimage – 64-byte SHA256d PoW preimage; feeds
mine_solution.input_hash –
SHA256d(input_script)— push asscriptSig_inputHash.output_hash –
SHA256d(output_script)— push asscriptSig_outputHash.
- __init__(preimage, input_hash, output_hash)¶
- exception pyrxd.glyph.dmint.V2UnvalidatedWarning[source]¶
Bases:
UserWarningRetained 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, mint1239f64a…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:
- 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
dMintScriptfor 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:
- 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 tominer_pkhfrom 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
heightbumped), value 1.Output 1: FT-wrapped reward output (value =
state.reward).Output 2: OP_RETURN (when
op_return_msgis set) — the output the PoW preimage binds (seebuild_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_timefromOP_TXLOCKTIMEand itstargetfrom the alt-stack DAA result on every mint, socurrent_timeIS the block locktime: it is written into the recreated state’slast_timeAND set as the txnLockTime(the two must agree), and for DAA modes it drives the target retarget.current_timemust 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 passesepoch_length/max_adjustment_log2orschedule(andhalf_lifefor 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:
Build the unsigned tx shell via this function.
Compute the real preimage AND scriptSig hashes via
build_pow_preimageonce 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).Mine for a valid
nonceviaverify_sha256d_solution(or the relevant algo).Replace input 0’s unlocking script with
build_mint_scriptsig(nonce, pow.input_hash, pow.output_hash). The two hashes MUST come from the samebuild_pow_preimagecall that produced the preimage the miner solved — splitting the sources is a silent on-chain rejection.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' * 8as 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_exhaustedis True;nonceis not 8 bytes;miner_pkhis not 20 bytes.- returns:
DmintMintResultwith the unsigned tx and updated state.
- Parameters:
- Return type:
- 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)
heightandtargetuse minimal pushes (variable width) so the state script is MINIMALDATA-compliant from height 0 / target MAX onward — the old fixed04 [LE4]height push was rejected by radiantd’s MINIMALDATA mempool policy on mainnet.lastTimestays a 4-byte push (Unix timestamps are always 4-byte minimal), which simplifies Part C’s04 || NUM2BIN(4, locktime)reconstruction.- Parameters:
params (DmintDeployParams)
- Return type:
- 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_SUFFIXwherealgo_byteis 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_epilogueis the inverse.- Raises:
ValidationError –
algois not a recognizedDmintAlgovalue (which would be a programming bug — the enum class enforces membership).- Parameters:
algo (DmintAlgo)
- Return type:
- 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()withis_v1=True.
- 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_NUMEQUALVERIFYat epilogue offset 168 sums photons under this codescript and requires the total to equal the contract’srewardfield. Producing a plain P2PKH instead breaks FT conservation and the network rejects the mint.- Raises:
ValidationError –
miner_pkhis not 20 bytes.- Parameters:
- Return type:
- 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:
The contract input’s outpoint txid + the contract ref (so a nonce mined for one contract slot can’t be replayed against another)
The miner’s funding-input locking script (so the miner cannot substitute a different funding source after finding a nonce)
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 pushCallers feed
preimagetomine_solution()and passinput_hash+output_hashtobuild_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
Transactionfrombuild_dmint_mint_tx()— vout[2] is required to be the OP_RETURN msg output (mainnet-canonical 4-output shape).
- Returns:
PowPreimageResultcarrying the preimage and the two script hashes that the scriptSig must push for the covenant to accept the mint.- Raises:
ValidationError –
unsigned_txhas fewer than 4 outputs (no OP_RETURN at vout[2]) OR vout[2] is not actually an OP_RETURN script. Build the tx viabuild_dmint_mint_tx()with a non-emptyop_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:
- 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
algoIdvia_push_minimal, never an 8-byte push.- Raises:
ValidationError –
height < 0;max_height < 1;height >= max_height(born-exhausted contract);reward < 1;targetnot in[1, MAX_SHA256D_TARGET]. The upper target bound isMAX_SHA256D_TARGET = 0x7fff...ffrather than2**64because 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’sdMintDiffToTargetformula always produces a value in this signed-positive range.- Parameters:
- Return type:
- 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 ofbuild_mint_scriptsig()) and the absence of the Photonic-Walletop_return_msgconvention 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 pushUnlike the V1 helper, this function takes
output_scriptas 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 selectingoutput_scriptshould 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_preimageleft 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_v1MUST beFalse— 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:
PowPreimageResultwith 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:
- 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_hashandoutput_hashfrom the samebuild_pow_preimage()call that produced the preimage the miner solved. The on-chain covenant recomputesSHA256(input_hash || output_hash)from these pushes and folds that into the PoW hash — diverging them silently produces amandatory-script-verify-flag-failedrejection after a successful mine.- Parameters:
nonce (bytes) – nonce_width-bytes nonce (found during mining).
input_hash (bytes) – 32-byte
SHA256d(input_script)fromPowPreimageResult.output_hash (bytes) – 32-byte
SHA256d(output_script)fromPowPreimageResult.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:
- 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
inputHashandoutputHashfrom 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:
- Returns:
PowPreimageResultwithpreimage,input_hash,output_hash.- Return type:
- 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 naivetarget << driftwould overshoot MAX).Note
V2-only DAA. V1 has no DAA (fixed difficulty).
- 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.
heightis 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 toEPOCH_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 keepstargetthere for the next epoch (difficulty floor 32768).Note
V2-only DAA.
- 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 ontimeDeltamirrors the on-chainOP_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.
- 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
heightreached; unchanged ifheightis below the lowest boundary.heightis the CURRENT (spent) contract’s height.scheduleis ascending(height, target)pairs.Note
V2-only DAA.
- pyrxd.glyph.dmint.difficulty_to_target(difficulty, algo=DmintAlgo.SHA256D)[source]¶
Convert difficulty to PoW target.
- 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. Oneget_utxoscall 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 whosetokenRefmatches. Slower (3+ extra round-trips) but works on any live token where you only know thetoken_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’stx_hash, and its output script byte-equal to the script the server claimed. Defends against a malicious or buggy ElectrumX serving altered bytes (mirrorsfind_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.
Nonereturns all available.min_confirmations (int) – Skip UTXOs younger than this many blocks. Default 1 (require at least 1 confirmation).
- Returns:
A list of
DmintContractUtxofor 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:
- async pyrxd.glyph.dmint.find_dmint_funding_utxo(client, miner_address, needed, *, require_confirmed=True)[source]¶
Scan
miner_addressfor 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 withheight == 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. SetFalseonly if you’re deliberately funding from a same-tx chain.
- Returns:
The largest qualifying funding UTXO.
- Raises:
InvalidFundingUtxoError – No plain-RXD UTXO at
miner_addresscoversneeded. The error message reports counts of (a) token-bearing skipped, (b) too-small skipped, (c) unconfirmed skipped (whenrequire_confirmed=True), and (d) network-error skipped, so the caller can diagnose why the wallet failed the scan.- Return type:
- pyrxd.glyph.dmint.is_token_bearing_script(script)[source]¶
Return True if
scriptuses 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 alsocount_input_refsfor the exactly-which-refs covenant guard).
- 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
targetstate 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:
ValidationError –
preimageis not 64 bytes,targetis not positive,nonce_widthis not 4 or 8, ormax_attemptsis < 1.NotImplementedError –
algois BLAKE3 or K12.MaxAttemptsError – No solution found within
max_attemptsiterations. The exception’sattemptsandelapsed_sattributes carry telemetry.
- Return type:
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()ormine_solution_external()directly. The two paths share semantics — both return aDmintMineResultwith a nonce that satisfies the target — but have disjoint parameter sets (max_attemptsvstimeout_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): runmine_solution()in this process. Slow but correct. Use for tests, small examples, and contracts where mining takes < a minute.miner_argv is not None: invokemine_solution_external()with the supplied argv. The external miner (e.g.pyrxd.contrib.miner, a custom binary, orglyph-miner) runs as a subprocess and returns a verified nonce via the JSON-over-stdio protocol. The local re-verification inmine_solution_externalis 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 tosubprocess.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_sinstead).timeout_s (float) – Subprocess timeout on the external-miner path. Ignored in-process (use
max_attemptsthere).
- Returns:
DmintMineResultwith the verified nonce.- Raises:
MaxAttemptsError – in-process exhausted
max_attempts, or external miner exceededtimeout_s/ explicitly signalled exhaustion.ValidationError – external miner returned a malformed response or a nonce that fails local verification.
- Return type:
- 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_argvas 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:
Read one JSON object from stdin:
{"preimage_hex", "target_hex", "nonce_width"}.Search for a valid nonce.
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 raisesMaxAttemptsErrorimmediately 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$PATHcan 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$PATHresolution.Verify the binary’s checksum against the upstream release before first use.
Run pyrxd in an environment where
$PATHis 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
MaxAttemptsErrorraised 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.FileNotFoundError –
miner_argv[0]is not on PATH.
- Return type:
- pyrxd.glyph.dmint.target_to_difficulty(target, algo=DmintAlgo.SHA256D)[source]¶
Convert PoW target to difficulty (approximate).
- 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.
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:
UserWarningRetained 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, mint1239f64a…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:
objectParameters 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
- class pyrxd.glyph.dmint.types.DmintCborPayload[source]¶
Bases:
objectThe
dmintobject 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
DmintPayloadtype in types.ts.- classmethod from_cbor_dict(d)[source]¶
Parse the
dmintCBOR value from an on-chain payload.- Parameters:
d (dict)
- Return type:
- __init__(algo, num_contracts, max_height, reward, premine, diff, daa_mode=DaaMode.FIXED, target_block_time=60, half_life=0, window_size=0)¶
- class pyrxd.glyph.dmint.types.DmintMintResult[source]¶
Bases:
objectOutput of
build_dmint_mint_tx().- Parameters:
tx – Unsigned transaction (caller must sign).
updated_state – New
DmintStatewritten 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 standardTransaction.sign()is not appropriate. The caller must either set the unlocking script directly or use a custom signing path. See docstring ofbuild_dmint_mint_tx()for details.
- class pyrxd.glyph.dmint.types.DmintV1ContractInitialState[source]¶
Bases:
objectJust-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.
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)
heightandtargetuse minimal pushes (variable width) so the state script is MINIMALDATA-compliant from height 0 / target MAX onward — the old fixed04 [LE4]height push was rejected by radiantd’s MINIMALDATA mempool policy on mainnet.lastTimestays a 4-byte push (Unix timestamps are always 4-byte minimal), which simplifies Part C’s04 || NUM2BIN(4, locktime)reconstruction.- Parameters:
params (DmintDeployParams)
- Return type:
- 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:
- 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
dMintScriptfor 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:
- 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
algoIdvia_push_minimal, never an 8-byte push.- Raises:
ValidationError –
height < 0;max_height < 1;height >= max_height(born-exhausted contract);reward < 1;targetnot in[1, MAX_SHA256D_TARGET]. The upper target bound isMAX_SHA256D_TARGET = 0x7fff...ffrather than2**64because 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’sdMintDiffToTargetformula always produces a value in this signed-positive range.- Parameters:
- Return type:
- 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_SUFFIXwherealgo_byteis 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_epilogueis the inverse.- Raises:
ValidationError –
algois not a recognizedDmintAlgovalue (which would be a programming bug — the enum class enforces membership).- Parameters:
algo (DmintAlgo)
- Return type:
- 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_NUMEQUALVERIFYat epilogue offset 168 sums photons under this codescript and requires the total to equal the contract’srewardfield. Producing a plain P2PKH instead breaks FT conservation and the network rejects the mint.- Raises:
ValidationError –
miner_pkhis not 20 bytes.- Parameters:
- Return type:
- 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()withis_v1=True.
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:
objectThe 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 — seedocs/solutions/runtime-errors/dmint-v1-mint-scriptsig-shape.mdfor the prior incident that motivated returning all three values from a single helper.- Parameters:
preimage – 64-byte SHA256d PoW preimage; feeds
mine_solution.input_hash –
SHA256d(input_script)— push asscriptSig_inputHash.output_hash –
SHA256d(output_script)— push asscriptSig_outputHash.
- 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
inputHashandoutputHashfrom 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:
- Returns:
PowPreimageResultwithpreimage,input_hash,output_hash.- Return type:
- 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_hashandoutput_hashfrom the samebuild_pow_preimage()call that produced the preimage the miner solved. The on-chain covenant recomputesSHA256(input_hash || output_hash)from these pushes and folds that into the PoW hash — diverging them silently produces amandatory-script-verify-flag-failedrejection after a successful mine.- Parameters:
nonce (bytes) – nonce_width-bytes nonce (found during mining).
input_hash (bytes) – 32-byte
SHA256d(input_script)fromPowPreimageResult.output_hash (bytes) – 32-byte
SHA256d(output_script)fromPowPreimageResult.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:
- 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 naivetarget << driftwould overshoot MAX).Note
V2-only DAA. V1 has no DAA (fixed difficulty).
- 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 ontimeDeltamirrors the on-chainOP_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.
- 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.
heightis 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 toEPOCH_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 keepstargetthere for the next epoch (difficulty floor 32768).Note
V2-only DAA.
- 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
heightreached; unchanged ifheightis below the lowest boundary.heightis the CURRENT (spent) contract’s height.scheduleis ascending(height, target)pairs.Note
V2-only DAA.
- pyrxd.glyph.dmint.miner.difficulty_to_target(difficulty, algo=DmintAlgo.SHA256D)[source]¶
Convert difficulty to PoW target.
- pyrxd.glyph.dmint.miner.target_to_difficulty(target, algo=DmintAlgo.SHA256D)[source]¶
Convert PoW target to difficulty (approximate).
- 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.
- class pyrxd.glyph.dmint.miner.DmintMineResult[source]¶
Bases:
objectThe 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.
- 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
targetstate 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:
ValidationError –
preimageis not 64 bytes,targetis not positive,nonce_widthis not 4 or 8, ormax_attemptsis < 1.NotImplementedError –
algois BLAKE3 or K12.MaxAttemptsError – No solution found within
max_attemptsiterations. The exception’sattemptsandelapsed_sattributes carry telemetry.
- Return type:
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_argvas 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:
Read one JSON object from stdin:
{"preimage_hex", "target_hex", "nonce_width"}.Search for a valid nonce.
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 raisesMaxAttemptsErrorimmediately 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$PATHcan 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$PATHresolution.Verify the binary’s checksum against the upstream release before first use.
Run pyrxd in an environment where
$PATHis 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
MaxAttemptsErrorraised 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.FileNotFoundError –
miner_argv[0]is not on PATH.
- Return type:
- 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()ormine_solution_external()directly. The two paths share semantics — both return aDmintMineResultwith a nonce that satisfies the target — but have disjoint parameter sets (max_attemptsvstimeout_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): runmine_solution()in this process. Slow but correct. Use for tests, small examples, and contracts where mining takes < a minute.miner_argv is not None: invokemine_solution_external()with the supplied argv. The external miner (e.g.pyrxd.contrib.miner, a custom binary, orglyph-miner) runs as a subprocess and returns a verified nonce via the JSON-over-stdio protocol. The local re-verification inmine_solution_externalis 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 tosubprocess.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_sinstead).timeout_s (float) – Subprocess timeout on the external-miner path. Ignored in-process (use
max_attemptsthere).
- Returns:
DmintMineResultwith the verified nonce.- Raises:
MaxAttemptsError – in-process exhausted
max_attempts, or external miner exceededtimeout_s/ explicitly signalled exhaustion.ValidationError – external miner returned a malformed response or a nonce that fails local verification.
- Return type:
- 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 tominer_pkhfrom 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
heightbumped), value 1.Output 1: FT-wrapped reward output (value =
state.reward).Output 2: OP_RETURN (when
op_return_msgis set) — the output the PoW preimage binds (seebuild_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_timefromOP_TXLOCKTIMEand itstargetfrom the alt-stack DAA result on every mint, socurrent_timeIS the block locktime: it is written into the recreated state’slast_timeAND set as the txnLockTime(the two must agree), and for DAA modes it drives the target retarget.current_timemust 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 passesepoch_length/max_adjustment_log2orschedule(andhalf_lifefor 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:
Build the unsigned tx shell via this function.
Compute the real preimage AND scriptSig hashes via
build_pow_preimageonce 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).Mine for a valid
nonceviaverify_sha256d_solution(or the relevant algo).Replace input 0’s unlocking script with
build_mint_scriptsig(nonce, pow.input_hash, pow.output_hash). The two hashes MUST come from the samebuild_pow_preimagecall that produced the preimage the miner solved — splitting the sources is a silent on-chain rejection.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' * 8as 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_exhaustedis True;nonceis not 8 bytes;miner_pkhis not 20 bytes.- returns:
DmintMintResultwith the unsigned tx and updated state.
- Parameters:
- Return type:
- 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:
The contract input’s outpoint txid + the contract ref (so a nonce mined for one contract slot can’t be replayed against another)
The miner’s funding-input locking script (so the miner cannot substitute a different funding source after finding a nonce)
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 pushCallers feed
preimagetomine_solution()and passinput_hash+output_hashtobuild_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
Transactionfrombuild_dmint_mint_tx()— vout[2] is required to be the OP_RETURN msg output (mainnet-canonical 4-output shape).
- Returns:
PowPreimageResultcarrying the preimage and the two script hashes that the scriptSig must push for the covenant to accept the mint.- Raises:
ValidationError –
unsigned_txhas fewer than 4 outputs (no OP_RETURN at vout[2]) OR vout[2] is not actually an OP_RETURN script. Build the tx viabuild_dmint_mint_tx()with a non-emptyop_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:
- 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 ofbuild_mint_scriptsig()) and the absence of the Photonic-Walletop_return_msgconvention 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 pushUnlike the V1 helper, this function takes
output_scriptas 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 selectingoutput_scriptshould 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_preimageleft 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_v1MUST beFalse— 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:
PowPreimageResultwith 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: