Symptom

Running examples/dmint_claim_demo.py with DRY_RUN=0 against any unspent V1 dMint contract: the local miner finds a nonce that satisfies pyrxd’s own verify_sha256d_solution, the tx signs cleanly, broadcast returns:

mandatory-script-verify-flag-failed (Script failed an OP_EQUALVERIFY operation) (code 16)

The error was masked twice: ElectrumX wraps the policy rejection as JSON-RPC code 1, and pyrxd’s ElectrumX client classifies that as a generic NetworkError. The demo reprinted it as "BROADCAST FAILED: ElectrumX connection lost". The actual cause was a transaction-level script-verify failure, not network loss.

Root Cause

The V1 covenant’s PoW preimage (epilogue offsets 99–115, decoded in docs/dmint-research-mainnet.md §3.5) is built from values pushed onto the scriptSig:

PoW_preimage = H1 || H2 || nonce
  H1 = SHA256(OP_OUTPOINTTXHASH || contractRef_from_state)
  H2 = SHA256(scriptSig_inputHash || scriptSig_outputHash)

The on-chain canonical mint at 146a4d68…f3c confirms the push convention:

  • scriptSig_inputHash = SHA256d(funding_input_locking_script)

  • scriptSig_outputHash = SHA256d(vout[2]_OP_RETURN_locking_script)

Both pushes are the double-SHA256 of the corresponding script bytes.

pyrxd.glyph.dmint.build_pow_preimage correctly computed the 64-byte preimage. But build_mint_scriptsig then pushed the two halves of that preimage as the scriptSig inputHash/outputHash:

return (
    bytes([nonce_width]) + nonce
    + b"\x20" + preimage[:32]    # pushed H1, NOT SHA256d(input_script)
    + b"\x20" + preimage[32:]    # pushed H2, NOT SHA256d(output_script)
    + b"\x00"
)

The covenant then computed:

H2_covenant = SHA256(H1_push || H2_push)
            = SHA256(pyrxd_preimage)        # 64 bytes hashed

…which is not equal to pyrxd’s preimage[32..64]. The actual preimage the covenant hashed was H1 || SHA256(pyrxd_preimage) || nonce, while pyrxd was searching for a nonce that satisfied H1 || H2 || nonce. Pyrxd-valid nonces were systematically covenant-invalid.

How It Went Undetected

Same root failure mode as dmint-v1-mint-shape-mismatch.md:

  1. The builder and verifier were tested against each other, not against chain truth. build_mint_scriptsig, build_pow_preimage, and verify_sha256d_solution are self-consistent under the buggy mapping. Every round-trip test passed.

  2. No end-to-end broadcast test. The pre-M2 V1 contracts on mainnet (RBG-class) were already drained or otherwise unminable in the M1 review window; nobody attempted a live mine + broadcast.

  3. The chain helpers worked. find_dmint_contract_utxos and the mint demo ran cleanly end-to-end except for the rejected broadcast, which the NetworkError reclassification hid.

This is the second instance in pyrxd’s dMint history of synthetic self-consistent tests masking a chain divergence. See the “Recurring Pattern” section below.

Working Solution

The scriptSig builder no longer derives pushes from the preimage. Both the preimage and the scriptSig are computed from the raw scripts, with the caller passing the script hashes explicitly:

@dataclass(frozen=True)
class PowPreimageResult:
    preimage: bytes              # 64 bytes: H1 || H2
    input_script_hash: bytes     # SHA256d(funding_input_locking_script)
    output_script_hash: bytes    # SHA256d(vout[2]_OP_RETURN_locking_script)

def build_pow_preimage(
    txid_le: bytes,
    contract_ref_bytes: bytes,
    input_script: bytes,
    output_script: bytes,
) -> PowPreimageResult:
    H1 = sha256(txid_le + contract_ref_bytes)
    input_hash  = sha256d(input_script)
    output_hash = sha256d(output_script)
    H2 = sha256(input_hash + output_hash)
    return PowPreimageResult(H1 + H2, input_hash, output_hash)

def build_mint_scriptsig(
    nonce: bytes,
    input_script_hash: bytes,    # raw SHA256d(input_script)
    output_script_hash: bytes,   # raw SHA256d(output_script)
    *,
    nonce_width: int,
) -> bytes:
    return (
        bytes([nonce_width]) + nonce
        + b"\x20" + input_script_hash
        + b"\x20" + output_script_hash
        + b"\x00"
    )

The mint-tx assembler now threads PowPreimageResult.input_script_hash / .output_script_hash directly into build_mint_scriptsig. The preimage is never used as a source of scriptSig push values.

Prevention Strategies

Golden-vector tests against real mainnet bytes (extended)

The test_byte_equal_to_mainnet_vout1 pattern from the prior incident is necessary but not sufficient: it verified output shape, not scriptSig content. Add a parallel suite:

  • TestCovenantShape — golden vectors for every chain-visible field the covenant inspects: scriptSig push layout, PoW preimage bytes for a known mainnet nonce, and the 64-byte preimage halves recomputed from the on-chain inputHash / outputHash pushes.

  • Each test cites a real txid + vin/vout index and asserts byte- equality against captured chain data — not against pyrxd-generated fixtures.

testmempoolaccept gate before merge

Any change to the V1 mint path must pass a radiant-cli testmempoolaccept (or regtest broadcast) against a real or replayed contract UTXO before the PR can merge. Self-consistent synthetic mining is not acceptance.

Don’t reclassify policy rejections as network errors

pyrxd’s ElectrumX client must distinguish code 1 (message contains script-verify / mandatory-) from a connection drop. A PolicyRejection exception type that carries the raw bitcoind message would have surfaced this on the first broadcast attempt.

The Recurring Pattern

This is the second time the same anti-pattern has shipped:

Incident

What was wrong

Why tests missed it

shape-mismatch (2026-05-08)

V1 mint output count / reward script shape diverged from mainnet

Round-trip parser(builder(x)) == x through pyrxd’s own parser

This incident (2026-05-11)

V1 mint scriptSig pushes diverged from what the covenant hashes

Round-trip verify(build_preimage(build_scriptsig(x))) == ok through pyrxd’s own verifier

Both follow the same form: builder + verifier authored together, tested against each other, no external ground truth. Both bugs are invisible to any test that consumes only pyrxd-produced bytes.

Treat this as the load-bearing rule of the dMint codebase: a green test suite that never compares to chain bytes is a green test suite that hasn’t been tested. Every wire-format builder needs (a) a golden-vector test against captured mainnet bytes, and (b) a testmempoolaccept gate against a real or replayed UTXO.

Mainnet Verification Evidence

Fix was validated by a live mainnet mint after merge:

  • txid: c9fdcd3488f3e396bec3ce0b766bb8070963e7e75bb513b8820b6663e469e530

  • network: Radiant mainnet

  • date: 2026-05-11

Verify independently:

radiant-cli getrawtransaction \
  c9fdcd3488f3e396bec3ce0b766bb8070963e7e75bb513b8820b6663e469e530 1

The accepted scriptSig pushes match the on-chain 146a4d68…f3c convention: 32-byte SHA256d(funding_input_script) and 32-byte SHA256d(vout[2]_op_return_script), not preimage halves.

References