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_STATESEPARATOR lives in types (not chain) because builders needs it and builders → chain would be a cycle.

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

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

class pyrxd.glyph.dmint.AttemptEstimate[source]

Bases: object

EXACT 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; attempts is the smallest n with P(hit within n) >= quantile.

__init__(target, effective_target, clamped, difficulty, probability_per_attempt, expected_attempts, quantile_attempts)
Parameters:
Return type:

None

target: int
effective_target: int
clamped: bool
difficulty: int
probability_per_attempt: float
expected_attempts: float
quantile_attempts: tuple[tuple[float, int], ...]
class pyrxd.glyph.dmint.DaaBytecodeVersion[source]

Bases: IntEnum

Which 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, Photonic c90e6506); 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 the OP_0 OP_MAX timeDelta 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 deploy dea3beb9….

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: object

What 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 (None for 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:
Return type:

None

half_life: int | None = None
daa_mode: DaaMode
version: DaaBytecodeVersion
daa_bytes: bytes
class pyrxd.glyph.dmint.DmintAlgo[source]

Bases: IntEnum

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

Bases: object

The dmint object embedded in Glyph V2 token metadata CBOR.

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

Field names mirror Photonic Wallet’s DmintPayload type (packages/lib/src/types.ts at becf41a):

daa?: { mode, targetBlockTime,
        halfLife?, asymptote?,        // ASERT
        windowSize?,                  // LWMA
        epochLength?, maxAdjustment?, // EPOCH
        schedule?: { height, difficulty }[] }  // SCHEDULE

Every 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, schedule were added; FIXED/ASERT/LWMA payloads that do not set them are unchanged). schedule entries are (height, difficulty) — difficulty, not target, is what the payload type declares.

max_adjustment is 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.ts maxAdjustmentToLog2; its Mint UI passes the same number to the payload and the builder), while pyrxd’s deploy-dmint --max-adjustment is a multiplier baked as DmintDeployParams.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=())
Parameters:
Return type:

None

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

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

Parameters:

d (dict)

Return type:

DmintCborPayload

half_life: int = 0
max_adjustment: int = 0
schedule: tuple[tuple[int, int], ...] = ()
target_block_time: int = 60
to_cbor_dict()[source]

Encode to the dict that becomes the dmint CBOR value.

Return type:

dict

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

Bases: object

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

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

  • vout – output index

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

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

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

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

None

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

Bases: object

Parameters for deploying a V2 dMint contract.

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

None

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

The deploy target for difficulty — target_for_difficulty(), for every algo.

BLAKE3 and K12 used to get MAX_V2_TARGET_256 // difficulty here: a target wider than the 8 bytes Part B2 can read, so the contract could never be minted. algo selects the hash opcode (and the algoId the state records); it does not change the target formula.

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

Bases: object

The output of a successful mine_solution() call.

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

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

  • elapsed_s – Wall-clock seconds spent searching.

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

None

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

Bases: object

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

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

The locking script must be a plain script with NO Glyph/FT/dMint ref pushes (OP_PUSHINPUTREF*, opcodes d0/d1/d2/d3/d8 — see pyrxd.glyph.script.REF_OPCODES). Spending a token-bearing UTXO as fee silently destroys the token; the V1 mint builder validates this and raises InvalidFundingUtxoError if the funding script carries any ref envelope.

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

  • vout – output index

  • value – photons locked in the UTXO

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

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

None

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

Bases: object

Output of build_dmint_mint_tx().

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

  • updated_state – New DmintState written into the contract output (height incremented, target updated if DAA is active). On the final mint no output carries a state: this is the spent state with only height advanced to max_height (so is_exhausted is True).

  • contract_script – The script of output 0: the recreated contract (state + separator + code), or on the final mint the burn d8 <contractRef> 6a that 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 standard Transaction.sign() is not appropriate. The caller must either set the unlocking script directly or use a custom signing path. See docstring of build_dmint_mint_tx() for details.

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

None

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

Bases: object

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

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

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

None

classmethod from_script(script_bytes)[source]

Parse a dMint contract UTXO script into a DmintState.

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

Parameters:

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

Raises:

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

Return type:

DmintState

property is_exhausted: bool
is_v1: bool = False
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> 6a in its place (see build_dmint_contract_burn_script()), and build_dmint_mint_tx() builds exactly that.

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

Bases: object

Just-deployed state of a V1 dMint contract template.

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

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

  • reward_sats – Photons emitted per successful mint.

  • 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)
Parameters:
Return type:

None

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

Bases: object

Progress snapshot during a real grind: MEASURED rate, PROJECTED remainder.

observed_hashes_per_second is 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: attempts already burned do not reduce remaining_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 (None if 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)
Parameters:
Return type:

None

attempts: int
elapsed_s: float
observed_hashes_per_second: float
remaining_mean_s: float | None
remaining_quantile_s: tuple[tuple[float, float], ...]
class pyrxd.glyph.dmint.MintEtaProjection[source]

Bases: object

PROJECTED wall-clock times. Every field is a projection.

aggregate_hashes_per_second assumes linear scaling across workers, 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 below single-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)
Parameters:
Return type:

None

workers: int
single_core_hashes_per_second: float
aggregate_hashes_per_second: float
mean_s: float
quantile_s: tuple[tuple[float, float], ...]
class pyrxd.glyph.dmint.PowPreimageResult[source]

Bases: object

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

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

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

  • input_hash – SHA256d(input_script) — push as scriptSig_inputHash.

  • output_hash – SHA256d(output_script) — push as scriptSig_outputHash.

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

None

preimage: bytes
input_hash: bytes
output_hash: bytes
class pyrxd.glyph.dmint.Sha256dBenchmark[source]

Bases: object

MEASURED single-core SHA256d throughput on this machine.

Every field here was observed, not assumed. Nothing in it accounts for multiple cores — see MintEtaProjection for 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)
Parameters:
Return type:

None

hashes: int
elapsed_s: float
hashes_per_second: float
first_digest: bytes
exception pyrxd.glyph.dmint.V2UnvalidatedWarning[source]

Bases: UserWarning

Retained warning category for V2 dMint code paths.

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

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

pyrxd.glyph.dmint.attempts_for_quantile(target, quantile)[source]

Attempts needed to hit with probability quantile. EXACT.

The smallest n with 1 - (1-p)**n >= quantile, i.e. ceil(ln(1-quantile) / ln(1-p)). Both logs go through math.log1p(), which keeps full precision at the p ~ 1e-10 scale where log(1 - p) would round to log(1.0) == 0 and the division would blow up.

Parameters:
  • target (int) – PoW target (clamped to MAX_SHA256D_TARGET).

  • quantile (float) – Strictly between 0 and 1.

Raises:

ValidationError – target < 1 or quantile out of range.

Return type:

int

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 in pyrxd.contrib.miner.parallel._worker. Benchmarking anything else (a bare sha256, a stand-in payload) would report a rate the miner never achieves.

The first batch is deliberately one hash wide so first_digest falls 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_s is not positive, preimage is not 64 bytes, nonce_width is not 4 or 8, or the clock did not advance.

Return type:

Sha256dBenchmark

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

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

Parameters:

params (DmintDeployParams)

Return type:

bytes

pyrxd.glyph.dmint.build_dmint_contract_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 equals max_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 reads OP_OUTPUTBYTECODE at the contract’s output index and OP_EQUALVERIFYs it against 0xd8 || contractRef || 0x6a assembled 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’s burnScript (src/glyph.ts).

Parameters:

contract_ref (GlyphRef)

Return type:

bytes

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

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

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

Parameters:

params (DmintDeployParams)

Return type:

bytes

pyrxd.glyph.dmint.build_dmint_mint_tx(contract_utxo, nonce, miner_pkh, current_time, fee_rate=10000, *, funding_utxo=None, op_return_msg=None, half_life=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 to miner_pkh from a separate plain-RXD funding input. V1 and V2 use the same consensus shape (the V2 covenant’s output-validation block is byte-identical to V1’s); they differ only in the nonce width (4B V1 / 8B V2) and the 10-item V2 state.

Transaction structure (both V1 and V2)

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

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

Outputs
  • Output 0: recreated contract (the spent script with its state advanced: height + 1, and on V2 also lastTime and target as the note below says), value 1.

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

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

  • Output 3: change back to miner_pkh.

The final mint (height + 1 == max_height, flagged is_final_mint on the result) has the same inputs and the same outputs 1-3, but output 0 is the burn build_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 a lastTime its 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_time from OP_TXLOCKTIME and its target from the alt-stack DAA result, so current_time IS the block locktime: it is written into the recreated state’s last_time AND set as the tx nLockTime (the two must agree), and for DAA modes it drives the target retarget. The final mint recreates no state: current_time is still set as the nLockTime, and Part B, which runs on the final mint too, still reads it in the modes whose retarget does, but no last_time or target is written. current_time must be <= 0x7FFFFFFF on every V2 mint (on a mint that recreates the contract, Part C’s NUM2BIN(_, 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 reads lastTime (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 passes epoch_length/max_adjustment_log2 or schedule (and half_life for ASERT) matching the deployed contract — a mismatch is caught before the PoW grind.

Note

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_*_v2 or *_legacy — then byte-verifies the supplied half_life against 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_life defaults to None — 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. Supplying half_life explicitly 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 lastTime this mint’s retarget reads and which is not a readable lastTime (that contract can no longer be minted at all); a current_time earlier than the contract’s last_time on 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:

  1. Build the unsigned tx shell via this function.

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

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

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

  5. Broadcast.

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

param contract_utxo:

The live dMint contract UTXO to spend.

param nonce:

8-byte PoW nonce, 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 nLockTime on every mint and, on a mint that recreates the contract, as the recreated state’s lastTime (the covenant rebuilds it from OP_TXLOCKTIME); drives the DAA retarget. The final mint writes no lastTime. 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_exhausted is True.

raises ValidationError:

nonce is not 8 bytes (4 for V1); miner_pkh is not 20 bytes.

returns:

DmintMintResult with the unsigned tx and updated state.

Parameters:
Return type:

DmintMintResult

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

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

Layout (canonical redesign §4.2):

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

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

Parameters:

params (DmintDeployParams)

Return type:

bytes

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

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

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

Raises:

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

Parameters:

algo (DmintAlgo)

Return type:

bytes

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

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

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

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

Parameters:
Return type:

bytes

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

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

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

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

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

Raises:

ValidationError – miner_pkh is not 20 bytes.

Parameters:
Return type:

bytes

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

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

The V1 covenant binds the PoW preimage to:

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

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

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

Layout (matches build_pow_preimage()):

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

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

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

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

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

Returns:

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

Raises:

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

Return type:

PowPreimageResult

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

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

Layout:

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-era dMintScript builds (its SHA256d/FIXED branch, packages/lib/src/script.ts at c8540a6: push4bytes(height), then pushMinimal for maxHeight, reward and target), and tests/test_dmint_v1_target_push.py checks 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 least 2**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 in tests/test_dmint_v1_target_push.py have been minted over a thousand times each with a 7-byte one. Until 2026-09-23 pyrxd pushed every target as 08 + 8 bytes, which is not minimal below 2**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; target not in [1, MAX_SHA256D_TARGET]. The upper target bound is MAX_SHA256D_TARGET = 0x7fff...ff rather than 2**64 because Bitcoin script integers are signed: pushing a value with the high bit set produces a negative number on the stack, and the on-chain target comparison would behave wrongly. Photonic Wallet’s dMintDiffToTarget formula always produces a value in this signed-positive range. A target of 0 is refused by pyrxd: only a hash whose compared 8 bytes are all zero meets it.

Parameters:
Return type:

bytes

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

Build the V2 mining preimage AND scriptSig hashes.

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

Layout (matches build_pow_preimage()):

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

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

Note

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

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

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

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

Returns:

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

Raises:

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

Return type:

PowPreimageResult

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

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

Format (SHA256d):

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

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

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

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

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

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

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

Return type:

bytes

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

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

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

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

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

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

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

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

Returns:

PowPreimageResult with preimage, input_hash, output_hash.

Return type:

PowPreimageResult

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

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_tx dispatches here when detect_contract_daa_bytecode() reports DaaBytecodeVersion.LEGACY; new deploys use compute_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 naive target << drift would overshoot MAX).

Both subtractions go through _script_int64(), as in the v2 mirror: for a target_time near the int64 limit the on-chain OP_SUB aborts, so this raises ValidationError rather than return a target the contract cannot compute.

Note

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

Parameters:
  • current_target (int)

  • last_time (int)

  • current_time (int)

  • target_time (int)

  • half_life (int)

Return type:

int

pyrxd.glyph.dmint.compute_next_target_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_tx dispatches here when detect_contract_daa_bytecode() reports DaaBytecodeVersion.LEGACY; new deploys use compute_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 naive target << drift would overshoot MAX).

Both subtractions go through _script_int64(), as in the v2 mirror: for a target_time near the int64 limit the on-chain OP_SUB aborts, so this raises ValidationError rather than return a target the contract cannot compute.

Note

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

Parameters:
  • current_target (int)

  • last_time (int)

  • current_time (int)

  • target_time (int)

  • half_life (int)

Return type:

int

pyrxd.glyph.dmint.compute_next_target_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 Photonic buildAsertDaaBytecode (script.ts at becf41a), and an exact port of the reference computeAsertV2Target in packages/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_div is load-bearing: OP_DIV truncates toward zero while Python // floors, and they differ for the negative excess of 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 raises ValidationError at the same points via _script_int64() so it never predicts an unmineable state):

  • timeDelta = current_time - last_time: for an ASERT/LWMA mint build_dmint_mint_tx requires both current_time and the state’s last_time to be readable lastTimes (is_readable_last_time: [2^23, 2^31 - 1]), so |timeDelta| <= 2^31 - 1 in either direction.

  • excess = timeDelta - target_time: target_time is a state script number in [1, 2^63 - 1], so excess is in [-(2^63 - 1) - (2^31 - 1), 2^31 - 2] and the OP_SUB underflows only for target_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 for target_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| since half_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 excess chain for an absurd target_time, and it is reported rather than mis-predicted.

Note

V2-only DAA. current_target must 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.

Parameters:
  • current_target (int)

  • last_time (int)

  • current_time (int)

  • target_time (int)

  • half_life (int)

Return type:

int

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

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

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

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

N = max_adjustment_log2 (1..4). clampedDelta >= target_time >> N >= 0 and the divide’s operands are positive, so floor == OP_DIV’s truncate-toward-zero. The lower bound is 0 when target_time < 2**N; pyrxd refuses to deploy such a contract, and build_dmint_mint_tx refuses any mint whose next target would be 1. The OP_SUB and the N x OP_2MUL of target_time go through _script_int64(), so an input the contract cannot evaluate raises ValidationError. The target is clamped to EPOCH_MAX_SAFE_TARGET (2^48) on BOTH sides of the multiply and the divide runs first, so the on-chain int64 multiply never overflows (Radiant-Core/Photonic-Wallet#2). Capping the output at 2^48 keeps target there for the next epoch (difficulty floor 32768).

Note

V2-only DAA.

Parameters:
  • current_target (int)

  • last_time (int)

  • current_time (int)

  • target_time (int)

  • height (int)

  • epoch_length (int)

  • max_adjustment_log2 (int)

Return type:

int

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

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 the OP_0 OP_MAX timeDelta floor) AND of _build_linear_daa_legacy_prefloor() (without it) for every current_time >= last_time. On a negative delta the two variants differ; this mirror gives the floored variant’s answer, and build_dmint_mint_tx does 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 deploy dea3beb9… carries the pre-floor variant and is mined with this formula. New deploys use compute_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 on timeDelta mirrors the on-chain OP_0 OP_MAX (Radiant-Core/Photonic-Wallet#2): a backwards-clock block (locktime earlier than the previous mint) gives a negative delta that would otherwise underflow the on-chain int64 multiply.

The two operations the bytecode can abort on — the OP_SUB of the times and the OP_4 OP_MUL of target_time — go through _script_int64(), so an input the contract cannot evaluate raises ValidationError instead of returning a target.

Note

V2-only DAA.

Parameters:
  • current_target (int)

  • last_time (int)

  • current_time (int)

  • target_time (int)

Return type:

int

pyrxd.glyph.dmint.compute_next_target_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 the OP_0 OP_MAX timeDelta floor) AND of _build_linear_daa_legacy_prefloor() (without it) for every current_time >= last_time. On a negative delta the two variants differ; this mirror gives the floored variant’s answer, and build_dmint_mint_tx does 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 deploy dea3beb9… carries the pre-floor variant and is mined with this formula. New deploys use compute_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 on timeDelta mirrors the on-chain OP_0 OP_MAX (Radiant-Core/Photonic-Wallet#2): a backwards-clock block (locktime earlier than the previous mint) gives a negative delta that would otherwise underflow the on-chain int64 multiply.

The two operations the bytecode can abort on — the OP_SUB of the times and the OP_4 OP_MUL of target_time — go through _script_int64(), so an input the contract cannot evaluate raises ValidationError instead of returning a target.

Note

V2-only DAA.

Parameters:
  • current_target (int)

  • last_time (int)

  • current_time (int)

  • target_time (int)

Return type:

int

pyrxd.glyph.dmint.compute_next_target_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 Photonic buildLinearDaaBytecode at becf41a) and a port of computeLwmaV2Target in packages/lib/src/dmintDaaV2.ts: exactly compute_next_target_asert_v2() with the responsiveness gain auto-set to target_time — driftFp = (excess * RADIX) / target_time — so a block twice the target time hits the ±25% clamp. Same int64 proof (target_time >= 1 is 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.

Parameters:
  • current_target (int)

  • last_time (int)

  • current_time (int)

  • target_time (int)

Return type:

int

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

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

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

Note

V2-only DAA.

Parameters:
Return type:

int

pyrxd.glyph.dmint.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_mode and the state/code boundary) and classifies the code section with pyrxd.glyph.dmint.builders.detect_daa_bytecode(). For ASERT the result also carries the half_life read 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_tx byte-verifies those by rebuilding Part B from the deploy parameters).

  • UnrecognizedDaaBytecodeError – the fragment matches no known generation.

Parameters:

contract_script (bytes)

Return type:

DetectedDaaBytecode

pyrxd.glyph.dmint.detect_daa_bytecode(code, daa_mode)[source]

Classify which ASERT/LWMA retarget-formula generation a V2 contract bakes.

code is the contract’s code section — the bytes AFTER the 0xbd OP_STATESEPARATOR (what build_dmint_code_script returns and what follows the parsed state in a UTXO script). daa_mode is 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 excess preamble c5 5279 94 5379 94, v2 has <RADIX push> OP_MUL = 03000001 95; legacy has <halfLife push> OP_DIV (5th byte 96, never 95 — 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 no excess at all — c5 5279 94 5379 54 95 a3 (OP_4 OP_MUL OP_MIN), with (LEGACY) or without (LEGACY_LWMA_PREFLOOR) the 00 a4 timeDelta floor.

Mirrors Photonic’s Glyph-miner extractDaaParamsFromCodeScript in intent, with one deliberate difference from the Radiant-Core fork this was ported from: that fork’s _asert_version_of_code classified 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 with UnrecognizedDaaBytecodeError (naming the bytes).

Raises:
  • ValueError – daa_mode is 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:

DetectedDaaBytecode

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

Convert difficulty to PoW target: MAX_SHA256D_TARGET // difficulty for every algo.

algo is accepted for backward compatibility and does not change the result — see target_for_difficulty(), the single definition this and DmintDeployParams.initial_target share. BLAKE3/K12 once got (2**256 - 1) // difficulty here, a target no dMint covenant can read.

Parameters:
Return type:

int

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**96 and E[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:
  • target (int) – The contract’s target state field.

  • quantiles (tuple[float, ...]) – Quantiles to report, each in (0, 1).

Raises:

ValidationError – target < 1 or a quantile is out of range.

Return type:

AttemptEstimate

>>> 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. One get_utxos call per contract. Use this shape immediately after deploy to verify all N contracts went live, or any time the caller has the deploy params handy.

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

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

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

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

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

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

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

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

Returns:

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

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

  • NetworkError – Propagated from the ElectrumX client.

Return type:

list[DmintContractUtxo]

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

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

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

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

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

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

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

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

Returns:

The largest qualifying funding UTXO.

Raises:

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

Return type:

DmintMinerFundingUtxo

pyrxd.glyph.dmint.is_minimal_4byte_scriptnum(n)[source]

Is n’s FIXED 4-byte little-endian push a minimally encoded CScriptNum?

The V2 dMint state script pushes lastTime as a fixed 04 <4B LE> (see builders._push_4bytes_le), and the ASERT/LWMA/EPOCH retarget fragments read that item back as a NUMBER (OP_2 OP_PICK; OP_SUB — see builders._V2_EXCESS_PREAMBLE). Radiant-Core builds the operand with CScriptNum(vch, fRequireMinimal=true) because SCRIPT_VERIFY_MINIMALDATA is in MANDATORY_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 +255 encodes as ff00).

Over [0, 0x7FFFFFFF] — the locktimes Part C’s NUM2BIN(_, 4) can write back — this is exactly n >= 2**23; that equality is not hard-coded here, it is derived by this predicate and pinned in tests/test_dmint_daa_v2_resync.py. The 2026-09-21 review measured the same boundary against a real radiant-core node: last_time=8388608 accepted, 8388607 rejected with mandatory-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 mean n: its top bit is the script-number SIGN, so the covenant reads it as a negative number while DmintState parses it unsigned. To ask whether a lastTime reads back as the number pyrxd wrote, use is_readable_last_time().

Parameters:

n (int)

Return type:

bool

pyrxd.glyph.dmint.is_readable_last_time(n)[source]

Does a V2 state’s fixed 4-byte lastTime push read back, as a number, as n?

True exactly when the push is minimally encoded (is_minimal_4byte_scriptnum()) AND bit 31 is clear, i.e. 2**23 <= n <= 0x7FFFFFFF. Below 2**23 the retarget’s CScriptNum read 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.

Parameters:

n (int)

Return type:

bool

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

Return True if script uses any OP_PUSHINPUTREF-family opcode.

Walks the script as an opcode stream so that only opcode position bytes are checked against 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 also count_input_refs for the exactly-which-refs covenant guard).

Parameters:

script (bytes)

Return type:

bool

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 over elapsed_s — so no scaling assumption is involved. The remaining-time fields are still projections, and they do not shrink as attempts grows: 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 attempts or elapsed_s.

Return type:

LiveMiningStats

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 target state field).

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

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

  • max_attempts (int) – Upper bound on iterations before raising MaxAttemptsError. See DEFAULT_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 every progress_interval_s while grinding. Feed the pair to live_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 – preimage is not 64 bytes, target is not positive, nonce_width is not 4 or 8, max_attempts is < 1, or progress_interval_s is not positive.

  • NotImplementedError – algo is BLAKE3 or K12.

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

Return type:

DmintMineResult

Note

There is no “easy” target for this loop. The verifier requires four leading zero bytes, so the mean is 2**96 / target and floors at 2**33 ≈ 8.6e9 attempts 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 with estimate_attempts() before starting it, and use benchmark_sha256d() (or pyrxd 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() or mine_solution_external() directly. The two paths share semantics — both return a DmintMineResult with a nonce that satisfies the target — but have disjoint parameter sets (max_attempts vs timeout_s, no-argv vs argv). Picking between them was a 30-line wrapper that every demo and operator script ended up rewriting; this function is that wrapper, with the branch in one place.

Dispatch rule:

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

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

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

  • target (int) – The PoW target.

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

  • algo (DmintAlgo) – The contract’s PoW algorithm. Only SHA256D can be mined: BLAKE3 and K12 raise NotImplementedError on 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 to subprocess.run() for the external miner. Use [sys.executable, "-m", "pyrxd.contrib.miner"] for the bundled parallel miner.

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

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

  • 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 to mine_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 as progress=None.

  • progress_interval_s (float) – Minimum seconds between progress calls.

Returns:

DmintMineResult with the verified nonce.

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

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

  • NotImplementedError – algo is BLAKE3 or K12 (either path).

Return type:

DmintMineResult

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

The miner is expected to:

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

  2. Search for a valid nonce.

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

  4. 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 $PATH can intercept calls. The local nonce re-verification (below) defends against the miner returning a wrong nonce, but cannot detect side-channel exfiltration: a malicious miner sees the preimage (which encodes the contract ref + miner binding) and can leak it out-of-band over the network.

Mitigations the caller should consider:

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

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

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

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

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

  • target (int) – The PoW target.

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

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

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

  • progress (Callable[[int, float], None] | None) – Optional callback(attempts, elapsed_s). When None (the default), behavior is byte-for-byte identical to before this parameter existed: a single blocking subprocess.run call 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 calls progress with the most recently observed one roughly every progress_interval_s — the same cadence contract mine_solution() documents. An external miner that never emits a progress frame (the common case today) simply means progress is never called; the grind still runs to completion or timeout exactly as it would with progress=None. A raising callback propagates and the subprocess is terminated — the supported way to impose a deadline, mirroring mine_solution() and pyrxd.contrib.miner.parallel.mine.

  • progress_interval_s (float) – Minimum seconds between progress calls. Ignored when progress is None.

  • 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 – algo is BLAKE3 or K12.

  • MaxAttemptsError – The miner exceeded timeout_s.

  • FileNotFoundError – miner_argv[0] is not on PATH.

Return type:

DmintMineResult

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:

  1. aggregate = single_core × workers assumes linear cross-core scaling. pyrxd does not measure that, and real machines do not achieve it.

  2. Even given a perfect rate, the completion time is a random variable. mean_s is its mean; the quantiles bracket it. There is no single “time remaining”.

Parameters:
Raises:

ValidationError – non-positive rate or workers < 1.

Return type:

MintEtaProjection

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 of algo (accepted for backward compatibility only).

Parameters:
Return type:

int

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

Verify a SHA256d PoW solution.

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

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

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

  • preimage (bytes)

  • nonce (bytes)

  • target (int)

Return type:

bool

The subpackage layers as types ← builders ← chain ← miner ← 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: UserWarning

Retained warning category for V2 dMint code paths.

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

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

pyrxd.glyph.dmint.types.MAX_SCRIPT_NUM_BYTES = 8

CScriptNum refuses an operand longer than MAXIMUM_ELEMENT_SIZE_64_BIT = 8 bytes (Radiant Core script.h, vendored at tests/vendor/radiant_core/script.h), so the largest script number is 2**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 as 04 || NUM2BIN(height + 1, 4) (epilogue 54 78 54 80 7e). OP_NUM2BIN aborts with IMPOSSIBLE_ENCODING when the minimal encoding of the number is longer than the size asked for (Radiant Core interpreter.cpp, vendored at tests/vendor/radiant_core/), and 2**31 needs five bytes. The last mint, the one whose new height equals max_height, takes the epilogue’s other branch and writes no height, so a contract with max_height = 2**31 makes its last mint from height 2**31 - 1. With a larger max_height the mint from 2**31 - 1 is not the last, needs NUM2BIN(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_height whose last mint a V1 contract can reach

pyrxd.glyph.dmint.types.MAX_V2_TARGET_TIME = 4294967295

Upper bound on a V2 deploy’s target_time in the modes whose retarget reads it as a number (DAA_MODES_READING_TARGET_TIME: ASERT, LWMA, EPOCH). There target_time is a spacing in seconds that the retarget compares with the difference of two timestamps, and in any mint the covenant accepts both are below 2**31: the state’s lastTime is 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 with NUM2BIN(…, 4). So no mint observes a spacing of 2**31 seconds or more. The cap is looser than that, at 0xFFFFFFFF: every value it refuses is a spacing no mint can meet, and the values between 2**31 and 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 doubles targetTime up to four times), and it keeps every intermediate of those retargets inside int64 for any pair of such timestamps. FIXED and SCHEDULE never read targetTime as a number, so this bound does not apply to them. (Photonic’s Mint form offers 10..3600; tests/test_dmint_deploy_bounds.py pins 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: IntEnum

Which 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, Photonic c90e6506); 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 the OP_0 OP_MAX timeDelta 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 deploy dea3beb9….

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 encoded CScriptNum?

The V2 dMint state script pushes lastTime as a fixed 04 <4B LE> (see builders._push_4bytes_le), and the ASERT/LWMA/EPOCH retarget fragments read that item back as a NUMBER (OP_2 OP_PICK; OP_SUB — see builders._V2_EXCESS_PREAMBLE). Radiant-Core builds the operand with CScriptNum(vch, fRequireMinimal=true) because SCRIPT_VERIFY_MINIMALDATA is in MANDATORY_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 +255 encodes as ff00).

