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 ← estimate
(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.AttemptEstimate[source]¶
Bases:
objectEXACT attempt statistics for a target — closed form, no measurement.
- Parameters:
target – The target as supplied.
effective_target –
min(target, MAX_SHA256D_TARGET)— pyrxd’s verifier (verify_sha256d_solution()) clamps, so the estimate must too.clamped – True iff the supplied target exceeded the ceiling and was clamped. A dMint covenant does NOT clamp: it reads the target as a signed script number of at most 8 bytes, so a target above the ceiling is unreadable (every mint aborts) or negative (no hash meets it). No contract carrying one can be minted.
difficulty –
target_to_difficulty(effective_target).probability_per_attempt –
p = effective_target / 2**96.expected_attempts –
1 / p = 2**96 / effective_target.quantile_attempts –
(quantile, attempts)pairs;attemptsis the smallest n withP(hit within n) >= quantile.
- __init__(target, effective_target, clamped, difficulty, probability_per_attempt, expected_attempts, quantile_attempts)¶
- class pyrxd.glyph.dmint.DaaBytecodeVersion[source]¶
Bases:
IntEnumWhich GENERATION of ASERT/LWMA retarget bytecode a deployed contract carries.
This is orthogonal to the dMint contract format (V1 vs V2 state layout): every member here is a V2-format contract. The retarget formula baked into Part B changed upstream on 2026-06-19 (ASERT, Photonic
ed53cd41) and 2026-06-20 (LWMA, Photonicc90e6506); pyrxd did not follow until 2026-09-16, so contracts deployed by pyrxd in between carry the older formula and MUST keep being mined under it — a covenant’s bytecode is immutable, and a miner that recomputes the next target with the wrong formula produces a state the covenant rejects.V2— the current canonical fractional fixed-point retarget (_build_asert_daa_v2/_build_linear_daa_v2). Every new deploy.LEGACY— the integer power-of-2 ASERT stepper (_build_asert_daa_legacy) or the unity-gain LWMA WITH theOP_0 OP_MAXtimeDelta floor (_build_linear_daa_legacy); what pyrxd emitted from 2026-06-17 to 2026-09-15.LEGACY_LWMA_PREFLOOR— LWMA only: the unity-gain retarget WITHOUT the timeDelta floor (_build_linear_daa_legacy_prefloor), as pyrxd emitted on 2026-06-16 — the bytecode of the mainnet LWMA deploydea3beb9….
Detected from a contract’s code section by
pyrxd.glyph.dmint.builders.detect_daa_bytecode(); a contract matching no known template is REPORTED (UnrecognizedDaaBytecodeError), never guessed.- __new__(value)¶
- LEGACY_LWMA_PREFLOOR = 0¶
- LEGACY = 1¶
- V2 = 2¶
- class pyrxd.glyph.dmint.DaaMode[source]¶
Bases:
IntEnum- __new__(value)¶
- FIXED = 0¶
- EPOCH = 1¶
- ASERT = 2¶
- LWMA = 3¶
- SCHEDULE = 4¶
- class pyrxd.glyph.dmint.DetectedDaaBytecode[source]¶
Bases:
objectWhat
detect_daa_bytecode()found baked into a contract’s Part B.- Parameters:
daa_mode – The mode that was asked about (ASERT or LWMA).
version – Which retarget-formula generation the bytecode is.
daa_bytes – The exact DAA fragment that matched, byte-for-byte.
half_life – ASERT only — the half-life constant READ OUT OF the bytecode (
Nonefor LWMA, which bakes no parameter). This is the ground truth a miner must use; a caller-supplied value that differs from it produces a target the covenant rejects.
- __init__(daa_mode, version, daa_bytes, half_life=None)¶
- Parameters:
daa_mode (DaaMode)
version (DaaBytecodeVersion)
daa_bytes (bytes)
half_life (int | None)
- Return type:
None
- version: DaaBytecodeVersion¶
- 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’s
DmintPayloadtype (packages/lib/src/types.tsatbecf41a):daa?: { mode, targetBlockTime, halfLife?, asymptote?, // ASERT windowSize?, // LWMA epochLength?, maxAdjustment?, // EPOCH schedule?: { height, difficulty }[] } // SCHEDULEEvery optional key is emitted ONLY when set (non-zero / non-empty), so a payload that does not use one is byte-identical to what pyrxd emitted before the key existed (2026-09-16:
asymptote,epochLength,maxAdjustment,schedulewere added; FIXED/ASERT/LWMA payloads that do not set them are unchanged).scheduleentries are(height, difficulty)— difficulty, not target, is what the payload type declares.max_adjustmentis stored as the RAW payload number and is informational only: Photonic’s builder reads a payload value of 1..4 as the log2 shift count and 8/16 as multipliers (script.tsmaxAdjustmentToLog2; its Mint UI passes the same number to the payload and the builder), while pyrxd’sdeploy-dmint --max-adjustmentis a multiplier baked asDmintDeployParams.max_adjustment_log2. So the payload value alone does not say which clamp a contract bakes — only the contract bytecode does. Validation accepts exactly the values Photonic’s builder accepts (EPOCH_MAX_ADJUSTMENT_PAYLOAD_VALUES); pyrxd does not reinterpret them.- __init__(algo, num_contracts, max_height, reward, premine, diff, daa_mode=DaaMode.FIXED, target_block_time=60, half_life=0, window_size=0, asymptote=0, epoch_length=0, max_adjustment=0, schedule=())¶
- 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=240, height=0, last_time=0, epoch_length=2016, max_adjustment_log2=2, schedule=())¶
- Parameters:
- Return type:
None
- property initial_target: int¶
The deploy target for
difficulty—target_for_difficulty(), for everyalgo.BLAKE3 and K12 used to get
MAX_V2_TARGET_256 // difficultyhere: a target wider than the 8 bytes Part B2 can read, so the contract could never be minted.algoselects the hash opcode (and the algoId the state records); it does not change the target formula.
- 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 d0/d1/d2/d3/d8 — seepyrxd.glyph.script.REF_OPCODES). 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). On the final mint no output carries a state: this is the spent state with onlyheightadvanced tomax_height(sois_exhaustedis True).contract_script – The script of output 0: the recreated contract (state + separator + code), or on the final mint the burn
d8 <contractRef> 6athat replaces it.reward_script – The FT-wrapped reward output script (P2PKH, then
bd d0 <tokenRef>and the 12-byte FT code), paid to the miner.fee – Transaction fee in photons.
is_final_mint – True when this mint takes the contract to
max_height: output 0 is the burn, not a recreated contract, and no further mint of this contract is possible.
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, is_final_mint=False)¶
- 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:
- property next_mint_is_final: bool¶
True when the next mint takes this contract to
max_height.That mint recreates no contract: the covenant’s final branch requires the burn
d8 <contractRef> 6ain its place (seebuild_dmint_contract_burn_script()), andbuild_dmint_mint_tx()builds exactly that.
- 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.
max_height – Maximum mints per contract.
target – The PoW target (
MAX_SHA256D_TARGET // difficulty). The codescript is rebuilt with today’s builder, which pushes it minimally; a contract pyrxd deployed before 2026-09-23 at difficulty 256 or more carries a non-minimal 8-byte push instead, so this fast path does not find it (it can never be minted anyway).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.LiveMiningStats[source]¶
Bases:
objectProgress snapshot during a real grind: MEASURED rate, PROJECTED remainder.
observed_hashes_per_secondis measured — it is the aggregate rate the running miner actually achieved, so it needs no scaling assumption. The remaining-time fields are projections built on it.Because the process is memoryless, the remaining-work distribution is identical to the distribution at attempt zero:
attemptsalready burned do not reduceremaining_mean_s. This is a distribution, not a countdown.- Parameters:
attempts – Attempts completed so far.
elapsed_s – Seconds elapsed so far.
observed_hashes_per_second – MEASURED aggregate rate.
remaining_mean_s – PROJECTED mean seconds still to go (
Noneif no rate is observable yet).remaining_quantile_s – PROJECTED
(quantile, seconds)pairs.
- __init__(attempts, elapsed_s, observed_hashes_per_second, remaining_mean_s, remaining_quantile_s)¶
- class pyrxd.glyph.dmint.MintEtaProjection[source]¶
Bases:
objectPROJECTED wall-clock times. Every field is a projection.
aggregate_hashes_per_secondassumes linear scaling acrossworkers, which this library does not measure. Treat it as an upper bound: shared L3, SMT siblings, and power/thermal limits all pull real aggregate throughput belowsingle-core × workers.- Parameters:
workers – Worker count the projection assumes.
single_core_hashes_per_second – The per-core rate fed in (MEASURED if it came from
benchmark_sha256d(), otherwise whatever the caller supplied).aggregate_hashes_per_second –
single-core × workers— PROJECTED.mean_s –
E[attempts] / aggregate— PROJECTED.quantile_s –
(quantile, seconds)pairs — PROJECTED.
- __init__(workers, single_core_hashes_per_second, aggregate_hashes_per_second, mean_s, quantile_s)¶
- 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)¶
- class pyrxd.glyph.dmint.Sha256dBenchmark[source]¶
Bases:
objectMEASURED single-core SHA256d throughput on this machine.
Every field here was observed, not assumed. Nothing in it accounts for multiple cores — see
MintEtaProjectionfor that.- Parameters:
hashes – SHA256d evaluations actually performed.
elapsed_s – Wall-clock seconds they took.
hashes_per_second –
hashes / elapsed_s— MEASURED, single core.first_digest – The digest of nonce 0, produced by the benchmark’s own inner loop. Pinning it in a test is what stops a refactor from silently benchmarking a different hash chain.
- __init__(hashes, elapsed_s, hashes_per_second, first_digest)¶
- 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.attempts_for_quantile(target, quantile)[source]¶
Attempts needed to hit with probability
quantile. EXACT.The smallest
nwith1 - (1-p)**n >= quantile, i.e.ceil(ln(1-quantile) / ln(1-p)). Both logs go throughmath.log1p(), which keeps full precision at thep ~ 1e-10scale wherelog(1 - p)would round tolog(1.0) == 0and the division would blow up.- Parameters:
- Raises:
ValidationError –
target < 1orquantileout of range.- Return type:
- pyrxd.glyph.dmint.benchmark_sha256d(*, duration_s=0.5, preimage=b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./0123456789:;<=>?', nonce_width=8)[source]¶
Measure this machine’s single-core SHA256d rate. MEASURED.
Runs the identical hash chain the miner runs —
sha256(sha256(preimage + nonce).digest()).digest()over a little-endian nonce — inlined exactly as inpyrxd.contrib.miner.parallel._worker. Benchmarking anything else (a baresha256, a stand-in payload) would report a rate the miner never achieves.The first batch is deliberately one hash wide so
first_digestfalls out of the benchmark’s own loop rather than a parallel expression that could drift away from it.Single core only. Multiply by a worker count at your own risk — see
project_mint_eta().- Parameters:
duration_s (float) – Target wall-clock duration. The loop overshoots by at most one batch.
preimage (bytes) – 64-byte preimage to hash against.
nonce_width (int) – 4 (V1) or 8 (V2). Throughput is width-independent in practice; the parameter exists so the benchmark can mirror a specific contract exactly.
- Raises:
ValidationError –
duration_sis not positive,preimageis not 64 bytes,nonce_widthis not 4 or 8, or the clock did not advance.- Return type:
- 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_burn_script(contract_ref)[source]¶
The output a FINAL mint puts where the contract used to be:
d8 <contractRef> 6a.OP_PUSHINPUTREFSINGLETON <contractRef> OP_RETURN. On the mint whose new height equalsmax_height, the output-validation block (V2 Part C and the V1 epilogue carry the same bytes here) takes its IF branch instead of rebuilding the next state: it readsOP_OUTPUTBYTECODEat the contract’s output index andOP_EQUALVERIFYs it against0xd8 || contractRef || 0x6aassembled on the stack (5279cd01d853797e016a7e88). The ref is the CONTRACT ref read from the state, not the token ref. The prologue before that branch also requires the token ref to appear in exactly as many outputs as there are FT reward outputs, so the burn must not carry it.Nothing can spend the output (execution ends at
OP_RETURN), so the contract singleton ends here and no further mint of it is possible. Same bytes as Glyph-miner’sburnScript(src/glyph.ts).
- 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=None, 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(or, on the final mint, burns it: see below), 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 (the spent script with its state advanced:
height+ 1, and on V2 alsolastTimeandtargetas the note below says), 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.
The final mint (
height + 1 == max_height, flaggedis_final_minton the result) has the same inputs and the same outputs 1-3, but output 0 is the burnbuild_dmint_contract_burn_script()(d8 <contractRef> 6a) at value 0 instead of a recreated contract: the covenant’s final branch requires exactly that script at the contract’s output index, and requires the token ref to appear in the FT reward outputs only. The FT reward is unchanged. No state is rebuilt, so the checks that exist to protect the RECREATED contract — its 1-photon value, and for V2 alastTimeits next retarget can read and a target above 1 — are not applied to it. What the covenant still evaluates on this spend is checked as on any other mint: the PoW binding, the reward, and for V2 the Part B retarget, which runs on the final mint too (its result is discarded, but an input it cannot evaluate still fails the spend). The contract’s photon goes to change. Same output order and burn value as Glyph-miner’s final mint.Note
V2 supports all five DAA modes — FIXED, ASERT, LWMA, EPOCH, SCHEDULE (the canonical Photonic redesign). On every mint that recreates the contract, the covenant rebuilds the next state’s
last_timefromOP_TXLOCKTIMEand itstargetfrom the alt-stack DAA result, 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. The final mint recreates no state:current_timeis still set as thenLockTime, and Part B, which runs on the final mint too, still reads it in the modes whose retarget does, but nolast_timeortargetis written.current_timemust be<= 0x7FFFFFFFon every V2 mint (on a mint that recreates the contract, Part C’sNUM2BIN(_, 4)cannot write a locktime with bit 31 set; pyrxd applies the same bound to the final mint). On a mint that recreates the contract, for the modes whose retarget readslastTime(DAA_MODES_READING_LAST_TIME: ASERT, LWMA, EPOCH) it must also be>= 2**23, so the lastTime it writes is a minimally encoded script number a later retarget can read; the final mint writes no lastTime, and that lower bound is not applied to it. 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
Two generations of ASERT/LWMA retarget bytecode exist on chain. Upstream moved to the fractional ASERT-v2 / LWMA-v2 formulas on 2026-06-19/20 and pyrxd followed on 2026-09-16; contracts pyrxd deployed in between bake the legacy formulas. This builder reads the contract’s own bytecode (
detect_contract_daa_bytecode()) and recomputes the target with the matching mirror —compute_next_target_*_v2or*_legacy— then byte-verifies the suppliedhalf_lifeagainst the baked Part B for BOTH generations. A contract carrying bytecode of neither generation is refused (UnrecognizedDaaBytecodeError) rather than mined under a guessed formula.half_lifedefaults toNone— use the value detected in the contract. Detection has already read the baked half-life at that point, so requiring the caller to restate it could only refuse honest work: when the deploy-side default moved 3600 → 240 on 2026-09-16, every previously-working invocation against a 3600 contract started failing on a value the builder already knew. Supplyinghalf_lifeexplicitly still byte-verifies it against the baked Part B and fails fast, naming the baked value, if the two disagree.Note
Also refused before any PoW grind: a state whose
lastTimethis mint’s retarget reads and which is not a readable lastTime (that contract can no longer be minted at all); acurrent_timeearlier than the contract’slast_timeon a 2026-06-16 pre-floor LWMA contract; any mint whose recreated target would be 1 while the spent target is larger, in every mode; and any input the retarget mirror reports the contract’s int64 arithmetic cannot evaluate.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, 4-byte for V1 (use
b'\x00' * 8, or* 4, 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:
V2: Unix timestamp written as the tx
nLockTimeon every mint and, on a mint that recreates the contract, as the recreated state’slastTime(the covenant rebuilds it fromOP_TXLOCKTIME); drives the DAA retarget. The final mint writes nolastTime. V1: must be 0.- param fee_rate:
Photons per byte for fee calculation (default 10_000, the Radiant post-V2 relay minimum).
- raises ContractExhaustedError:
contract_utxo.state.is_exhaustedis True.- raises ValidationError:
nonceis not 8 bytes (4 for V1);miner_pkhis not 20 bytes.- returns:
DmintMintResultwith the unsigned tx and updated state.
- Parameters:
contract_utxo (DmintContractUtxo)
nonce (bytes)
miner_pkh (bytes)
current_time (int)
fee_rate (int)
funding_utxo (DmintMinerFundingUtxo | None)
op_return_msg (bytes | None)
half_life (int | None)
epoch_length (int | None)
max_adjustment_log2 (int | None)
- 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:
04 <height:4 LE> | d8 contractRef(36B) | d0 tokenRef(36B) | maxHeight | reward | target
the last three as minimal script-number pushes (
_push_minimal()). That is byte for byte what Photonic Wallet’s V1-eradMintScriptbuilds (its SHA256d/FIXED branch,packages/lib/src/script.tsat c8540a6:push4bytes(height), thenpushMinimalfor maxHeight, reward and target), andtests/test_dmint_v1_target_push.pychecks it against a transcription of that builder.The target has to be minimal. The V1 epilogue compares the target, exactly as pushed, with the proof-of-work number (
… 81 76 00 a2 69 a2 69: OP_BIN2NUM normalises the hash window, never the target), and Radiant reads that operand as a script number with minimal encoding required. The minimal push is 8 bytes only for a target of at least2**55(difficulty 255 or less); below that it is shorter, down to OP_1..OP_16 for targets 1..16. Mainnet V1 contracts carry 6-, 7- and 8-byte targets, and two pinned intests/test_dmint_v1_target_push.pyhave been minted over a thousand times each with a 7-byte one. Until 2026-09-23 pyrxd pushed every target as08+ 8 bytes, which is not minimal below2**55, so no V1 contract it deployed at difficulty 256 or more can ever be minted (pyrxd.glyph.dmint.miner._unreadable_target_reason()refuses them).- 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. A target of 0 is refused by pyrxd: only a hash whose compared 8 bytes are all zero meets it.- 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)¶
LEGACY ASERT target — the integer power-of-2 stepper baked into pre-resync contracts.
Off-chain mirror of
pyrxd.glyph.dmint.builders._build_asert_daa_legacy(), the ASERT bytecode pyrxd emitted before 2026-09-16 (upstream replaced it with ASERT-v2 on 2026-06-19,ed53cd41). A contract deployed under it MUST keep being mined with THIS formula — the covenant recomputes the next target with the bytecode it was born with, and rejects any other value.build_dmint_mint_txdispatches here whendetect_contract_daa_bytecode()reportsDaaBytecodeVersion.LEGACY; new deploys usecompute_next_target_asert_v2().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).Both subtractions go through
_script_int64(), as in the v2 mirror: for atarget_timenear the int64 limit the on-chain OP_SUB aborts, so this raisesValidationErrorrather than return a target the contract cannot compute.Note
V2-only DAA. V1 has no DAA (fixed difficulty).
- pyrxd.glyph.dmint.compute_next_target_asert_legacy(current_target, last_time, current_time, target_time, half_life)[source]¶
LEGACY ASERT target — the integer power-of-2 stepper baked into pre-resync contracts.
Off-chain mirror of
pyrxd.glyph.dmint.builders._build_asert_daa_legacy(), the ASERT bytecode pyrxd emitted before 2026-09-16 (upstream replaced it with ASERT-v2 on 2026-06-19,ed53cd41). A contract deployed under it MUST keep being mined with THIS formula — the covenant recomputes the next target with the bytecode it was born with, and rejects any other value.build_dmint_mint_txdispatches here whendetect_contract_daa_bytecode()reportsDaaBytecodeVersion.LEGACY; new deploys usecompute_next_target_asert_v2().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).Both subtractions go through
_script_int64(), as in the v2 mirror: for atarget_timenear the int64 limit the on-chain OP_SUB aborts, so this raisesValidationErrorrather than return a target the contract cannot compute.Note
V2-only DAA. V1 has no DAA (fixed difficulty).
- pyrxd.glyph.dmint.compute_next_target_asert_v2(current_target, last_time, current_time, target_time, half_life)[source]¶
ASERT-v2 next target — the fractional, symmetric, damped retarget of CURRENT deploys.
Off-chain mirror of
pyrxd.glyph.dmint.builders._build_asert_daa_v2(), which is the canonical PhotonicbuildAsertDaaBytecode(script.tsatbecf41a), and an exact port of the referencecomputeAsertV2Targetinpackages/lib/src/dmintDaaV2.ts(same commit) — bigint arithmetic there, so the only semantic to carry over is that BigInt and OP_DIV both truncate toward zero:excess = (current_time - last_time) - target_time driftFp = trunc((excess * RADIX) / half_life) # RADIX = 2^16, signed driftFp = clamp(driftFp, -RADIX/4, +RADIX/4) # ±25%/mint damping t = min(current_target, MAX_TARGET/4) # difficulty floor 4 new = clamp(t + (t / RADIX) * driftFp, 1, MAX_TARGET/4)
_trunc_divis load-bearing: OP_DIV truncates toward zero while Python//floors, and they differ for the negativeexcessof an early block (e.g.excess = -1, half_life = 240→ trunc(-65536/240) = -273; floor gives -274). A floor here would recreate a target the covenant does not, and the mint is rejected.int64 bounds, every intermediate (the covenant aborts, not wraps, outside
[-(2^63-1), 2^63-1]; this mirror raisesValidationErrorat the same points via_script_int64()so it never predicts an unmineable state):timeDelta = current_time - last_time: for an ASERT/LWMA mintbuild_dmint_mint_txrequires bothcurrent_timeand the state’slast_timeto be readable lastTimes (is_readable_last_time:[2^23, 2^31 - 1]), so|timeDelta| <= 2^31 - 1in either direction.excess = timeDelta - target_time:target_timeis a state script number in[1, 2^63 - 1], soexcessis in[-(2^63 - 1) - (2^31 - 1), 2^31 - 2]and the OP_SUB underflows only fortarget_time > 2^63 - 1 + timeDelta— checked.excess * RADIX:|excess| <= 2^47 - 1⇒|excess * 2^16| <= 2^63 - 2^16, representable;|excess| >= 2^47⇒|product| >= 2^63, which OP_MUL aborts. Positive excess never reaches it (excess <= 2^31 - 2); negative excess reaches it only fortarget_time >= 2^47 + timeDelta(≈ 1.4e14 s ≈ 4.5 Myr) — checked. Photonic’s proof (|excess| <= ~4.3e9→|product| <= ~2.8e14 < 2^63) is the realistic-deploy case of the same bound.driftFp = trunc(excess*RADIX / half_life):|driftFp| <= |excess*RADIX|sincehalf_life >= 1; then clamped to|driftFp| <= 2^14.t = min(current_target, MAX/4) <= 2^61 - 1;t / RADIX <= 2^45 - 1.delta = (t/RADIX) * driftFp:|delta| < 2^45 * 2^14 = 2^59— cannot overflow.t + delta < 2^61 + 2^59 < 2^63— cannot overflow.
So the only reachable abort is the
excesschain for an absurdtarget_time, and it is reported rather than mis-predicted.Note
V2-only DAA.
current_targetmust itself be a valid script number (1..MAX_SHA256D_TARGET): the covenant compares it with CScriptNum ops, so a target wider than 8 bytes cannot run through any DAA on chain — for any algorithm.
- 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).
clampedDelta >= target_time >> N >= 0and the divide’s operands are positive, so floor == OP_DIV’s truncate-toward-zero. The lower bound is 0 whentarget_time < 2**N; pyrxd refuses to deploy such a contract, andbuild_dmint_mint_txrefuses any mint whose next target would be 1. The OP_SUB and the N x OP_2MUL oftarget_timego through_script_int64(), so an input the contract cannot evaluate raisesValidationError. 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)¶
LEGACY LWMA target — the unity-gain single-sample retarget of pre-resync contracts.
Off-chain mirror of
pyrxd.glyph.dmint.builders._build_linear_daa_legacy()(with theOP_0 OP_MAXtimeDelta floor) AND of_build_linear_daa_legacy_prefloor()(without it) for everycurrent_time >= last_time. On a negative delta the two variants differ; this mirror gives the floored variant’s answer, andbuild_dmint_mint_txdoes not build a backwards mint of a pre-floor contract. This is the LWMA bytecode pyrxd emitted before 2026-09-16 (upstream replaced it with LWMA-v2 on 2026-06-20,c90e6506); the mainnet LWMA deploydea3beb9…carries the pre-floor variant and is mined with this formula. New deploys usecompute_next_target_linear_v2().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.The two operations the bytecode can abort on — the OP_SUB of the times and the
OP_4 OP_MULoftarget_time— go through_script_int64(), so an input the contract cannot evaluate raisesValidationErrorinstead of returning a target.Note
V2-only DAA.
- pyrxd.glyph.dmint.compute_next_target_linear_legacy(current_target, last_time, current_time, target_time)[source]¶
LEGACY LWMA target — the unity-gain single-sample retarget of pre-resync contracts.
Off-chain mirror of
pyrxd.glyph.dmint.builders._build_linear_daa_legacy()(with theOP_0 OP_MAXtimeDelta floor) AND of_build_linear_daa_legacy_prefloor()(without it) for everycurrent_time >= last_time. On a negative delta the two variants differ; this mirror gives the floored variant’s answer, andbuild_dmint_mint_txdoes not build a backwards mint of a pre-floor contract. This is the LWMA bytecode pyrxd emitted before 2026-09-16 (upstream replaced it with LWMA-v2 on 2026-06-20,c90e6506); the mainnet LWMA deploydea3beb9…carries the pre-floor variant and is mined with this formula. New deploys usecompute_next_target_linear_v2().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.The two operations the bytecode can abort on — the OP_SUB of the times and the
OP_4 OP_MULoftarget_time— go through_script_int64(), so an input the contract cannot evaluate raisesValidationErrorinstead of returning a target.Note
V2-only DAA.
- pyrxd.glyph.dmint.compute_next_target_linear_v2(current_target, last_time, current_time, target_time)[source]¶
LWMA-v2 next target — the damped fractional retarget of CURRENT LWMA deploys.
Off-chain mirror of
pyrxd.glyph.dmint.builders._build_linear_daa_v2()(canonical PhotonicbuildLinearDaaBytecodeatbecf41a) and a port ofcomputeLwmaV2Targetinpackages/lib/src/dmintDaaV2.ts: exactlycompute_next_target_asert_v2()with the responsiveness gain auto-set totarget_time—driftFp = (excess * RADIX) / target_time— so a block twice the target time hits the ±25% clamp. Same int64 proof (target_time >= 1is deploy-enforced, so the OP_DIV never divides by zero).The old unity-gain LWMA (
compute_next_target_linear_legacy()) could jump difficulty 4× in one block and slam the target to 1 on a zero-delta block; this caps each step at ±25% and is stable on-target.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.detect_contract_daa_bytecode(contract_script)[source]¶
Which ASERT/LWMA retarget-formula generation a V2 dMint contract script bakes.
Parses the state (for the declared
daa_modeand the state/code boundary) and classifies the code section withpyrxd.glyph.dmint.builders.detect_daa_bytecode(). For ASERT the result also carries thehalf_liferead out of the bytecode — the value a miner MUST use.- Raises:
ValidationError – not a V2 contract script, or its DAA mode is not ASERT/LWMA (FIXED/EPOCH/SCHEDULE have a single generation;
build_dmint_mint_txbyte-verifies those by rebuilding Part B from the deploy parameters).UnrecognizedDaaBytecodeError – the fragment matches no known generation.
- Parameters:
contract_script (bytes)
- Return type:
- pyrxd.glyph.dmint.detect_daa_bytecode(code, daa_mode)[source]¶
Classify which ASERT/LWMA retarget-formula generation a V2 contract bakes.
codeis the contract’s code section — the bytes AFTER the0xbdOP_STATESEPARATOR (whatbuild_dmint_code_scriptreturns and what follows the parsed state in a UTXO script).daa_modeis the mode the state script declares (item 6); this function checks the CODE agrees with it.Recognition is rebuild-and-compare, not a prefix sniff: the half-life constant is read out of the bytecode, the candidate template is rebuilt from it with the same builder a deploy would use, and the whole fragment (plus the Part B4 that must follow it) has to match byte-for-byte. So a contract is classified only if pyrxd can reproduce its exact bytes, which is also the condition under which the mint builder can recompute its target. The discriminators, verified against the bytes the builders emit (
tests/test_dmint_daa_v2_resync.py):ASERT: after the 7-byte
excesspreamblec5 5279 94 5379 94, v2 has<RADIX push> OP_MUL=03000001 95; legacy has<halfLife push> OP_DIV(5th byte96, never95— even for halfLife 65536).LWMA: v2 shares the preamble and signature, then
5379 96(OP_3 PICK OP_DIV) as the gain divisor; legacy has noexcessat all —c5 5279 94 5379 54 95 a3(OP_4 OP_MUL OP_MIN), with (LEGACY) or without (LEGACY_LWMA_PREFLOOR) the00 a4timeDelta floor.
Mirrors Photonic’s Glyph-miner
extractDaaParamsFromCodeScriptin intent, with one deliberate difference from the Radiant-Core fork this was ported from: that fork’s_asert_version_of_codeclassified ANY fragment lacking the v2 signature as legacy (return 2 if sig else 1), so a corrupt or unknown contract would be mined under a guessed formula. Here an unrecognised fragment is REPORTED withUnrecognizedDaaBytecodeError(naming the bytes).- Raises:
ValueError –
daa_modeis not ASERT or LWMA — FIXED/EPOCH/SCHEDULE have a single generation and are byte-verified by rebuilding Part B from the deploy parameters instead.UnrecognizedDaaBytecodeError – the code is not a V2 dMint template, or its DAA fragment matches no known generation of
daa_mode.
- Parameters:
- Return type:
- pyrxd.glyph.dmint.difficulty_to_target(difficulty, algo=DmintAlgo.SHA256D)[source]¶
Convert difficulty to PoW target:
MAX_SHA256D_TARGET // difficultyfor everyalgo.algois accepted for backward compatibility and does not change the result — seetarget_for_difficulty(), the single definition this andDmintDeployParams.initial_targetshare. BLAKE3/K12 once got(2**256 - 1) // difficultyhere, a target no dMint covenant can read.
- pyrxd.glyph.dmint.estimate_attempts(target, *, quantiles=(0.5, 0.9, 0.99))[source]¶
Closed-form attempt statistics for a dMint target. EXACT.
p = target / 2**96andE[attempts] = 2**96 / target— derived from the verifier predicate, not estimated. See the module docstring for the derivation and for why the difficulty multiplier (MAX_SHA256D_TARGET / target) is not an attempt count.Valid for V1 and V2 alike: rerolling a V1 preimage after a 2**32-nonce sweep re-partitions the same i.i.d. hash stream, which leaves the total-attempts distribution untouched.
- Parameters:
- Raises:
ValidationError –
target < 1or a quantile is out of range.- Return type:
>>> from pyrxd.glyph.dmint import difficulty_to_target, estimate_attempts >>> est = estimate_attempts(difficulty_to_target(1)) >>> round(est.expected_attempts) 8589934592
- 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_minimal_4byte_scriptnum(n)[source]¶
Is
n’s FIXED 4-byte little-endian push a minimally encodedCScriptNum?The V2 dMint state script pushes
lastTimeas a fixed04 <4B LE>(seebuilders._push_4bytes_le), and the ASERT/LWMA/EPOCH retarget fragments read that item back as a NUMBER (OP_2 OP_PICK; OP_SUB— seebuilders._V2_EXCESS_PREAMBLE). Radiant-Core builds the operand withCScriptNum(vch, fRequireMinimal=true)becauseSCRIPT_VERIFY_MINIMALDATAis inMANDATORY_SCRIPT_VERIFY_FLAGS(policy.h) — so this is CONSENSUS, not mempool policy, and a non-minimal operand aborts the script rather than being merely non-standard.The rule, transcribed from
CScriptNum’s constructor: the encoding is minimal unless the most-significant byte has nothing but the sign bit, with one exception — if the second-most-significant byte already has its high bit set, the extra byte is carrying the sign and IS minimal (this is how+255encodes asff00).Over
[0, 0x7FFFFFFF]— the locktimes Part C’sNUM2BIN(_, 4)can write back — this is exactlyn >= 2**23; that equality is not hard-coded here, it is derived by this predicate and pinned intests/test_dmint_daa_v2_resync.py. The 2026-09-21 review measured the same boundary against a real radiant-core node:last_time=8388608accepted,8388607rejected withmandatory-script-verify-flag-failed.This answers ONLY the encoding question. A value with bit 31 set (
0x80800000,0xFFFFFFFF) can be minimally encoded and still not meann: its top bit is the script-number SIGN, so the covenant reads it as a negative number whileDmintStateparses it unsigned. To ask whether a lastTime reads back as the number pyrxd wrote, useis_readable_last_time().
- pyrxd.glyph.dmint.is_readable_last_time(n)[source]¶
Does a V2 state’s fixed 4-byte
lastTimepush read back, as a number, asn?True exactly when the push is minimally encoded (
is_minimal_4byte_scriptnum()) AND bit 31 is clear, i.e.2**23 <= n <= 0x7FFFFFFF. Below2**23the retarget’sCScriptNumread aborts the script (MINIMALDATA is consensus on Radiant); with bit 31 set the covenant reads a negative number that pyrxd’s off-chain mirrors, which parse the state unsigned, would not reproduce. Both the deploy guard (pyrxd.glyph.builder.require_mineable_last_time) and the mint builder (build_dmint_mint_tx) refuse on this predicate, so pyrxd never writes a lastTime a later retarget reads differently from — or cannot read at all.
- 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
pyrxd.glyph.script.REF_OPCODES— 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.live_stats(attempts, elapsed_s, estimate)[source]¶
Snapshot a running grind: MEASURED rate + PROJECTED remaining time.
Unlike
project_mint_eta(), the aggregate rate here is measured — it is what the miner actually achieved overelapsed_s— so no scaling assumption is involved. The remaining-time fields are still projections, and they do not shrink asattemptsgrows: the geometric distribution is memoryless, so the work remaining after a billion misses is distributed exactly as it was at attempt zero.- Parameters:
attempts (int) – Attempts completed so far (0 is allowed).
elapsed_s (float) – Seconds elapsed so far (0 is allowed; then no rate is observable and the remaining fields are empty).
estimate (AttemptEstimate) – From
estimate_attempts().
- Raises:
ValidationError – negative
attemptsorelapsed_s.- Return type:
- pyrxd.glyph.dmint.mine_solution(preimage, target, *, algo=DmintAlgo.SHA256D, nonce_width=4, max_attempts=600000000, progress=None, progress_interval_s=0.5)[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. SeeDEFAULT_MAX_ATTEMPTS— it is a fail-fast cap in attempts, not a wall-clock budget.progress (Callable[[int, float], None] | None) – Optional
callback(attempts, elapsed_s)invoked roughly everyprogress_interval_swhile grinding. Feed the pair tolive_stats()for an observed rate + remaining-time quantiles. Exceptions raised by the callback propagate to the caller — which is a supported way to impose a deadline on the grind.progress_interval_s (float) – Minimum seconds between callbacks. Checked at a 65536-attempt granularity, so a very slow machine may report less often than requested.
- Raises:
ValidationError –
preimageis not 64 bytes,targetis not positive,nonce_widthis not 4 or 8,max_attemptsis < 1, orprogress_interval_sis not positive.NotImplementedError –
algois BLAKE3 or K12.MaxAttemptsError – No solution found within
max_attemptsiterations. The exception’sattemptsandelapsed_sattributes carry telemetry.
- Return type:
Note
There is no “easy” target for this loop. The verifier requires four leading zero bytes, so the mean is
2**96 / targetand floors at2**33 ≈ 8.6e9attempts even at difficulty 1 — lowering the difficulty cannot bring a run under that. (An earlier version of this docstring claimed a shifted target gave “~1 in 256 expected”; it confused the difficulty multiplier for an attempt count, and no such example completes in milliseconds.) Size a run withestimate_attempts()before starting it, and usebenchmark_sha256d()(orpyrxd glyph dmint-estimate) to turn that into wall clock.Usage:
from pyrxd.glyph.dmint import estimate_attempts, mine_solution est = estimate_attempts(target) # EXACT: mean + quantiles result = mine_solution( preimage, target, nonce_width=4, max_attempts=est.quantile_attempts[-1][1], # e.g. the p99 budget progress=lambda attempts, elapsed: ..., # live rate + ETA )
- pyrxd.glyph.dmint.mine_solution_dispatch(preimage, target, *, nonce_width=4, algo=DmintAlgo.SHA256D, miner_argv=None, max_attempts=600000000, timeout_s=600.0, progress=None, progress_interval_s=0.5)[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) – The contract’s PoW algorithm. Only SHA256D can be mined: BLAKE3 and K12 raise
NotImplementedErroron BOTH paths, before any hashing. (This used to be ignored on the external-miner path, whose protocol carries no algorithm field and whose answer is re-verified with SHA256d — so a BLAKE3/K12 caller got a SHA256d grind, not a refusal.)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).progress (Callable[[int, float], None] | None) – Live-progress hook. On the in-process path this is
mine_solution()’s own callback. On the external-miner path it is passed tomine_solution_external(), which streams a miner’s optional stderr progress frames if it emits any (added after 0.13.0) — an external miner that doesn’t know about progress frames simply never triggers the callback, same asprogress=None.progress_interval_s (float) – Minimum seconds between
progresscalls.
- 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.
NotImplementedError –
algois BLAKE3 or K12 (either path).
- Return type:
- pyrxd.glyph.dmint.mine_solution_external(preimage, target, *, miner_argv, nonce_width=4, timeout_s=600.0, progress=None, progress_interval_s=0.5, algo=DmintAlgo.SHA256D)[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).OPTIONALLY (added after 0.13.0) write zero or more progress lines to stderr while it searches:
{"progress": {"attempts": N, "elapsed_s": F}}, one JSON object per line. Purely additive — a miner that has never heard of this writes nothing extra, and nothing here changes for it.
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.progress (Callable[[int, float], None] | None) – Optional
callback(attempts, elapsed_s). WhenNone(the default), behavior is byte-for-byte identical to before this parameter existed: a single blockingsubprocess.runcall with stderr fully discarded (subprocess.DEVNULL), same as pyrxd <= 0.13.0. Passing a callback opts into a streaming invocation that reads (rather than discards) stderr, parses any progress frames the miner chooses to emit, and callsprogresswith the most recently observed one roughly everyprogress_interval_s— the same cadence contractmine_solution()documents. An external miner that never emits a progress frame (the common case today) simply meansprogressis never called; the grind still runs to completion or timeout exactly as it would withprogress=None. A raising callback propagates and the subprocess is terminated — the supported way to impose a deadline, mirroringmine_solution()andpyrxd.contrib.miner.parallel.mine.progress_interval_s (float) – Minimum seconds between
progresscalls. Ignored whenprogressisNone.algo (DmintAlgo) – The contract’s PoW algorithm. The protocol carries no algorithm field and the answer is re-verified with SHA256d, so anything but SHA256D is refused before the miner is spawned. Pass the contract’s own
state.algo; the default keeps older callers working.
- Raises:
ValidationError – The miner returned a malformed JSON response, a nonce of wrong width, or a nonce that fails local verification.
NotImplementedError –
algois BLAKE3 or K12.MaxAttemptsError – The miner exceeded
timeout_s.FileNotFoundError –
miner_argv[0]is not on PATH.
- Return type:
- pyrxd.glyph.dmint.project_mint_eta(estimate, *, single_core_hashes_per_second, workers=1)[source]¶
Turn EXACT attempt counts into PROJECTED wall-clock times.
Two separate reasons this is a projection and not a measurement:
aggregate = single_core × workersassumes linear cross-core scaling. pyrxd does not measure that, and real machines do not achieve it.Even given a perfect rate, the completion time is a random variable.
mean_sis its mean; the quantiles bracket it. There is no single “time remaining”.
- Parameters:
estimate (AttemptEstimate) – From
estimate_attempts().single_core_hashes_per_second (float) – Per-core rate (from
benchmark_sha256d(), or supplied).workers (int) – Worker count to project across.
- Raises:
ValidationError – non-positive rate or
workers < 1.- Return type:
- pyrxd.glyph.dmint.target_to_difficulty(target, algo=DmintAlgo.SHA256D)[source]¶
Convert PoW target to difficulty (approximate):
MAX_SHA256D_TARGET // target.The inverse of
difficulty_to_target(), and like it independent ofalgo(accepted for backward compatibility only).
- 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 ← estimate
(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 (37 — every module-level name defined here, so the count is checkable rather than
decorative, and tests/test_dmint_deploy_bounds.py checks it and the list below against the
module’s own definitions; it read “20” while listing 17 before 2026-09-22, and “31” while
listing 32 on 2026-09-23):
V2UnvalidatedWarning, MAX_SHA256D_TARGET, MAX_V2_TARGET_256, MAX_SCRIPT_NUM_BYTES, MAX_SCRIPT_NUM, MAX_V1_MAX_HEIGHT, MAX_V2_TARGET_TIME, target_for_difficulty, _refuse_above, check_dmint_core_bounds, check_dmint_v1_bounds, check_v2_numeric_bounds, EPOCH_MAX_ADJUSTMENT_LOG2_VALUES, EPOCH_MAX_ADJUSTMENT_PAYLOAD_VALUES, EPOCH_MAX_SAFE_TARGET, SCHEDULE_MAX_ENTRIES, ASERT_V2_RADIX, ASERT_V2_DRIFT_CLAMP, ASERT_V2_MAX_TARGET_DIV4, DEFAULT_ASERT_HALFLIFE, DmintAlgo, DaaMode, DaaBytecodeVersion, _OP_STATESEPARATOR, _PART_B1, _PART_B2, _PART_B4, is_minimal_4byte_scriptnum, is_readable_last_time, DAA_MODES_READING_DEPLOY_LAST_TIME, DAA_MODES_READING_LAST_TIME, DAA_MODES_READING_TARGET_TIME, DmintDeployParams, DmintCborPayload, _schedule_from_cbor, 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.
- pyrxd.glyph.dmint.types.MAX_SCRIPT_NUM_BYTES = 8¶
CScriptNumrefuses an operand longer thanMAXIMUM_ELEMENT_SIZE_64_BIT= 8 bytes (Radiant Corescript.h, vendored attests/vendor/radiant_core/script.h), so the largest script number is2**63 - 1. Every number a dMint covenant reads as one — the state’s height, maxHeight, reward and target on every mint, targetTime in ASERT/LWMA/EPOCH, and the constants its DAA fragment bakes — has to fit, or the script aborts.- Type:
The widest numeric operand Radiant’s interpreter reads
- pyrxd.glyph.dmint.types.MAX_V1_MAX_HEIGHT = 2147483648¶
2**31. A V1 contract stores its height as a 4-byte field, and every mint but the last writes the next one as04 || NUM2BIN(height + 1, 4)(epilogue54 78 54 80 7e).OP_NUM2BINaborts withIMPOSSIBLE_ENCODINGwhen the minimal encoding of the number is longer than the size asked for (Radiant Coreinterpreter.cpp, vendored attests/vendor/radiant_core/), and2**31needs five bytes. The last mint, the one whose new height equalsmax_height, takes the epilogue’s other branch and writes no height, so a contract withmax_height = 2**31makes its last mint from height2**31 - 1. With a largermax_heightthe mint from2**31 - 1is not the last, needsNUM2BIN(2**31, 4), and aborts, so the contract can never be minted past that height. V2 pushes its height minimally and has no such limit.- Type:
The largest
max_heightwhose last mint a V1 contract can reach
- pyrxd.glyph.dmint.types.MAX_V2_TARGET_TIME = 4294967295¶
Upper bound on a V2 deploy’s
target_timein the modes whose retarget reads it as a number (DAA_MODES_READING_TARGET_TIME: ASERT, LWMA, EPOCH). Theretarget_timeis a spacing in seconds that the retarget compares with the difference of two timestamps, and in any mint the covenant accepts both are below2**31: the state’slastTimeis a 4-byte signed script number, and the mint’s own time must fit the same 4-byte field, since Part C writes it into the next state withNUM2BIN(…, 4). So no mint observes a spacing of2**31seconds or more. The cap is looser than that, at0xFFFFFFFF: every value it refuses is a spacing no mint can meet, and the values between2**31and the cap, which no mint can meet either, still deploy (narrowing it is not needed for the int64 bound below). The bound is NOT the int64 abort point, which is further out (ASERT and LWMA multiply(timeDelta - targetTime)by 2**16, which aborts the script once the difference reaches 2**47; EPOCH doublestargetTimeup to four times), and it keeps every intermediate of those retargets inside int64 for any pair of such timestamps. FIXED and SCHEDULE never readtargetTimeas a number, so this bound does not apply to them. (Photonic’s Mint form offers 10..3600;tests/test_dmint_deploy_bounds.pypins the values mainnet deploys use.)
- 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.DaaBytecodeVersion[source]¶
Bases:
IntEnumWhich GENERATION of ASERT/LWMA retarget bytecode a deployed contract carries.
This is orthogonal to the dMint contract format (V1 vs V2 state layout): every member here is a V2-format contract. The retarget formula baked into Part B changed upstream on 2026-06-19 (ASERT, Photonic
ed53cd41) and 2026-06-20 (LWMA, Photonicc90e6506); pyrxd did not follow until 2026-09-16, so contracts deployed by pyrxd in between carry the older formula and MUST keep being mined under it — a covenant’s bytecode is immutable, and a miner that recomputes the next target with the wrong formula produces a state the covenant rejects.V2— the current canonical fractional fixed-point retarget (_build_asert_daa_v2/_build_linear_daa_v2). Every new deploy.LEGACY— the integer power-of-2 ASERT stepper (_build_asert_daa_legacy) or the unity-gain LWMA WITH theOP_0 OP_MAXtimeDelta floor (_build_linear_daa_legacy); what pyrxd emitted from 2026-06-17 to 2026-09-15.LEGACY_LWMA_PREFLOOR— LWMA only: the unity-gain retarget WITHOUT the timeDelta floor (_build_linear_daa_legacy_prefloor), as pyrxd emitted on 2026-06-16 — the bytecode of the mainnet LWMA deploydea3beb9….
Detected from a contract’s code section by
pyrxd.glyph.dmint.builders.detect_daa_bytecode(); a contract matching no known template is REPORTED (UnrecognizedDaaBytecodeError), never guessed.- LEGACY_LWMA_PREFLOOR = 0¶
- LEGACY = 1¶
- V2 = 2¶
- __new__(value)¶
- pyrxd.glyph.dmint.types.is_minimal_4byte_scriptnum(n)[source]¶
Is
n’s FIXED 4-byte little-endian push a minimally encodedCScriptNum?The V2 dMint state script pushes
lastTimeas a fixed04 <4B LE>(seebuilders._push_4bytes_le), and the ASERT/LWMA/EPOCH retarget fragments read that item back as a NUMBER (OP_2 OP_PICK; OP_SUB— seebuilders._V2_EXCESS_PREAMBLE). Radiant-Core builds the operand withCScriptNum(vch, fRequireMinimal=true)becauseSCRIPT_VERIFY_MINIMALDATAis inMANDATORY_SCRIPT_VERIFY_FLAGS(policy.h) — so this is CONSENSUS, not mempool policy, and a non-minimal operand aborts the script rather than being merely non-standard.The rule, transcribed from
CScriptNum’s constructor: the encoding is minimal unless the most-significant byte has nothing but the sign bit, with one exception — if the second-most-significant byte already has its high bit set, the extra byte is carrying the sign and IS minimal (this is how+255encodes asff00).Over
[0, 0x7FFFFFFF]— the locktimes Part C’sNUM2BIN(_, 4)can write back — this is exactlyn >= 2**23; that equality is not hard-coded here, it is derived by this predicate and pinned intests/test_dmint_daa_v2_resync.py. The 2026-09-21 review measured the same boundary against a real radiant-core node:last_time=8388608accepted,8388607rejected withmandatory-script-verify-flag-failed.This answers ONLY the encoding question. A value with bit 31 set (
0x80800000,0xFFFFFFFF) can be minimally encoded and still not meann: its top bit is the script-number SIGN, so the covenant reads it as a negative number whileDmintStateparses it unsigned. To ask whether a lastTime reads back as the number pyrxd wrote, useis_readable_last_time().
- pyrxd.glyph.dmint.types.is_readable_last_time(n)[source]¶
Does a V2 state’s fixed 4-byte
lastTimepush read back, as a number, asn?True exactly when the push is minimally encoded (
is_minimal_4byte_scriptnum()) AND bit 31 is clear, i.e.2**23 <= n <= 0x7FFFFFFF. Below2**23the retarget’sCScriptNumread aborts the script (MINIMALDATA is consensus on Radiant); with bit 31 set the covenant reads a negative number that pyrxd’s off-chain mirrors, which parse the state unsigned, would not reproduce. Both the deploy guard (pyrxd.glyph.builder.require_mineable_last_time) and the mint builder (build_dmint_mint_tx) refuse on this predicate, so pyrxd never writes a lastTime a later retarget reads differently from — or cannot read at all.
- pyrxd.glyph.dmint.types.DAA_MODES_READING_DEPLOY_LAST_TIME = frozenset({DaaMode.ASERT, DaaMode.LWMA})¶
The DAA modes whose retarget fragment reads
lastTimeon the first mint, i.e. while the state still carries the value chosen at deploy.ASERT and LWMA open their fragment with the unconditional “excess” preamble (
OP_TXLOCKTIME OP_2 OP_PICK OP_SUB …), so the very first mint — and every mint after it — constructs aCScriptNumfrom the state’slastTime. EPOCH reads it only inside a branch gated onheight > 0 and height % epochLength == 0, which height 0 never takes; SCHEDULE/FIXED never read it at all. So the value chosen AT DEPLOY is never read by those three: every state above height 0 carries thelastTimeits own mint wrote. (That second value IS read by EPOCH at each boundary — seeDAA_MODES_READING_LAST_TIME, which the mint builder guards.)This membership is DERIVED from the emitted bytecode and checked against this constant in
tests/test_dmint_daa_v2_resync.py(a mode whose fragment starts readinglastTimeunconditionally must appear here, or that test fails).
- pyrxd.glyph.dmint.types.DAA_MODES_READING_LAST_TIME = frozenset({DaaMode.EPOCH, DaaMode.ASERT, DaaMode.LWMA})¶
The DAA modes whose retarget fragment reads
lastTimeas a number on SOME mint — so alastTimea mint WRITES for one of these can be read by a later mint.ASERT and LWMA read it on every mint; EPOCH reads it on every epoch-boundary mint (a state at
height > 0withheight % epochLength == 0). SCHEDULE and FIXED never read it.build_dmint_mint_txrefuses to write an unreadablelastTime(is_readable_last_time()) for these modes. Membership is DERIVED by running every generation of every mode’s fragment under the int64/MINIMALDATA evaluator intests/test_dmint_daa_offchain_onchain_differential.pywith an unreadable lastTime, and checked against this constant there.
- pyrxd.glyph.dmint.types.DAA_MODES_READING_TARGET_TIME = frozenset({DaaMode.EPOCH, DaaMode.ASERT, DaaMode.LWMA})¶
The DAA modes whose retarget fragment reads the state’s
targetTimeas a number.ASERT and LWMA subtract it from the observed spacing; EPOCH scales and divides by it at an epoch boundary. FIXED has no fragment and SCHEDULE’s reads only the height, so for those two
targetTimeis only ever carried as bytes: Part A never picks it, Part B4 drops it withOP_DROP, and Part C copies it inside the baked middle literal.MAX_V2_TARGET_TIMEapplies to these modes only. Membership is DERIVED by running every generation of every mode’s fragment under the int64/MINIMALDATA evaluator with atargetTimetoo wide to read as a number (tests/test_dmint_deploy_bounds.py), and checked against this constant there.
- pyrxd.glyph.dmint.types.target_for_difficulty(difficulty)[source]¶
The PoW target a dMint contract is deployed with at
difficulty:MAX_SHA256D_TARGET // difficulty.The same formula for EVERY hash algorithm. Part B1 (
_PART_B1, identical in V1 and V2 code) reverses the PoW hash, drops all but 12 bytes, requires 4 of them to be zero and reads the other 8 as the number Part B2 compares with the target — whether the hash opcode before it is OP_HASH256, OP_BLAKE3 or OP_K12. It is the formula of canonical PhotonicdMintDiffToTarget(MAX_TARGET / BigInt(difficulty),packages/lib/src/script.tsat becf41a7), which takes no algorithm argument, and the BLAKE3/K12 V2 contracts on mainnet carry exactly these targets (e.g. a declareddiffof 2 →0x3fffffffffffffff).
- pyrxd.glyph.dmint.types.check_dmint_core_bounds(*, stage, max_height, reward, difficulty, names=None)[source]¶
Refuse a
max_height,rewardordifficultyno dMint contract, V1 or V2, can use.Called from
check_v2_numeric_bounds()(so from every V2 deploy path) and fromcheck_dmint_v1_bounds()(every V1 deploy path), so the two versions refuse the same values with the same words.namesmaps a parameter to the name the caller knows it by (reward->reward_photonsor--reward). Each bound is what the covenant reads, and the V1 epilogue reads these the same way V2’s Part C does:max_height<=MAX_SCRIPT_NUM— the covenant adds 1 to the height and compares it with maxHeight as script numbers on every mint (OP_1ADD…OP_NUMEQUAL; V1:54 7a 81 8b 76 53 7a 9c).reward<=RADIANT_MAX_PHOTONS— each mint’s reward outputs must together hold exactlyrewardphotons (OP_CODESCRIPTHASHVALUESUM_OUTPUTS … OP_NUMEQUALVERIFY; V1:76 e4 7b 9d), and Radiant consensus refuses a transaction whose outputs total more thanMAX_MONEY(bad-txns-txouttotal-toolarge), so a larger reward could never be paid.difficulty<=MAX_SHA256D_TARGET— above it the target is 0, which only a hash whose compared 8 bytes are all zero can meet.
Before 2026-09-23 V1 held
max_heightandrewardto0xFFFFFF, called “V1’s 3-byte ceiling”. No covenant rule was behind it: the first V1 contracts pyrxd decoded carry both as 3-byte pushes (docs/dmint-research-mainnet.md§2.3,docs/dmint-research-photonic-deploy.md), and that width became a limit. It refused deploys Photonic builds — mainnet V1 contracts include a max height of 300,000,000 and a reward of 888,888,888, whichtests/test_dmint_v1_target_push.pyrebuilds byte for byte. V1 has one tighter limit of its own, onmax_height: seecheck_dmint_v1_bounds().
- pyrxd.glyph.dmint.types.check_dmint_v1_bounds(*, stage, max_height, reward, difficulty, names=None)[source]¶
Refuse V1 deploy parameters that no V1 contract built from them could mint to the end.
check_dmint_core_bounds()(the bounds V1 shares with V2), thenmax_height<=MAX_V1_MAX_HEIGHT(2**31), past which a V1 contract stops at height2**31 - 1with mints left (that constant has the reason). Called fromDmintV1DeployParams.__post_init__(every V1 deploy is built from one) and fromdeploy-dmintfor V1, first, so a refusal names the flag the user typed.This bounds DEPLOYS. Mainnet has V1 contracts with a larger
max_height($BRO: 696,969,000,000); pyrxd parses and mints them like any other up to height2**31 - 1, and refuses the mint from there (miner._unmintable_reason).
- pyrxd.glyph.dmint.types.check_v2_numeric_bounds(*, stage, max_height, reward, difficulty, daa_mode, target_time, half_life, epoch_length, schedule, names=None)[source]¶
Refuse V2 deploy parameters no contract built from them could ever be minted with.
Upper bounds only; each type keeps its own lower-bound checks. The three V1 shares —
max_height,rewardanddifficulty— arecheck_dmint_core_bounds(), which this calls first. Called fromDmintDeployParams.__post_init__(which both V2 deploy scripts — the fee placeholder and the real reveal — are built from), fromDmintV2DeployParams.__post_init__(so the API refuses on the caller’s own object) and fromdeploy-dmint --v2(so the CLI refuses before any wallet or network work, naming the flag the user typed).namesmaps a parameter (max_height,reward,difficulty,target_time,half_life,epoch_length,schedule) to the name the caller knows it by; unmapped ones keep their own name._push_minimalseparately refuses to emit any number wider thanMAX_SCRIPT_NUM_BYTES, whichever builder asks.Each bound comes from what the covenant can read or observe, not from a guess at what is sensible; every value on the mainnet V2 deploys surveyed is far inside all of them (
tests/test_dmint_deploy_bounds.pypins the observed values as accepted):max_height,reward,difficulty— seecheck_dmint_core_bounds().target_time<=MAX_V2_TARGET_TIMEin the modes that read it as a number (DAA_MODES_READING_TARGET_TIME) — a wider spacing than any mint can observe; see that constant. FIXED and SCHEDULE only carrytargetTimeas bytes, so there it is held only toMAX_SCRIPT_NUM: the one bound here that is pyrxd’s encoder’s (it writes every state number in at most 8 bytes), not the covenant’s.half_life<=MAX_SCRIPT_NUM(ASERT, where it is baked in) — only ever a divisor (OP_DIV), so any readable number works; nothing narrower is imposed.epoch_length<=MAX_SCRIPT_NUM(EPOCH) — only ever a divisor (OP_MOD).each SCHEDULE height <=
MAX_SCRIPT_NUM— compared with the height (OP_GREATERTHANOREQUAL).
- class pyrxd.glyph.dmint.types.DmintDeployParams[source]¶
Bases:
objectParameters for deploying a V2 dMint contract.
- property initial_target: int¶
The deploy target for
difficulty—target_for_difficulty(), for everyalgo.BLAKE3 and K12 used to get
MAX_V2_TARGET_256 // difficultyhere: a target wider than the 8 bytes Part B2 can read, so the contract could never be minted.algoselects the hash opcode (and the algoId the state records); it does not change the target formula.
- __init__(contract_ref, token_ref, max_height, reward, difficulty, algo=DmintAlgo.SHA256D, daa_mode=DaaMode.FIXED, target_time=60, half_life=240, 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’s
DmintPayloadtype (packages/lib/src/types.tsatbecf41a):daa?: { mode, targetBlockTime, halfLife?, asymptote?, // ASERT windowSize?, // LWMA epochLength?, maxAdjustment?, // EPOCH schedule?: { height, difficulty }[] } // SCHEDULEEvery optional key is emitted ONLY when set (non-zero / non-empty), so a payload that does not use one is byte-identical to what pyrxd emitted before the key existed (2026-09-16:
asymptote,epochLength,maxAdjustment,schedulewere added; FIXED/ASERT/LWMA payloads that do not set them are unchanged).scheduleentries are(height, difficulty)— difficulty, not target, is what the payload type declares.max_adjustmentis stored as the RAW payload number and is informational only: Photonic’s builder reads a payload value of 1..4 as the log2 shift count and 8/16 as multipliers (script.tsmaxAdjustmentToLog2; its Mint UI passes the same number to the payload and the builder), while pyrxd’sdeploy-dmint --max-adjustmentis a multiplier baked asDmintDeployParams.max_adjustment_log2. So the payload value alone does not say which clamp a contract bakes — only the contract bytecode does. Validation accepts exactly the values Photonic’s builder accepts (EPOCH_MAX_ADJUSTMENT_PAYLOAD_VALUES); pyrxd does not reinterpret them.- 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, asymptote=0, epoch_length=0, max_adjustment=0, schedule=())¶
- 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). On the final mint no output carries a state: this is the spent state with onlyheightadvanced tomax_height(sois_exhaustedis True).contract_script – The script of output 0: the recreated contract (state + separator + code), or on the final mint the burn
d8 <contractRef> 6athat replaces it.reward_script – The FT-wrapped reward output script (P2PKH, then
bd d0 <tokenRef>and the 12-byte FT code), paid to the miner.fee – Transaction fee in photons.
is_final_mint – True when this mint takes the contract to
max_height: output 0 is the burn, not a recreated contract, and no further mint of this contract is possible.
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.
max_height – Maximum mints per contract.
target – The PoW target (
MAX_SHA256D_TARGET // difficulty). The codescript is rebuilt with today’s builder, which pushes it minimally; a contract pyrxd deployed before 2026-09-23 at difficulty 256 or more carries a non-minimal 8-byte push instead, so this fast path does not find it (it can never be minted anyway).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 (24 + 4 epilogue constants shared with chain):
_push_minimal, _push_4bytes_le, _PART_A, _POW_HASH_OP, _build_asert_daa_legacy, _build_linear_daa_legacy, _build_linear_daa_legacy_prefloor, _build_asert_daa_v2, _build_linear_daa_v2, _build_epoch_daa, _build_schedule_daa, _build_part_b, DetectedDaaBytecode, detect_daa_bytecode, build_dmint_state_script, build_dmint_code_script, build_dmint_contract_script, build_dmint_contract_burn_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
- class pyrxd.glyph.dmint.builders.DetectedDaaBytecode[source]¶
Bases:
objectWhat
detect_daa_bytecode()found baked into a contract’s Part B.- Parameters:
daa_mode – The mode that was asked about (ASERT or LWMA).
version – Which retarget-formula generation the bytecode is.
daa_bytes – The exact DAA fragment that matched, byte-for-byte.
half_life – ASERT only — the half-life constant READ OUT OF the bytecode (
Nonefor LWMA, which bakes no parameter). This is the ground truth a miner must use; a caller-supplied value that differs from it produces a target the covenant rejects.
- version: DaaBytecodeVersion¶
- __init__(daa_mode, version, daa_bytes, half_life=None)¶
- Parameters:
daa_mode (DaaMode)
version (DaaBytecodeVersion)
daa_bytes (bytes)
half_life (int | None)
- Return type:
None
- pyrxd.glyph.dmint.builders.detect_daa_bytecode(code, daa_mode)[source]¶
Classify which ASERT/LWMA retarget-formula generation a V2 contract bakes.
codeis the contract’s code section — the bytes AFTER the0xbdOP_STATESEPARATOR (whatbuild_dmint_code_scriptreturns and what follows the parsed state in a UTXO script).daa_modeis the mode the state script declares (item 6); this function checks the CODE agrees with it.Recognition is rebuild-and-compare, not a prefix sniff: the half-life constant is read out of the bytecode, the candidate template is rebuilt from it with the same builder a deploy would use, and the whole fragment (plus the Part B4 that must follow it) has to match byte-for-byte. So a contract is classified only if pyrxd can reproduce its exact bytes, which is also the condition under which the mint builder can recompute its target. The discriminators, verified against the bytes the builders emit (
tests/test_dmint_daa_v2_resync.py):ASERT: after the 7-byte
excesspreamblec5 5279 94 5379 94, v2 has<RADIX push> OP_MUL=03000001 95; legacy has<halfLife push> OP_DIV(5th byte96, never95— even for halfLife 65536).LWMA: v2 shares the preamble and signature, then
5379 96(OP_3 PICK OP_DIV) as the gain divisor; legacy has noexcessat all —c5 5279 94 5379 54 95 a3(OP_4 OP_MUL OP_MIN), with (LEGACY) or without (LEGACY_LWMA_PREFLOOR) the00 a4timeDelta floor.
Mirrors Photonic’s Glyph-miner
extractDaaParamsFromCodeScriptin intent, with one deliberate difference from the Radiant-Core fork this was ported from: that fork’s_asert_version_of_codeclassified ANY fragment lacking the v2 signature as legacy (return 2 if sig else 1), so a corrupt or unknown contract would be mined under a guessed formula. Here an unrecognised fragment is REPORTED withUnrecognizedDaaBytecodeError(naming the bytes).- Raises:
ValueError –
daa_modeis not ASERT or LWMA — FIXED/EPOCH/SCHEDULE have a single generation and are byte-verified by rebuilding Part B from the deploy parameters instead.UnrecognizedDaaBytecodeError – the code is not a V2 dMint template, or its DAA fragment matches no known generation of
daa_mode.
- Parameters:
- Return type:
- 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_contract_burn_script(contract_ref)[source]¶
The output a FINAL mint puts where the contract used to be:
d8 <contractRef> 6a.OP_PUSHINPUTREFSINGLETON <contractRef> OP_RETURN. On the mint whose new height equalsmax_height, the output-validation block (V2 Part C and the V1 epilogue carry the same bytes here) takes its IF branch instead of rebuilding the next state: it readsOP_OUTPUTBYTECODEat the contract’s output index andOP_EQUALVERIFYs it against0xd8 || contractRef || 0x6aassembled on the stack (5279cd01d853797e016a7e88). The ref is the CONTRACT ref read from the state, not the token ref. The prologue before that branch also requires the token ref to appear in exactly as many outputs as there are FT reward outputs, so the burn must not carry it.Nothing can spend the output (execution ends at
OP_RETURN), so the contract singleton ends here and no further mint of it is possible. Same bytes as Glyph-miner’sburnScript(src/glyph.ts).
- 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:
04 <height:4 LE> | d8 contractRef(36B) | d0 tokenRef(36B) | maxHeight | reward | target
the last three as minimal script-number pushes (
_push_minimal()). That is byte for byte what Photonic Wallet’s V1-eradMintScriptbuilds (its SHA256d/FIXED branch,packages/lib/src/script.tsat c8540a6:push4bytes(height), thenpushMinimalfor maxHeight, reward and target), andtests/test_dmint_v1_target_push.pychecks it against a transcription of that builder.The target has to be minimal. The V1 epilogue compares the target, exactly as pushed, with the proof-of-work number (
… 81 76 00 a2 69 a2 69: OP_BIN2NUM normalises the hash window, never the target), and Radiant reads that operand as a script number with minimal encoding required. The minimal push is 8 bytes only for a target of at least2**55(difficulty 255 or less); below that it is shorter, down to OP_1..OP_16 for targets 1..16. Mainnet V1 contracts carry 6-, 7- and 8-byte targets, and two pinned intests/test_dmint_v1_target_push.pyhave been minted over a thousand times each with a 7-byte one. Until 2026-09-23 pyrxd pushed every target as08+ 8 bytes, which is not minimal below2**55, so no V1 contract it deployed at difficulty 256 or more can ever be minted (pyrxd.glyph.dmint.miner._unreadable_target_reason()refuses them).- 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. A target of 0 is refused by pyrxd: only a hash whose compared 8 bytes are all zero meets it.- 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 (21):
_OP_STATESEPARATOR (re-export from types), _parse_script_int, _decode_script_le_int, _POW_OP_BYTE_TO_ALGO, _parse_dmint_script, _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 (22):
PowPreimageResult, build_pow_preimage, build_mint_scriptsig, ProgressCallback, DEFAULT_PROGRESS_INTERVAL_S, _PROGRESS_CHECK_MASK, 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
- pyrxd.glyph.dmint.miner.ProgressCallback¶
callback(attempts, elapsed_s). Deliberately two plain scalars rather than a dataclass —pyrxd.contrib.miner.parallelimplements the same shape and must stay importable without pulling inpyrxd.glyph.dmint(its workers re-import the module on every spawn). Feed the pair tolive_stats()to turn it into an observed rate plus remaining-time quantiles.- Type:
Live-progress hook
- pyrxd.glyph.dmint.miner.MIN_FEE_RATE: int = 10000¶
Default fee rate for
build_dmint_mint_tx(), in photons per byte. DERIVED from the single definition of Radiant’s effective relay floor inpyrxd.fee_sizing, exactly aspyrxd.glyph.builder.MIN_FEE_RATE,pyrxd.glyph.ft.MIN_FEE_RATEandpyrxd.wallet.DEFAULT_FEE_RATEare.It was a hardcoded
10_000, and this is the call site where that costs the most: a mint transaction is the end of a multi-minute PoW grind, Radiant has neither RBF nor CPFP, and a rate below the floor cannot be bumped — the transaction squats on its inputs until mempool expiry while the contract’s height advances and another miner takes the slot. The floor has already moved once (LEGACY_MIN_RELAY_TX_FEE_PER_KB->RADIANT_CORE_2_MIN_RELAY_TX_FEE_PER_KB, a 10x step); a literal here would have silently kept the pre-2.0 rate.
- 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_legacy(current_target, last_time, current_time, target_time, half_life)[source]¶
LEGACY ASERT target — the integer power-of-2 stepper baked into pre-resync contracts.
Off-chain mirror of
pyrxd.glyph.dmint.builders._build_asert_daa_legacy(), the ASERT bytecode pyrxd emitted before 2026-09-16 (upstream replaced it with ASERT-v2 on 2026-06-19,ed53cd41). A contract deployed under it MUST keep being mined with THIS formula — the covenant recomputes the next target with the bytecode it was born with, and rejects any other value.build_dmint_mint_txdispatches here whendetect_contract_daa_bytecode()reportsDaaBytecodeVersion.LEGACY; new deploys usecompute_next_target_asert_v2().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).Both subtractions go through
_script_int64(), as in the v2 mirror: for atarget_timenear the int64 limit the on-chain OP_SUB aborts, so this raisesValidationErrorrather than return a target the contract cannot compute.Note
V2-only DAA. V1 has no DAA (fixed difficulty).
- pyrxd.glyph.dmint.miner.compute_next_target_linear_legacy(current_target, last_time, current_time, target_time)[source]¶
LEGACY LWMA target — the unity-gain single-sample retarget of pre-resync contracts.
Off-chain mirror of
pyrxd.glyph.dmint.builders._build_linear_daa_legacy()(with theOP_0 OP_MAXtimeDelta floor) AND of_build_linear_daa_legacy_prefloor()(without it) for everycurrent_time >= last_time. On a negative delta the two variants differ; this mirror gives the floored variant’s answer, andbuild_dmint_mint_txdoes not build a backwards mint of a pre-floor contract. This is the LWMA bytecode pyrxd emitted before 2026-09-16 (upstream replaced it with LWMA-v2 on 2026-06-20,c90e6506); the mainnet LWMA deploydea3beb9…carries the pre-floor variant and is mined with this formula. New deploys usecompute_next_target_linear_v2().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.The two operations the bytecode can abort on — the OP_SUB of the times and the
OP_4 OP_MULoftarget_time— go through_script_int64(), so an input the contract cannot evaluate raisesValidationErrorinstead of returning a target.Note
V2-only DAA.
- pyrxd.glyph.dmint.miner.compute_next_target_asert(current_target, last_time, current_time, target_time, half_life)¶
Historical names. They compute EXACTLY what they computed before 2026-09-16 — the legacy formulas — so no existing caller’s numbers change silently. They are NOT the formula a contract deployed by the current builder carries: for that use the
_v2functions, or letbuild_dmint_mint_txdispatch ondetect_contract_daa_bytecode(). Kept as aliases rather than repointed at v2 because repointing would change results for legacy contracts without a trace.
- pyrxd.glyph.dmint.miner.compute_next_target_linear(current_target, last_time, current_time, target_time)¶
LEGACY LWMA target — the unity-gain single-sample retarget of pre-resync contracts.
Off-chain mirror of
pyrxd.glyph.dmint.builders._build_linear_daa_legacy()(with theOP_0 OP_MAXtimeDelta floor) AND of_build_linear_daa_legacy_prefloor()(without it) for everycurrent_time >= last_time. On a negative delta the two variants differ; this mirror gives the floored variant’s answer, andbuild_dmint_mint_txdoes not build a backwards mint of a pre-floor contract. This is the LWMA bytecode pyrxd emitted before 2026-09-16 (upstream replaced it with LWMA-v2 on 2026-06-20,c90e6506); the mainnet LWMA deploydea3beb9…carries the pre-floor variant and is mined with this formula. New deploys usecompute_next_target_linear_v2().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.The two operations the bytecode can abort on — the OP_SUB of the times and the
OP_4 OP_MULoftarget_time— go through_script_int64(), so an input the contract cannot evaluate raisesValidationErrorinstead of returning a target.Note
V2-only DAA.
- pyrxd.glyph.dmint.miner.compute_next_target_asert_v2(current_target, last_time, current_time, target_time, half_life)[source]¶
ASERT-v2 next target — the fractional, symmetric, damped retarget of CURRENT deploys.
Off-chain mirror of
pyrxd.glyph.dmint.builders._build_asert_daa_v2(), which is the canonical PhotonicbuildAsertDaaBytecode(script.tsatbecf41a), and an exact port of the referencecomputeAsertV2Targetinpackages/lib/src/dmintDaaV2.ts(same commit) — bigint arithmetic there, so the only semantic to carry over is that BigInt and OP_DIV both truncate toward zero:excess = (current_time - last_time) - target_time driftFp = trunc((excess * RADIX) / half_life) # RADIX = 2^16, signed driftFp = clamp(driftFp, -RADIX/4, +RADIX/4) # ±25%/mint damping t = min(current_target, MAX_TARGET/4) # difficulty floor 4 new = clamp(t + (t / RADIX) * driftFp, 1, MAX_TARGET/4)
_trunc_divis load-bearing: OP_DIV truncates toward zero while Python//floors, and they differ for the negativeexcessof an early block (e.g.excess = -1, half_life = 240→ trunc(-65536/240) = -273; floor gives -274). A floor here would recreate a target the covenant does not, and the mint is rejected.int64 bounds, every intermediate (the covenant aborts, not wraps, outside
[-(2^63-1), 2^63-1]; this mirror raisesValidationErrorat the same points via_script_int64()so it never predicts an unmineable state):timeDelta = current_time - last_time: for an ASERT/LWMA mintbuild_dmint_mint_txrequires bothcurrent_timeand the state’slast_timeto be readable lastTimes (is_readable_last_time:[2^23, 2^31 - 1]), so|timeDelta| <= 2^31 - 1in either direction.excess = timeDelta - target_time:target_timeis a state script number in[1, 2^63 - 1], soexcessis in[-(2^63 - 1) - (2^31 - 1), 2^31 - 2]and the OP_SUB underflows only fortarget_time > 2^63 - 1 + timeDelta— checked.excess * RADIX:|excess| <= 2^47 - 1⇒|excess * 2^16| <= 2^63 - 2^16, representable;|excess| >= 2^47⇒|product| >= 2^63, which OP_MUL aborts. Positive excess never reaches it (excess <= 2^31 - 2); negative excess reaches it only fortarget_time >= 2^47 + timeDelta(≈ 1.4e14 s ≈ 4.5 Myr) — checked. Photonic’s proof (|excess| <= ~4.3e9→|product| <= ~2.8e14 < 2^63) is the realistic-deploy case of the same bound.driftFp = trunc(excess*RADIX / half_life):|driftFp| <= |excess*RADIX|sincehalf_life >= 1; then clamped to|driftFp| <= 2^14.t = min(current_target, MAX/4) <= 2^61 - 1;t / RADIX <= 2^45 - 1.delta = (t/RADIX) * driftFp:|delta| < 2^45 * 2^14 = 2^59— cannot overflow.t + delta < 2^61 + 2^59 < 2^63— cannot overflow.
So the only reachable abort is the
excesschain for an absurdtarget_time, and it is reported rather than mis-predicted.Note
V2-only DAA.
current_targetmust itself be a valid script number (1..MAX_SHA256D_TARGET): the covenant compares it with CScriptNum ops, so a target wider than 8 bytes cannot run through any DAA on chain — for any algorithm.
- pyrxd.glyph.dmint.miner.compute_next_target_linear_v2(current_target, last_time, current_time, target_time)[source]¶
LWMA-v2 next target — the damped fractional retarget of CURRENT LWMA deploys.
Off-chain mirror of
pyrxd.glyph.dmint.builders._build_linear_daa_v2()(canonical PhotonicbuildLinearDaaBytecodeatbecf41a) and a port ofcomputeLwmaV2Targetinpackages/lib/src/dmintDaaV2.ts: exactlycompute_next_target_asert_v2()with the responsiveness gain auto-set totarget_time—driftFp = (excess * RADIX) / target_time— so a block twice the target time hits the ±25% clamp. Same int64 proof (target_time >= 1is deploy-enforced, so the OP_DIV never divides by zero).The old unity-gain LWMA (
compute_next_target_linear_legacy()) could jump difficulty 4× in one block and slam the target to 1 on a zero-delta block; this caps each step at ±25% and is stable on-target.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).
clampedDelta >= target_time >> N >= 0and the divide’s operands are positive, so floor == OP_DIV’s truncate-toward-zero. The lower bound is 0 whentarget_time < 2**N; pyrxd refuses to deploy such a contract, andbuild_dmint_mint_txrefuses any mint whose next target would be 1. The OP_SUB and the N x OP_2MUL oftarget_timego through_script_int64(), so an input the contract cannot evaluate raisesValidationError. 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.detect_contract_daa_bytecode(contract_script)[source]¶
Which ASERT/LWMA retarget-formula generation a V2 dMint contract script bakes.
Parses the state (for the declared
daa_modeand the state/code boundary) and classifies the code section withpyrxd.glyph.dmint.builders.detect_daa_bytecode(). For ASERT the result also carries thehalf_liferead out of the bytecode — the value a miner MUST use.- Raises:
ValidationError – not a V2 contract script, or its DAA mode is not ASERT/LWMA (FIXED/EPOCH/SCHEDULE have a single generation;
build_dmint_mint_txbyte-verifies those by rebuilding Part B from the deploy parameters).UnrecognizedDaaBytecodeError – the fragment matches no known generation.
- Parameters:
contract_script (bytes)
- Return type:
- pyrxd.glyph.dmint.miner.difficulty_to_target(difficulty, algo=DmintAlgo.SHA256D)[source]¶
Convert difficulty to PoW target:
MAX_SHA256D_TARGET // difficultyfor everyalgo.algois accepted for backward compatibility and does not change the result — seetarget_for_difficulty(), the single definition this andDmintDeployParams.initial_targetshare. BLAKE3/K12 once got(2**256 - 1) // difficultyhere, a target no dMint covenant can read.
- pyrxd.glyph.dmint.miner.target_to_difficulty(target, algo=DmintAlgo.SHA256D)[source]¶
Convert PoW target to difficulty (approximate):
MAX_SHA256D_TARGET // target.The inverse of
difficulty_to_target(), and like it independent ofalgo(accepted for backward compatibility only).
- 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.
- pyrxd.glyph.dmint.miner.DEFAULT_PROGRESS_INTERVAL_S = 0.5¶
Default seconds between progress callbacks. Callers that render to a terminal usually want to rate-limit further inside their own callback.
- 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, progress=None, progress_interval_s=0.5)[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. SeeDEFAULT_MAX_ATTEMPTS— it is a fail-fast cap in attempts, not a wall-clock budget.progress (Callable[[int, float], None] | None) – Optional
callback(attempts, elapsed_s)invoked roughly everyprogress_interval_swhile grinding. Feed the pair tolive_stats()for an observed rate + remaining-time quantiles. Exceptions raised by the callback propagate to the caller — which is a supported way to impose a deadline on the grind.progress_interval_s (float) – Minimum seconds between callbacks. Checked at a 65536-attempt granularity, so a very slow machine may report less often than requested.
- Raises:
ValidationError –
preimageis not 64 bytes,targetis not positive,nonce_widthis not 4 or 8,max_attemptsis < 1, orprogress_interval_sis not positive.NotImplementedError –
algois BLAKE3 or K12.MaxAttemptsError – No solution found within
max_attemptsiterations. The exception’sattemptsandelapsed_sattributes carry telemetry.
- Return type:
Note
There is no “easy” target for this loop. The verifier requires four leading zero bytes, so the mean is
2**96 / targetand floors at2**33 ≈ 8.6e9attempts even at difficulty 1 — lowering the difficulty cannot bring a run under that. (An earlier version of this docstring claimed a shifted target gave “~1 in 256 expected”; it confused the difficulty multiplier for an attempt count, and no such example completes in milliseconds.) Size a run withestimate_attempts()before starting it, and usebenchmark_sha256d()(orpyrxd glyph dmint-estimate) to turn that into wall clock.Usage:
from pyrxd.glyph.dmint import estimate_attempts, mine_solution est = estimate_attempts(target) # EXACT: mean + quantiles result = mine_solution( preimage, target, nonce_width=4, max_attempts=est.quantile_attempts[-1][1], # e.g. the p99 budget progress=lambda attempts, elapsed: ..., # live rate + ETA )
- pyrxd.glyph.dmint.miner.mine_solution_external(preimage, target, *, miner_argv, nonce_width=4, timeout_s=600.0, progress=None, progress_interval_s=0.5, algo=DmintAlgo.SHA256D)[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).OPTIONALLY (added after 0.13.0) write zero or more progress lines to stderr while it searches:
{"progress": {"attempts": N, "elapsed_s": F}}, one JSON object per line. Purely additive — a miner that has never heard of this writes nothing extra, and nothing here changes for it.
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.progress (Callable[[int, float], None] | None) – Optional
callback(attempts, elapsed_s). WhenNone(the default), behavior is byte-for-byte identical to before this parameter existed: a single blockingsubprocess.runcall with stderr fully discarded (subprocess.DEVNULL), same as pyrxd <= 0.13.0. Passing a callback opts into a streaming invocation that reads (rather than discards) stderr, parses any progress frames the miner chooses to emit, and callsprogresswith the most recently observed one roughly everyprogress_interval_s— the same cadence contractmine_solution()documents. An external miner that never emits a progress frame (the common case today) simply meansprogressis never called; the grind still runs to completion or timeout exactly as it would withprogress=None. A raising callback propagates and the subprocess is terminated — the supported way to impose a deadline, mirroringmine_solution()andpyrxd.contrib.miner.parallel.mine.progress_interval_s (float) – Minimum seconds between
progresscalls. Ignored whenprogressisNone.algo (DmintAlgo) – The contract’s PoW algorithm. The protocol carries no algorithm field and the answer is re-verified with SHA256d, so anything but SHA256D is refused before the miner is spawned. Pass the contract’s own
state.algo; the default keeps older callers working.
- Raises:
ValidationError – The miner returned a malformed JSON response, a nonce of wrong width, or a nonce that fails local verification.
NotImplementedError –
algois BLAKE3 or K12.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, progress=None, progress_interval_s=0.5)[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) – The contract’s PoW algorithm. Only SHA256D can be mined: BLAKE3 and K12 raise
NotImplementedErroron BOTH paths, before any hashing. (This used to be ignored on the external-miner path, whose protocol carries no algorithm field and whose answer is re-verified with SHA256d — so a BLAKE3/K12 caller got a SHA256d grind, not a refusal.)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).progress (Callable[[int, float], None] | None) – Live-progress hook. On the in-process path this is
mine_solution()’s own callback. On the external-miner path it is passed tomine_solution_external(), which streams a miner’s optional stderr progress frames if it emits any (added after 0.13.0) — an external miner that doesn’t know about progress frames simply never triggers the callback, same asprogress=None.progress_interval_s (float) – Minimum seconds between
progresscalls.
- 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.
NotImplementedError –
algois BLAKE3 or K12 (either path).
- 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=None, 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(or, on the final mint, burns it: see below), 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 (the spent script with its state advanced:
height+ 1, and on V2 alsolastTimeandtargetas the note below says), 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.
The final mint (
height + 1 == max_height, flaggedis_final_minton the result) has the same inputs and the same outputs 1-3, but output 0 is the burnbuild_dmint_contract_burn_script()(d8 <contractRef> 6a) at value 0 instead of a recreated contract: the covenant’s final branch requires exactly that script at the contract’s output index, and requires the token ref to appear in the FT reward outputs only. The FT reward is unchanged. No state is rebuilt, so the checks that exist to protect the RECREATED contract — its 1-photon value, and for V2 alastTimeits next retarget can read and a target above 1 — are not applied to it. What the covenant still evaluates on this spend is checked as on any other mint: the PoW binding, the reward, and for V2 the Part B retarget, which runs on the final mint too (its result is discarded, but an input it cannot evaluate still fails the spend). The contract’s photon goes to change. Same output order and burn value as Glyph-miner’s final mint.Note
V2 supports all five DAA modes — FIXED, ASERT, LWMA, EPOCH, SCHEDULE (the canonical Photonic redesign). On every mint that recreates the contract, the covenant rebuilds the next state’s
last_timefromOP_TXLOCKTIMEand itstargetfrom the alt-stack DAA result, 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. The final mint recreates no state:current_timeis still set as thenLockTime, and Part B, which runs on the final mint too, still reads it in the modes whose retarget does, but nolast_timeortargetis written.current_timemust be<= 0x7FFFFFFFon every V2 mint (on a mint that recreates the contract, Part C’sNUM2BIN(_, 4)cannot write a locktime with bit 31 set; pyrxd applies the same bound to the final mint). On a mint that recreates the contract, for the modes whose retarget readslastTime(DAA_MODES_READING_LAST_TIME: ASERT, LWMA, EPOCH) it must also be>= 2**23, so the lastTime it writes is a minimally encoded script number a later retarget can read; the final mint writes no lastTime, and that lower bound is not applied to it. 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
Two generations of ASERT/LWMA retarget bytecode exist on chain. Upstream moved to the fractional ASERT-v2 / LWMA-v2 formulas on 2026-06-19/20 and pyrxd followed on 2026-09-16; contracts pyrxd deployed in between bake the legacy formulas. This builder reads the contract’s own bytecode (
detect_contract_daa_bytecode()) and recomputes the target with the matching mirror —compute_next_target_*_v2or*_legacy— then byte-verifies the suppliedhalf_lifeagainst the baked Part B for BOTH generations. A contract carrying bytecode of neither generation is refused (UnrecognizedDaaBytecodeError) rather than mined under a guessed formula.half_lifedefaults toNone— use the value detected in the contract. Detection has already read the baked half-life at that point, so requiring the caller to restate it could only refuse honest work: when the deploy-side default moved 3600 → 240 on 2026-09-16, every previously-working invocation against a 3600 contract started failing on a value the builder already knew. Supplyinghalf_lifeexplicitly still byte-verifies it against the baked Part B and fails fast, naming the baked value, if the two disagree.Note
Also refused before any PoW grind: a state whose
lastTimethis mint’s retarget reads and which is not a readable lastTime (that contract can no longer be minted at all); acurrent_timeearlier than the contract’slast_timeon a 2026-06-16 pre-floor LWMA contract; any mint whose recreated target would be 1 while the spent target is larger, in every mode; and any input the retarget mirror reports the contract’s int64 arithmetic cannot evaluate.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, 4-byte for V1 (use
b'\x00' * 8, or* 4, 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:
V2: Unix timestamp written as the tx
nLockTimeon every mint and, on a mint that recreates the contract, as the recreated state’slastTime(the covenant rebuilds it fromOP_TXLOCKTIME); drives the DAA retarget. The final mint writes nolastTime. V1: must be 0.- param fee_rate:
Photons per byte for fee calculation (default 10_000, the Radiant post-V2 relay minimum).
- raises ContractExhaustedError:
contract_utxo.state.is_exhaustedis True.- raises ValidationError:
nonceis not 8 bytes (4 for V1);miner_pkhis not 20 bytes.- returns:
DmintMintResultwith the unsigned tx and updated state.
- Parameters:
contract_utxo (DmintContractUtxo)
nonce (bytes)
miner_pkh (bytes)
current_time (int)
fee_rate (int)
funding_utxo (DmintMinerFundingUtxo | None)
op_return_msg (bytes | None)
half_life (int | None)
epoch_length (int | None)
max_adjustment_log2 (int | None)
- 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:
Hash-rate benchmark & time-to-mint estimator¶
Hash-rate benchmark + time-to-mint estimator for dMint SHA256d PoW.
Answers “how long will this contract take me to mint?” with three strictly separated classes of number — the separation is part of the API, not just the presentation:
- MEASURED
benchmark_sha256d()runs the same SHA256d chain the miner runs and reports the single-core rate it actually observed on this machine.live_stats()likewise reports the aggregate rate observed during a real grind.- EXACT
estimate_attempts()—p,E[attempts], and the attempt quantiles are closed-form consequences of the verifier predicate. No measurement, no assumption, no fitting.- PROJECTED
project_mint_eta()— every wall-clock ETA, and the aggregate rate it divides by. Aggregate rate issingle-core × workers: cross-core scaling is not measured, and real machines fall short of linear (shared L3, SMT, thermal/power limits).
Where the probability comes from¶
verify_sha256d_solution accepts a digest iff:
full[0:4] == b"\x00\x00\x00\x00"
and int.from_bytes(full[4:12], "big") < min(target, MAX_SHA256D_TARGET)
Since MAX_SHA256D_TARGET = 2**63 - 1 < 2**64, the effective target
never reaches the width of the second field, so those two clauses are
exactly the single condition “the top 96 bits of the digest, read
big-endian, are < target”: if the low 64 bits are already below a bound
under 2**64, the high 32 bits must be zero, and conversely. SHA-256
output is uniform over those 96 bits, and exactly target of the
2**96 values satisfy it, so:
p = target / 2**96
E[attempts] = 2**96 / target
Cross-check against a number the codebase already asserts elsewhere: at
difficulty 1 (target = 2**63 - 1) this gives p ≈ 2**-33 and
P(at least one hit in a 2**32 V1 nonce sweep) = 1 - (1-p)**(2**32)
≈ 0.3935 — the “~39% chance” documented on the V1 reroll loop in
pyrxd.cli.glyph_cmds._mine_claim_with_rerolls. See
tests/test_dmint_estimate.py for that cross-check as an executable
test.
Warning
MAX_SHA256D_TARGET / target is the difficulty multiplier
(target_to_difficulty()), not an attempt
count — it is low by a factor of 2**96 / MAX_SHA256D_TARGET ≈
2**33. At difficulty 1 it claims one expected attempt where the
true mean is 2**33 ≈ 8.6 billion.
Mining is a Bernoulli process, so attempts-to-first-hit is geometric and therefore memoryless: hashes already burned change nothing about the remaining distribution. That is why this module reports a mean plus quantiles and never a countdown. It is also why the V1 OP_RETURN reroll loop does not perturb the estimate: partitioning an i.i.d. hash stream into 2**32-nonce chunks leaves the total-attempts distribution unchanged, so one estimate covers V1 (4-byte nonce + rerolls) and V2 (8-byte nonce) alike.
Layering: this module sits at the end of the subpackage’s one-way graph
(types ← builders ← chain ← miner ← estimate).
- Symbols (12):
SHA256D_HIT_SPACE, DEFAULT_QUANTILES, DEFAULT_BENCHMARK_SECONDS, BENCHMARK_PREIMAGE, Sha256dBenchmark, AttemptEstimate, MintEtaProjection, LiveMiningStats, benchmark_sha256d, estimate_attempts, attempts_for_quantile, project_mint_eta, live_stats
- pyrxd.glyph.dmint.estimate.DEFAULT_QUANTILES: tuple[float, ...] = (0.5, 0.9, 0.99)¶
Quantiles reported by default. Geometric ⇒ memoryless, so these describe the remaining work at any point in a grind, not a countdown.
- pyrxd.glyph.dmint.estimate.DEFAULT_BENCHMARK_SECONDS = 0.5¶
Default benchmark duration. Long enough to average over scheduler noise, short enough that a CLI invocation still feels instant.
- pyrxd.glyph.dmint.estimate.BENCHMARK_PREIMAGE = b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./0123456789:;<=>?'¶
Fixed 64-byte benchmark preimage. Real preimages are hashes, so their bytes are irrelevant to throughput; pinning one makes the benchmark’s first digest a reproducible value a test can byte-match.
- class pyrxd.glyph.dmint.estimate.Sha256dBenchmark[source]¶
Bases:
objectMEASURED single-core SHA256d throughput on this machine.
Every field here was observed, not assumed. Nothing in it accounts for multiple cores — see
MintEtaProjectionfor that.- Parameters:
hashes – SHA256d evaluations actually performed.
elapsed_s – Wall-clock seconds they took.
hashes_per_second –
hashes / elapsed_s— MEASURED, single core.first_digest – The digest of nonce 0, produced by the benchmark’s own inner loop. Pinning it in a test is what stops a refactor from silently benchmarking a different hash chain.
- class pyrxd.glyph.dmint.estimate.AttemptEstimate[source]¶
Bases:
objectEXACT attempt statistics for a target — closed form, no measurement.
- Parameters:
target – The target as supplied.
effective_target –
min(target, MAX_SHA256D_TARGET)— pyrxd’s verifier (verify_sha256d_solution()) clamps, so the estimate must too.clamped – True iff the supplied target exceeded the ceiling and was clamped. A dMint covenant does NOT clamp: it reads the target as a signed script number of at most 8 bytes, so a target above the ceiling is unreadable (every mint aborts) or negative (no hash meets it). No contract carrying one can be minted.
difficulty –
target_to_difficulty(effective_target).probability_per_attempt –
p = effective_target / 2**96.expected_attempts –
1 / p = 2**96 / effective_target.quantile_attempts –
(quantile, attempts)pairs;attemptsis the smallest n withP(hit within n) >= quantile.
- __init__(target, effective_target, clamped, difficulty, probability_per_attempt, expected_attempts, quantile_attempts)¶
- class pyrxd.glyph.dmint.estimate.MintEtaProjection[source]¶
Bases:
objectPROJECTED wall-clock times. Every field is a projection.
aggregate_hashes_per_secondassumes linear scaling acrossworkers, which this library does not measure. Treat it as an upper bound: shared L3, SMT siblings, and power/thermal limits all pull real aggregate throughput belowsingle-core × workers.- Parameters:
workers – Worker count the projection assumes.
single_core_hashes_per_second – The per-core rate fed in (MEASURED if it came from
benchmark_sha256d(), otherwise whatever the caller supplied).aggregate_hashes_per_second –
single-core × workers— PROJECTED.mean_s –
E[attempts] / aggregate— PROJECTED.quantile_s –
(quantile, seconds)pairs — PROJECTED.
- class pyrxd.glyph.dmint.estimate.LiveMiningStats[source]¶
Bases:
objectProgress snapshot during a real grind: MEASURED rate, PROJECTED remainder.
observed_hashes_per_secondis measured — it is the aggregate rate the running miner actually achieved, so it needs no scaling assumption. The remaining-time fields are projections built on it.Because the process is memoryless, the remaining-work distribution is identical to the distribution at attempt zero:
attemptsalready burned do not reduceremaining_mean_s. This is a distribution, not a countdown.- Parameters:
attempts – Attempts completed so far.
elapsed_s – Seconds elapsed so far.
observed_hashes_per_second – MEASURED aggregate rate.
remaining_mean_s – PROJECTED mean seconds still to go (
Noneif no rate is observable yet).remaining_quantile_s – PROJECTED
(quantile, seconds)pairs.
- pyrxd.glyph.dmint.estimate.benchmark_sha256d(*, duration_s=0.5, preimage=b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./0123456789:;<=>?', nonce_width=8)[source]¶
Measure this machine’s single-core SHA256d rate. MEASURED.
Runs the identical hash chain the miner runs —
sha256(sha256(preimage + nonce).digest()).digest()over a little-endian nonce — inlined exactly as inpyrxd.contrib.miner.parallel._worker. Benchmarking anything else (a baresha256, a stand-in payload) would report a rate the miner never achieves.The first batch is deliberately one hash wide so
first_digestfalls out of the benchmark’s own loop rather than a parallel expression that could drift away from it.Single core only. Multiply by a worker count at your own risk — see
project_mint_eta().- Parameters:
duration_s (float) – Target wall-clock duration. The loop overshoots by at most one batch.
preimage (bytes) – 64-byte preimage to hash against.
nonce_width (int) – 4 (V1) or 8 (V2). Throughput is width-independent in practice; the parameter exists so the benchmark can mirror a specific contract exactly.
- Raises:
ValidationError –
duration_sis not positive,preimageis not 64 bytes,nonce_widthis not 4 or 8, or the clock did not advance.- Return type:
- pyrxd.glyph.dmint.estimate.attempts_for_quantile(target, quantile)[source]¶
Attempts needed to hit with probability
quantile. EXACT.The smallest
nwith1 - (1-p)**n >= quantile, i.e.ceil(ln(1-quantile) / ln(1-p)). Both logs go throughmath.log1p(), which keeps full precision at thep ~ 1e-10scale wherelog(1 - p)would round tolog(1.0) == 0and the division would blow up.- Parameters:
- Raises:
ValidationError –
target < 1orquantileout of range.- Return type:
- pyrxd.glyph.dmint.estimate.estimate_attempts(target, *, quantiles=(0.5, 0.9, 0.99))[source]¶
Closed-form attempt statistics for a dMint target. EXACT.
p = target / 2**96andE[attempts] = 2**96 / target— derived from the verifier predicate, not estimated. See the module docstring for the derivation and for why the difficulty multiplier (MAX_SHA256D_TARGET / target) is not an attempt count.Valid for V1 and V2 alike: rerolling a V1 preimage after a 2**32-nonce sweep re-partitions the same i.i.d. hash stream, which leaves the total-attempts distribution untouched.
- Parameters:
- Raises:
ValidationError –
target < 1or a quantile is out of range.- Return type:
>>> from pyrxd.glyph.dmint import difficulty_to_target, estimate_attempts >>> est = estimate_attempts(difficulty_to_target(1)) >>> round(est.expected_attempts) 8589934592
- pyrxd.glyph.dmint.estimate.project_mint_eta(estimate, *, single_core_hashes_per_second, workers=1)[source]¶
Turn EXACT attempt counts into PROJECTED wall-clock times.
Two separate reasons this is a projection and not a measurement:
aggregate = single_core × workersassumes linear cross-core scaling. pyrxd does not measure that, and real machines do not achieve it.Even given a perfect rate, the completion time is a random variable.
mean_sis its mean; the quantiles bracket it. There is no single “time remaining”.
- Parameters:
estimate (AttemptEstimate) – From
estimate_attempts().single_core_hashes_per_second (float) – Per-core rate (from
benchmark_sha256d(), or supplied).workers (int) – Worker count to project across.
- Raises:
ValidationError – non-positive rate or
workers < 1.- Return type:
- pyrxd.glyph.dmint.estimate.live_stats(attempts, elapsed_s, estimate)[source]¶
Snapshot a running grind: MEASURED rate + PROJECTED remaining time.
Unlike
project_mint_eta(), the aggregate rate here is measured — it is what the miner actually achieved overelapsed_s— so no scaling assumption is involved. The remaining-time fields are still projections, and they do not shrink asattemptsgrows: the geometric distribution is memoryless, so the work remaining after a billion misses is distributed exactly as it was at attempt zero.- Parameters:
attempts (int) – Attempts completed so far (0 is allowed).
elapsed_s (float) – Seconds elapsed so far (0 is allowed; then no rate is observable and the remaining fields are empty).
estimate (AttemptEstimate) – From
estimate_attempts().
- Raises:
ValidationError – negative
attemptsorelapsed_s.- Return type: