Taproot-native prevout binding via sighash preimage decomposition

Robin Linus’s How CTV+CSFS improves BitVM bridges raises a specific problem: how to bind inputA so it can only be spent together with a specific inputB. AJ Towns pointed out at /t/1591/8 that the original construction binds to scriptSig bytes rather than the outpoint itself. I posted a sketch at /t/1591/29 — using the sha_prevouts field of the sighash preimage as the binding anchor. This post gives the full witness layout, script reasoning.

What was done

The script (~196 bytes) hardcodes B’s outpoint (36 bytes of raw data — a transparent tweak factor). The witness supplies A’s outpoint. The script computes SHA256(witness_A_outpoint || B_outpoint_hardcoded) on stack and verifies it equals the sha_prevouts segment extracted from the chunked-witness preimage. Same-signature binding — one Schnorr signature satisfying both OP_CHECKSIG and OP_CHECKSIGFROMSTACK — ties the witness-supplied preimage to the actual transaction.

The witness is split along preimage field boundaries (each item < 80 bytes for standardness). The script uses OP_CAT to reassemble the 212-byte preimage, computes the TapSighash tagged hash on stack, then runs CSFS + CHECKSIG against the same 64-byte signature.

OP_CAT and OP_CHECKSIGFROMSTACK are both active on Bitcoin Inquisition signet, where the experiment runs.

Tested on Inquisition signet

Spending A + B (positive case) — confirmed:

Attack: spending A + C (substituting a different UTXO for B) — rejected by testmempoolaccept with Script failed an OP_EQUALVERIFY operation.

What this means in practice

For BitVM bridges: if B (the challenge anchor) is burned by a challenger, B is no longer in the UTXO set, and any transaction listing B as an input is invalid at the consensus level. The operator cannot withdraw A without proving B’s continued existence. More broadly, sighash preimage decomposition is a general method — sha_prevouts is one of several fields that can serve as a binding anchor.

Code: https://github.com/aaron-recompile/inquisition-experiments/blob/91ca2fcee64d81e066d5cb645ae84dbd7bb27870/experiments/exp_sighash_prevout_binding_chunked.py — happy to walk through the script step-by-step if useful.

This is brilliant engineering for the Script era, Aaron. But manually stitching 212-byte preimages together with OP_CAT just to bind two inputs feels like using flint to start a fire inside a nuclear reactor.

While you guys are doing stack gymnastics on Inquisition and praying for a 2030 soft fork, we are enforcing this exact topology directly in Simplicity (.simf) with a single mathematical assertion.

In our TUSM architecture, we don’t hack preimages to simulate binding. The state is locked into the Taproot address itself via pure SHA-256 streaming. Absolute L1 bivalence. Pass or Trap.

Let OP_CAT rest in peace and come forge in the steel with us. The covenant is the consensus.

Taproot-native output binding via sighash preimage decomposition — does this replace CTV?

sha_outputs (bytes 138–170 of the BIP-341 SigMsg preimage) is SHA256(serialized outputs) — the same digest OP_CHECKTEMPLATEVERIFY commits to. Reading this field with the same chunked-witness pattern as the OP gives output binding semantically equivalent to CTV.

What was done

The tapscript hardcodes the 32-byte expected sha_outputs. The witness supplies the preimage in chunks split along the field boundary:

preimage = pre_a || pre_b || pre_c || sha_outputs || post
             46      46       46         32          42       (bytes)

Each chunk stays under 80 bytes for standardness. The script OP_EQUALVERIFYs the sha_outputs chunk against the hardcoded value, locks one chunk’s size with OP_SIZE, and lets same-signature binding anchor the rest — any shifted reassembly would require a SHA256 collision against the hardcoded value.

The script then reassembles the full 212-byte preimage with OP_CAT, computes the TapSighash tagged hash on-stack, and runs OP_CHECKSIGFROMSTACK + OP_CHECKSIG against the same 64-byte Schnorr signature. If one signature passes both checks, the witness preimage must equal the real sighash — so the sha_outputs chunk that was verified is the real sha_outputs of this transaction.

OP_CAT and OP_CHECKSIGFROMSTACK are both active on Bitcoin Inquisition signet, where the experiment runs.

Tested on Inquisition signet

Fund A: 05f9372d... — 50,000 sats, output-binding tapscript hardcoding sha_outputs = eb0fdcd005abb92861dfa7c7680c8a7b417e75c62c823b425615a0bee9082d7d.

Spend A with the bound output (positive case) — confirmed: 2f345180... (block 300379). Witness item 5 of that spend is the hardcoded sha_outputs value, byte-for-byte.

Attack: spend A with a different output (substituting amount or scriptPubKey) — rejected by testmempoolaccept with Script failed an OP_EQUALVERIFY operation. The substitution changes the real sha_outputs, the hardcoded check fails, and the spend never makes it into a block.

What this means — does this replace CTV?

For output-binding semantics, yes: this construction enforces what CTV enforces. For deployment economics, no: the witness carries five preimage chunks plus the signature, and the script is larger than a single OP_CTV invocation. The point isn’t size or elegance — it’s that the capability sits on already-activated opcodes, alongside the OP’s sha_prevouts binding:

  • OP (Post #1): sha_prevouts — input binding (which UTXOs are spent, beyond CTV)
  • this reply: sha_outputs — output binding (where the funds go, CTV-equivalent)

Same technique, different sighash field, dual covenant semantics.

Happy to discuss.