Over [0, 0x7FFFFFFF] — the locktimes Part C’s NUM2BIN(_, 4) can write back — this is exactly n >= 2**23; that equality is not hard-coded here, it is derived by this predicate and pinned in tests/test_dmint_daa_v2_resync.py. The 2026-09-21 review measured the same boundary against a real radiant-core node: last_time=8388608 accepted, 8388607 rejected with mandatory-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 mean n: its top bit is the script-number SIGN, so the covenant reads it as a negative number while DmintState parses it unsigned. To ask whether a lastTime reads back as the number pyrxd wrote, use is_readable_last_time().

Parameters:

n (int)

Return type:

bool

pyrxd.glyph.dmint.types.is_readable_last_time(n)[source]

Does a V2 state’s fixed 4-byte lastTime push read back, as a number, as n?

True exactly when the push is minimally encoded (is_minimal_4byte_scriptnum()) AND bit 31 is clear, i.e. 2**23 <= n <= 0x7FFFFFFF. Below 2**23 the retarget’s CScriptNum read 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.

Parameters:

n (int)

Return type:

bool

pyrxd.glyph.dmint.types.DAA_MODES_READING_DEPLOY_LAST_TIME = frozenset({DaaMode.ASERT, DaaMode.LWMA})

The DAA modes whose retarget fragment reads lastTime on 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 a CScriptNum from the state’s lastTime. EPOCH reads it only inside a branch gated on height > 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 the lastTime its own mint wrote. (That second value IS read by EPOCH at each boundary — see DAA_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 reading lastTime unconditionally 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 lastTime as a number on SOME mint — so a lastTime a 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 > 0 with height % epochLength == 0). SCHEDULE and FIXED never read it. build_dmint_mint_tx refuses to write an unreadable lastTime (is_readable_last_time()) for these modes. Membership is DERIVED by running every generation of every mode’s fragment under the int64/MINIMALDATA evaluator in tests/test_dmint_daa_offchain_onchain_differential.py with 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 targetTime as 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 targetTime is only ever carried as bytes: Part A never picks it, Part B4 drops it with OP_DROP, and Part C copies it inside the baked middle literal. MAX_V2_TARGET_TIME applies to these modes only. Membership is DERIVED by running every generation of every mode’s fragment under the int64/MINIMALDATA evaluator with a targetTime too 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 Photonic dMintDiffToTarget (MAX_TARGET / BigInt(difficulty), packages/lib/src/script.ts at becf41a7), which takes no algorithm argument, and the BLAKE3/K12 V2 contracts on mainnet carry exactly these targets (e.g. a declared diff of 2 → 0x3fffffffffffffff).

