bitcoincoding.dev / Experiments
Spend A only if B is co-spent
A tapscript construction that binds a spend to a specific second UTXO through sha_prevouts, using only OP_CAT and OP_CHECKSIGFROMSTACK. One signature passes both CSFS and CHECKSIG, so the script can read the real transaction.
The question
Robin Linus's thread How CTV+CSFS improves BitVM bridges needs a connector: an operator's step must consume one specific UTXO. CTV commits to a transaction's outputs, not its inputs, and ajtowns showed that the scriptSig-based workaround is open to substitution.
Can tapscript itself demand that a particular outpoint is spent in the same transaction?
What was built
- Same-signature binding. One Schnorr signature must pass OP_CHECKSIGFROMSTACK over a message the witness supplies and OP_CHECKSIG over the sighash the node computes. One signature cannot stand for two messages, so the witness copy of the sighash preimage has to be the real one.
- Preimage decomposition. The witness supplies the 212-byte preimage in chunks; the script inspects one chunk, then rebuilds the whole with OP_CAT.
- A 196-byte leaf that hardcodes B's outpoint. The witness supplies A's outpoint, and
SHA256(A || B)must equalsha_prevoutsat preimage bytes 10–41. - Two confirmed spends of A with B. Swapping in a different coin C is rejected by
testmempoolaccept: Script failed an OP_EQUALVERIFY operation.
How it works
witness (bottom -> top): sig(64) post_c(56) post_b(57) post_a(57) pre(10) sha(32) a_outpoint(36)
<B_outpoint> CAT SHA256 SWAP DUP ROT EQUALVERIFY # sha == SHA256(a_outpoint || B)
SWAP SIZE 10 EQUALVERIFY # pre is 10 bytes -> sha sits at offset 10
SWAP CAT SWAP CAT SWAP CAT SWAP CAT # pre||sha||post_a||post_b||post_c
<tag[0:32]> <tag[32:64]> CAT SWAP CAT SHA256 # TapSighash(preimage)
OVER SWAP <pk> CHECKSIGFROMSTACK VERIFY # sig valid over witness preimage
<pk> CHECKSIG # same sig valid over real sighashIf C replaces B, the real sha_prevouts is SHA256(A || C). Supplying it fails the EQUALVERIFY; supplying SHA256(A || B) makes the witness preimage differ from the real one, so the same signature cannot pass both checks.
On chain
| Step | Transaction | Block | Note |
|---|---|---|---|
| Spend A + B | b7959a71…4606 | 300156 | |
| Spend A + B (second round) | 7311da6e…3558 | 300156 |
A + C (C substituted for B): rejected by testmempoolaccept, never broadcast.
Check it yourself
The repository re-checks every claim on this page (no keys, no node).
git clone https://github.com/aaron-recompile/sighash-prevout-binding
cd sighash-prevout-binding
python3 verify_onchain.py
30/30 checks passedWhat it does not show
- A signet experiment, not a bridge implementation.
- Assumes SIGHASH_DEFAULT, no annex and a script-path spend (the fixed 212-byte preimage layout).
- Same-signature binding needs a participating signer; keyless covenants are out of scope.
Sources
Technical record · facts.json
{
"page": "https://bitcoincoding.dev/experiments/prevout-binding/",
"title": "Spend A only if B is co-spent",
"context": "Answers Robin Linus · BitVM bridges",
"author": "Aaron Zhang (Aaron Recompile)",
"date": "2026-04-28",
"network": "Bitcoin Inquisition signet",
"summary": "A tapscript construction that binds a spend to a specific second UTXO through sha_prevouts, using only OP_CAT and OP_CHECKSIGFROMSTACK. One signature passes both CSFS and CHECKSIG, so the script can read the real transaction.",
"transactions": [
{
"role": "Spend A + B",
"txid": "b7959a71b74b8bbed69d52c22a9b3b90af176391be2d0b7f33eb1a6473304606",
"block": 300156,
"explorer": "https://mempool.space/signet/tx/b7959a71b74b8bbed69d52c22a9b3b90af176391be2d0b7f33eb1a6473304606"
},
{
"role": "Spend A + B (second round)",
"txid": "7311da6e30886fab6594b633bcb97e4436d49f02ecc244d4bc7c59cb1ea83558",
"block": 300156,
"explorer": "https://mempool.space/signet/tx/7311da6e30886fab6594b633bcb97e4436d49f02ecc244d4bc7c59cb1ea83558"
}
],
"verify": {
"repository": "https://github.com/aaron-recompile/sighash-prevout-binding",
"command": "python3 verify_onchain.py",
"result": "30/30 checks passed",
"needs": "no keys, no node"
},
"does_not_show": [
"A signet experiment, not a bridge implementation.",
"Assumes SIGHASH_DEFAULT, no annex and a script-path spend (the fixed 212-byte preimage layout).",
"Same-signature binding needs a participating signer; keyless covenants are out of scope."
],
"sources": [
{
"title": "Delving Bitcoin · How CTV+CSFS improves BitVM bridges, the question (#28)",
"url": "https://delvingbitcoin.org/t/how-ctv-csfs-improves-bitvm-bridges/1591/28"
},
{
"title": "Delving Bitcoin · the same thread, on-chain evidence (#29)",
"url": "https://delvingbitcoin.org/t/how-ctv-csfs-improves-bitvm-bridges/1591/29"
},
{
"title": "Delving Bitcoin · Taproot-native prevout binding via sighash preimage decomposition",
"url": "https://delvingbitcoin.org/t/taproot-native-prevout-binding-via-sighash-preimage-decomposition/2483"
}
],
"negative_test": "A + C (C substituted for B): rejected by testmempoolaccept, never broadcast."
}