Parameters:

difficulty (int)

Return type:

int

pyrxd.glyph.dmint.types.check_dmint_core_bounds(*, stage, max_height, reward, difficulty, names=None)[source]

Refuse a max_height, reward or difficulty no dMint contract, V1 or V2, can use.

Called from check_v2_numeric_bounds() (so from every V2 deploy path) and from check_dmint_v1_bounds() (every V1 deploy path), so the two versions refuse the same values with the same words. names maps a parameter to the name the caller knows it by (reward -> reward_photons or --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 exactly reward photons (OP_CODESCRIPTHASHVALUESUM_OUTPUTS … OP_NUMEQUALVERIFY; V1: 76 e4 7b 9d), and Radiant consensus refuses a transaction whose outputs total more than MAX_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_height and reward to 0xFFFFFF, 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, which tests/test_dmint_v1_target_push.py rebuilds byte for byte. V1 has one tighter limit of its own, on max_height: see check_dmint_v1_bounds().

Parameters:
  • stage (str)

  • max_height (int)

  • reward (int)

  • difficulty (int)

  • names (Mapping[str, str] | None)

Return type:

None

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), then max_height <= MAX_V1_MAX_HEIGHT (2**31), past which a V1 contract stops at height 2**31 - 1 with mints left (that constant has the reason). Called from DmintV1DeployParams.__post_init__ (every V1 deploy is built from one) and from deploy-dmint for 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 height 2**31 - 1, and refuses the mint from there (miner._unmintable_reason).

Parameters:
  • stage (str)

  • max_height (int)

  • reward (int)

  • difficulty (int)

  • names (Mapping[str, str] | None)

Return type:

None

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, reward and difficulty — are check_dmint_core_bounds(), which this calls first. Called from DmintDeployParams.__post_init__ (which both V2 deploy scripts — the fee placeholder and the real reveal — are built from), from DmintV2DeployParams.__post_init__ (so the API refuses on the caller’s own object) and from deploy-dmint --v2 (so the CLI refuses before any wallet or network work, naming the flag the user typed). names maps 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_minimal separately refuses to emit any number wider than MAX_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.py pins the observed values as accepted):

  • max_height, reward, difficulty — see check_dmint_core_bounds().

  • target_time <= MAX_V2_TARGET_TIME in 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 carry targetTime as bytes, so there it is held only to MAX_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).

Parameters:
Return type:

None

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

Bases: object

Parameters for deploying a V2 dMint contract.

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

The deploy target for difficulty — target_for_difficulty(), for every algo.

BLAKE3 and K12 used to get MAX_V2_TARGET_256 // difficulty here: a target wider than the 8 bytes Part B2 can read, so the contract could never be minted. algo selects 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: object

The dmint object embedded in Glyph V2 token metadata CBOR.

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

Field names mirror Photonic Wallet’s DmintPayload type (packages/lib/src/types.ts at becf41a):

daa?: { mode, targetBlockTime,
        halfLife?, asymptote?,        // ASERT
        windowSize?,                  // LWMA
        epochLength?, maxAdjustment?, // EPOCH
        schedule?: { height, difficulty }[] }  // SCHEDULE

Every 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, schedule were added; FIXED/ASERT/LWMA payloads that do not set them are unchanged). schedule entries are (height, difficulty) — difficulty, not target, is what the payload type declares.

max_adjustment is 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.ts maxAdjustmentToLog2; its Mint UI passes the same number to the payload and the builder), while pyrxd’s deploy-dmint --max-adjustment is a multiplier baked as DmintDeployParams.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.

algo: DmintAlgo
num_contracts: int
max_height: int
reward: int
premine: int
diff: int
daa_mode: DaaMode = 0
target_block_time: int = 60
half_life: int = 0
window_size: int = 0
asymptote: int = 0
epoch_length: int = 0
max_adjustment: int = 0
schedule: tuple[tuple[int, int], ...] = ()
to_cbor_dict()[source]

Encode to the dict that becomes the dmint CBOR value.

Return type:

dict

classmethod from_cbor_dict(d)[source]

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

Parameters:

d (dict)

Return type:

DmintCborPayload

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

None

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

Bases: object

Output of build_dmint_mint_tx().

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

  • updated_state – New DmintState written into the contract output (height incremented, target updated if DAA is active). On the final mint no output carries a state: this is the spent state with only height advanced to max_height (so is_exhausted is True).

  • contract_script – The script of output 0: the recreated contract (state + separator + code), or on the final mint the burn d8 <contractRef> 6a that 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 standard Transaction.sign() is not appropriate. The caller must either set the unlocking script directly or use a custom signing path. See docstring of build_dmint_mint_tx() for details.

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

None

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

Bases: object

Just-deployed state of a V1 dMint contract template.

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

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

  • reward_sats – Photons emitted per successful mint.

  • 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.

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

None

Covenant & transaction builders

Script-construction primitives for the dMint subpackage.

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

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

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

Symbols (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: object

What 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 (None for 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.

daa_mode: DaaMode
version: DaaBytecodeVersion
daa_bytes: bytes
half_life: int | None = None
__init__(daa_mode, version, daa_bytes, half_life=None)
Parameters:
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.

code is the contract’s code section — the bytes AFTER the 0xbd OP_STATESEPARATOR (what build_dmint_code_script returns and what follows the parsed state in a UTXO script). daa_mode is 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 excess preamble c5 5279 94 5379 94, v2 has <RADIX push> OP_MUL = 03000001 95; legacy has <halfLife push> OP_DIV (5th byte 96, never 95 — 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 no excess at all — c5 5279 94 5379 54 95 a3 (OP_4 OP_MUL OP_MIN), with (LEGACY) or without (LEGACY_LWMA_PREFLOOR) the 00 a4 timeDelta floor.

Mirrors Photonic’s Glyph-miner extractDaaParamsFromCodeScript in intent, with one deliberate difference from the Radiant-Core fork this was ported from: that fork’s _asert_version_of_code classified 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 with UnrecognizedDaaBytecodeError (naming the bytes).

Raises:
  • ValueError – daa_mode is 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:

DetectedDaaBytecode

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

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

Layout (canonical redesign §4.2):

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

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

Parameters:

params (DmintDeployParams)

Return type:

bytes

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

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

Parameters:

params (DmintDeployParams)

Return type:

bytes

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

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

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

Parameters:

params (DmintDeployParams)

Return type:

bytes

pyrxd.glyph.dmint.builders.build_dmint_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 equals max_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 reads OP_OUTPUTBYTECODE at the contract’s output index and OP_EQUALVERIFYs it against 0xd8 || contractRef || 0x6a assembled 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’s burnScript (src/glyph.ts).

Parameters:

contract_ref (GlyphRef)

Return type:

bytes

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

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

Layout:

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-era dMintScript builds (its SHA256d/FIXED branch, packages/lib/src/script.ts at c8540a6: push4bytes(height), then pushMinimal for maxHeight, reward and target), and tests/test_dmint_v1_target_push.py checks 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 least 2**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 in tests/test_dmint_v1_target_push.py have been minted over a thousand times each with a 7-byte one. Until 2026-09-23 pyrxd pushed every target as 08 + 8 bytes, which is not minimal below 2**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; target not in [1, MAX_SHA256D_TARGET]. The upper target bound is MAX_SHA256D_TARGET = 0x7fff...ff rather than 2**64 because Bitcoin script integers are signed: pushing a value with the high bit set produces a negative number on the stack, and the on-chain target comparison would behave wrongly. Photonic Wallet’s dMintDiffToTarget formula always produces a value in this signed-positive range. A target of 0 is refused by pyrxd: only a hash whose compared 8 bytes are all zero meets it.

Parameters:
Return type:

bytes

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

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

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

Raises:

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

Parameters:

algo (DmintAlgo)

Return type:

bytes

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

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

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

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

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

Raises:

ValidationError – miner_pkh is not 20 bytes.

Parameters:
Return type:

bytes

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

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

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

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

Parameters:
Return type:

bytes

Contract & chain state

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

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

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

Symbols (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.parallel implements the same shape and must stay importable without pulling in pyrxd.glyph.dmint (its workers re-import the module on every spawn). Feed the pair to live_stats() to turn it into an observed rate plus remaining-time quantiles.

Type:

Live-progress hook

alias of Callable[[int, float], None]

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 in pyrxd.fee_sizing, exactly as pyrxd.glyph.builder.MIN_FEE_RATE, pyrxd.glyph.ft.MIN_FEE_RATE and pyrxd.wallet.DEFAULT_FEE_RATE are.

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: object

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

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

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

  • input_hash – SHA256d(input_script) — push as scriptSig_inputHash.

  • output_hash – SHA256d(output_script) — push as scriptSig_outputHash.

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

None

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

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

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

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

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

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

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

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

Returns:

PowPreimageResult with preimage, input_hash, output_hash.

Return type:

PowPreimageResult

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

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

Format (SHA256d):

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

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

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

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

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

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

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

Return type:

bytes

pyrxd.glyph.dmint.miner.compute_next_target_asert_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_tx dispatches here when detect_contract_daa_bytecode() reports DaaBytecodeVersion.LEGACY; new deploys use compute_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 naive target << drift would overshoot MAX).

Both subtractions go through _script_int64(), as in the v2 mirror: for a target_time near the int64 limit the on-chain OP_SUB aborts, so this raises ValidationError rather than return a target the contract cannot compute.

Note

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

Parameters:
  • current_target (int)

  • last_time (int)

  • current_time (int)

  • target_time (int)

  • half_life (int)

Return type:

int

pyrxd.glyph.dmint.miner.compute_next_target_linear_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 the OP_0 OP_MAX timeDelta floor) AND of _build_linear_daa_legacy_prefloor() (without it) for every current_time >= last_time. On a negative delta the two variants differ; this mirror gives the floored variant’s answer, and build_dmint_mint_tx does 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 deploy dea3beb9… carries the pre-floor variant and is mined with this formula. New deploys use compute_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 on timeDelta mirrors the on-chain OP_0 OP_MAX (Radiant-Core/Photonic-Wallet#2): a backwards-clock block (locktime earlier than the previous mint) gives a negative delta that would otherwise underflow the on-chain int64 multiply.

The two operations the bytecode can abort on — the OP_SUB of the times and the OP_4 OP_MUL of target_time — go through _script_int64(), so an input the contract cannot evaluate raises ValidationError instead of returning a target.

Note

V2-only DAA.

Parameters:
  • current_target (int)

  • last_time (int)

  • current_time (int)

  • target_time (int)

Return type:

int

pyrxd.glyph.dmint.miner.compute_next_target_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 _v2 functions, or let build_dmint_mint_tx dispatch on detect_contract_daa_bytecode(). Kept as aliases rather than repointed at v2 because repointing would change results for legacy contracts without a trace.

Parameters:
  • current_target (int)

  • last_time (int)

  • current_time (int)

  • target_time (int)

  • half_life (int)

Return type:

int

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

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 the OP_0 OP_MAX timeDelta floor) AND of _build_linear_daa_legacy_prefloor() (without it) for every current_time >= last_time. On a negative delta the two variants differ; this mirror gives the floored variant’s answer, and build_dmint_mint_tx does 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 deploy dea3beb9… carries the pre-floor variant and is mined with this formula. New deploys use compute_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 on timeDelta mirrors the on-chain OP_0 OP_MAX (Radiant-Core/Photonic-Wallet#2): a backwards-clock block (locktime earlier than the previous mint) gives a negative delta that would otherwise underflow the on-chain int64 multiply.

The two operations the bytecode can abort on — the OP_SUB of the times and the OP_4 OP_MUL of target_time — go through _script_int64(), so an input the contract cannot evaluate raises ValidationError instead of returning a target.

Note

V2-only DAA.

Parameters:
  • current_target (int)

  • last_time (int)

  • current_time (int)

  • target_time (int)

Return type:

int

pyrxd.glyph.dmint.miner.compute_next_target_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 Photonic buildAsertDaaBytecode (script.ts at becf41a), and an exact port of the reference computeAsertV2Target in packages/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_div is load-bearing: OP_DIV truncates toward zero while Python // floors, and they differ for the negative excess of 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 raises ValidationError at the same points via _script_int64() so it never predicts an unmineable state):

  • timeDelta = current_time - last_time: for an ASERT/LWMA mint build_dmint_mint_tx requires both current_time and the state’s last_time to be readable lastTimes (is_readable_last_time: [2^23, 2^31 - 1]), so |timeDelta| <= 2^31 - 1 in either direction.

  • excess = timeDelta - target_time: target_time is a state script number in [1, 2^63 - 1], so excess is in [-(2^63 - 1) - (2^31 - 1), 2^31 - 2] and the OP_SUB underflows only for target_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 for target_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| since half_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 excess chain for an absurd target_time, and it is reported rather than mis-predicted.

Note

V2-only DAA. current_target must 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.

Parameters:
  • current_target (int)

  • last_time (int)

  • current_time (int)

  • target_time (int)

  • half_life (int)

Return type:

int

pyrxd.glyph.dmint.miner.compute_next_target_linear_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 Photonic buildLinearDaaBytecode at becf41a) and a port of computeLwmaV2Target in packages/lib/src/dmintDaaV2.ts: exactly compute_next_target_asert_v2() with the responsiveness gain auto-set to target_time — driftFp = (excess * RADIX) / target_time — so a block twice the target time hits the ±25% clamp. Same int64 proof (target_time >= 1 is 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.

Parameters:
  • current_target (int)

  • last_time (int)

  • current_time (int)

  • target_time (int)

Return type:

int

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

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

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

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

N = max_adjustment_log2 (1..4). clampedDelta >= target_time >> N >= 0 and the divide’s operands are positive, so floor == OP_DIV’s truncate-toward-zero. The lower bound is 0 when target_time < 2**N; pyrxd refuses to deploy such a contract, and build_dmint_mint_tx refuses any mint whose next target would be 1. The OP_SUB and the N x OP_2MUL of target_time go through _script_int64(), so an input the contract cannot evaluate raises ValidationError. The target is clamped to EPOCH_MAX_SAFE_TARGET (2^48) on BOTH sides of the multiply and the divide runs first, so the on-chain int64 multiply never overflows (Radiant-Core/Photonic-Wallet#2). Capping the output at 2^48 keeps target there for the next epoch (difficulty floor 32768).

Note

V2-only DAA.

Parameters:
  • current_target (int)

  • last_time (int)

  • current_time (int)

  • target_time (int)

  • height (int)

  • epoch_length (int)

  • max_adjustment_log2 (int)

Return type:

int

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

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

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

Note

V2-only DAA.

Parameters:
Return type:

int

pyrxd.glyph.dmint.miner.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_mode and the state/code boundary) and classifies the code section with pyrxd.glyph.dmint.builders.detect_daa_bytecode(). For ASERT the result also carries the half_life read 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_tx byte-verifies those by rebuilding Part B from the deploy parameters).

  • UnrecognizedDaaBytecodeError – the fragment matches no known generation.

Parameters:

contract_script (bytes)

Return type:

DetectedDaaBytecode

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

Convert difficulty to PoW target: MAX_SHA256D_TARGET // difficulty for every algo.

algo is accepted for backward compatibility and does not change the result — see target_for_difficulty(), the single definition this and DmintDeployParams.initial_target share. BLAKE3/K12 once got (2**256 - 1) // difficulty here, a target no dMint covenant can read.

Parameters:
Return type:

int

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 of algo (accepted for backward compatibility only).

Parameters:
Return type:

int

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

Verify a SHA256d PoW solution.

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

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

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

  • preimage (bytes)

  • nonce (bytes)

  • target (int)

Return type:

bool

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: object

The output of a successful mine_solution() call.

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

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

  • elapsed_s – Wall-clock seconds spent searching.

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

None

pyrxd.glyph.dmint.miner.mine_solution(preimage, target, *, algo=DmintAlgo.SHA256D, nonce_width=4, max_attempts=600000000, 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 target state field).

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

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

  • max_attempts (int) – Upper bound on iterations before raising MaxAttemptsError. See DEFAULT_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 every progress_interval_s while grinding. Feed the pair to live_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 – preimage is not 64 bytes, target is not positive, nonce_width is not 4 or 8, max_attempts is < 1, or progress_interval_s is not positive.

  • NotImplementedError – algo is BLAKE3 or K12.

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

Return type:

DmintMineResult

Note

There is no “easy” target for this loop. The verifier requires four leading zero bytes, so the mean is 2**96 / target and floors at 2**33 ≈ 8.6e9 attempts 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 with estimate_attempts() before starting it, and use benchmark_sha256d() (or pyrxd 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_argv as a subprocess, writes one JSON line to its stdin, reads one JSON line from its stdout, and re-verifies the returned nonce locally. The local re-verification is the load-bearing safety check — a wrong nonce from the external process raises rather than getting silently embedded in a transaction.

The miner is expected to:

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

  2. Search for a valid nonce.

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

  4. 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 $PATH can intercept calls. The local nonce re-verification (below) defends against the miner returning a wrong nonce, but cannot detect side-channel exfiltration: a malicious miner sees the preimage (which encodes the contract ref + miner binding) and can leak it out-of-band over the network.

Mitigations the caller should consider:

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

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

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

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

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

  • target (int) – The PoW target.

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

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

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

  • progress (Callable[[int, float], None] | None) – Optional callback(attempts, elapsed_s). When None (the default), behavior is byte-for-byte identical to before this parameter existed: a single blocking subprocess.run call 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 calls progress with the most recently observed one roughly every progress_interval_s — the same cadence contract mine_solution() documents. An external miner that never emits a progress frame (the common case today) simply means progress is never called; the grind still runs to completion or timeout exactly as it would with progress=None. A raising callback propagates and the subprocess is terminated — the supported way to impose a deadline, mirroring mine_solution() and pyrxd.contrib.miner.parallel.mine.

  • progress_interval_s (float) – Minimum seconds between progress calls. Ignored when progress is None.

  • 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 – algo is BLAKE3 or K12.

  • MaxAttemptsError – The miner exceeded timeout_s.

  • FileNotFoundError – miner_argv[0] is not on PATH.

Return type:

DmintMineResult

pyrxd.glyph.dmint.miner.mine_solution_dispatch(preimage, target, *, nonce_width=4, algo=DmintAlgo.SHA256D, miner_argv=None, max_attempts=600000000, timeout_s=600.0, 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() or mine_solution_external() directly. The two paths share semantics — both return a DmintMineResult with a nonce that satisfies the target — but have disjoint parameter sets (max_attempts vs timeout_s, no-argv vs argv). Picking between them was a 30-line wrapper that every demo and operator script ended up rewriting; this function is that wrapper, with the branch in one place.

Dispatch rule:

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

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

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

  • target (int) – The PoW target.

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

  • algo (DmintAlgo) – The contract’s PoW algorithm. Only SHA256D can be mined: BLAKE3 and K12 raise NotImplementedError on 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 to subprocess.run() for the external miner. Use [sys.executable, "-m", "pyrxd.contrib.miner"] for the bundled parallel miner.

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

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

  • 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 to mine_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 as progress=None.

  • progress_interval_s (float) – Minimum seconds between progress calls.

Returns:

DmintMineResult with the verified nonce.

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

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

  • NotImplementedError – algo is BLAKE3 or K12 (either path).

Return type:

DmintMineResult

pyrxd.glyph.dmint.miner.build_dmint_mint_tx(contract_utxo, nonce, miner_pkh, current_time, fee_rate=10000, *, funding_utxo=None, op_return_msg=None, half_life=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 to miner_pkh from a separate plain-RXD funding input. V1 and V2 use the same consensus shape (the V2 covenant’s output-validation block is byte-identical to V1’s); they differ only in the nonce width (4B V1 / 8B V2) and the 10-item V2 state.

Transaction structure (both V1 and V2)

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

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

Outputs
  • Output 0: recreated contract (the spent script with its state advanced: height + 1, and on V2 also lastTime and target as the note below says), value 1.

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

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

  • Output 3: change back to miner_pkh.

The final mint (height + 1 == max_height, flagged is_final_mint on the result) has the same inputs and the same outputs 1-3, but output 0 is the burn build_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 a lastTime its 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_time from OP_TXLOCKTIME and its target from the alt-stack DAA result, so current_time IS the block locktime: it is written into the recreated state’s last_time AND set as the tx nLockTime (the two must agree), and for DAA modes it drives the target retarget. The final mint recreates no state: current_time is still set as the nLockTime, and Part B, which runs on the final mint too, still reads it in the modes whose retarget does, but no last_time or target is written. current_time must be <= 0x7FFFFFFF on every V2 mint (on a mint that recreates the contract, Part C’s NUM2BIN(_, 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 reads lastTime (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 passes epoch_length/max_adjustment_log2 or schedule (and half_life for ASERT) matching the deployed contract — a mismatch is caught before the PoW grind.

Note

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_*_v2 or *_legacy — then byte-verifies the supplied half_life against 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_life defaults to None — 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. Supplying half_life explicitly 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 lastTime this mint’s retarget reads and which is not a readable lastTime (that contract can no longer be minted at all); a current_time earlier than the contract’s last_time on 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:

  1. Build the unsigned tx shell via this function.

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

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

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

  5. Broadcast.

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

param contract_utxo:

The live dMint contract UTXO to spend.

param nonce:

8-byte PoW nonce, 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 nLockTime on every mint and, on a mint that recreates the contract, as the recreated state’s lastTime (the covenant rebuilds it from OP_TXLOCKTIME); drives the DAA retarget. The final mint writes no lastTime. 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_exhausted is True.

raises ValidationError:

nonce is not 8 bytes (4 for V1); miner_pkh is not 20 bytes.

returns:

DmintMintResult with the unsigned tx and updated state.

Parameters:
Return type:

DmintMintResult

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

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

The V1 covenant binds the PoW preimage to:

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

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

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

Layout (matches build_pow_preimage()):

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

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

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

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

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

Returns:

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

Raises:

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

Return type:

PowPreimageResult

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

Build the V2 mining preimage AND scriptSig hashes.

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

Layout (matches build_pow_preimage()):

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

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

Note

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

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

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

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

Returns:

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

Raises:

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

Return type:

PowPreimageResult

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 is single-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: object

MEASURED single-core SHA256d throughput on this machine.

Every field here was observed, not assumed. Nothing in it accounts for multiple cores — see MintEtaProjection for 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.

hashes: int
elapsed_s: float
hashes_per_second: float
first_digest: bytes
__init__(hashes, elapsed_s, hashes_per_second, first_digest)
Parameters:
Return type:

None

class pyrxd.glyph.dmint.estimate.AttemptEstimate[source]

Bases: object

EXACT 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; attempts is the smallest n with P(hit within n) >= quantile.

target: int
effective_target: int
clamped: bool
difficulty: int
probability_per_attempt: float
expected_attempts: float
quantile_attempts: tuple[tuple[float, int], ...]
__init__(target, effective_target, clamped, difficulty, probability_per_attempt, expected_attempts, quantile_attempts)
Parameters:
Return type:

None

class pyrxd.glyph.dmint.estimate.MintEtaProjection[source]

Bases: object

PROJECTED wall-clock times. Every field is a projection.

aggregate_hashes_per_second assumes linear scaling across workers, 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 below single-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.

workers: int
single_core_hashes_per_second: float
aggregate_hashes_per_second: float
mean_s: float
quantile_s: tuple[tuple[float, float], ...]
__init__(workers, single_core_hashes_per_second, aggregate_hashes_per_second, mean_s, quantile_s)
Parameters:
Return type:

None

class pyrxd.glyph.dmint.estimate.LiveMiningStats[source]

Bases: object

Progress snapshot during a real grind: MEASURED rate, PROJECTED remainder.

observed_hashes_per_second is 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: attempts already burned do not reduce remaining_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 (None if no rate is observable yet).

  • remaining_quantile_s – PROJECTED (quantile, seconds) pairs.

attempts: int
elapsed_s: float
observed_hashes_per_second: float
remaining_mean_s: float | None
remaining_quantile_s: tuple[tuple[float, float], ...]
__init__(attempts, elapsed_s, observed_hashes_per_second, remaining_mean_s, remaining_quantile_s)
Parameters:
Return type:

None

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 in pyrxd.contrib.miner.parallel._worker. Benchmarking anything else (a bare sha256, a stand-in payload) would report a rate the miner never achieves.

The first batch is deliberately one hash wide so first_digest falls 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_s is not positive, preimage is not 64 bytes, nonce_width is not 4 or 8, or the clock did not advance.

Return type:

Sha256dBenchmark

pyrxd.glyph.dmint.estimate.attempts_for_quantile(target, quantile)[source]

Attempts needed to hit with probability quantile. EXACT.

The smallest n with 1 - (1-p)**n >= quantile, i.e. ceil(ln(1-quantile) / ln(1-p)). Both logs go through math.log1p(), which keeps full precision at the p ~ 1e-10 scale where log(1 - p) would round to log(1.0) == 0 and the division would blow up.

Parameters:
  • target (int) – PoW target (clamped to MAX_SHA256D_TARGET).

  • quantile (float) – Strictly between 0 and 1.

Raises:

ValidationError – target < 1 or quantile out of range.

Return type:

int

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**96 and E[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:
  • target (int) – The contract’s target state field.

  • quantiles (tuple[float, ...]) – Quantiles to report, each in (0, 1).

Raises:

ValidationError – target < 1 or a quantile is out of range.

Return type:

AttemptEstimate

>>> 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:

  1. aggregate = single_core × workers assumes linear cross-core scaling. pyrxd does not measure that, and real machines do not achieve it.

  2. Even given a perfect rate, the completion time is a random variable. mean_s is its mean; the quantiles bracket it. There is no single “time remaining”.

Parameters:
Raises:

ValidationError – non-positive rate or workers < 1.

Return type:

MintEtaProjection

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 over elapsed_s — so no scaling assumption is involved. The remaining-time fields are still projections, and they do not shrink as attempts grows: 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 attempts or elapsed_s.

Return type:

LiveMiningStats