Skip to content

PoC: tooling for OP_TEMPLATEHASH#108

Open
darosior wants to merge 21 commits intobitcoin-inquisition:29.xfrom
darosior:2512_inquisition_templatehash_tooling
Open

PoC: tooling for OP_TEMPLATEHASH#108
darosior wants to merge 21 commits intobitcoin-inquisition:29.xfrom
darosior:2512_inquisition_templatehash_tooling

Conversation

@darosior
Copy link

This is a PoC implementation of extensions to standard tooling (PSBT and Miniscript descriptors) to support our "Taproot-native (re-)bindable transactions" proposal. It's based on #100.

I will post on the mailing list the rationale for the various design choices, but in short this PR introduces 4 new Miniscript fragments, and 3 new PSBT output fields. The Miniscript fragments are the following:

fragment Bitcoin Script Type Properties Sub-fragment X required type
pk_i OP_INTERNALKEY K o; n; d; u; k; s; e N/A
th(h) <h> OP_TEMPLATEHASH OP_EQUAL B z; u; t; k N/A
cms(X,m) [X] <m> OP_SWAP OP_CHECKSIGFROMSTACK B o=o_x ; n=n_x ; d=d_x ; g=g_x ; h=h_x ; i=i_x ; j=j_x ; k=k_x ; u; s; f=f_x ; e=e_x K
r:X [X] OP_TEMPLATEHASH OP_SWAP OP_CHECKSIGFROMSTACK B o=o_x ; n=n_x ; d=d_x ; g=g_x ; h=h_x ; i=i_x ; j=j_x ; k=k_x ; u; t; s; f=f_x ; e=e_x K

The 't' property is not currently described in the BIP or on the Miniscript website. This is a new property i had to introduce to split two related properties "a spending path needs a signature" and "the spending transaction is encumbered". See commit a3e3ad1 for details.

The PSBT fields introduced are the following:

Name <keytype> <keydata> <valuedata>
Committed Transaction Template PSBT_OUT_COMMITTED_TXS = 0x0b <32 bytes of template hash> <bytes of Bitcoin-serialized transaction committed>
Additional Taproot Internal keys PSBT_OUT_TAP_INTERNAL_KEYS = 0x0c <bytes taproot output key> <32-byte internal key>
Additional Taproot Trees PSBT_OUT_TAP_TREES = 0x0d <bytes taproot output key> {<8-bit uint depth> <8-bit uint leaf version> <compact size uint scriptlen> <bytes script>}*

This is PoC quality, but should still give some reasonable assurance given the unit and end-to-end sanity checks, and especially the integration of the new fragments in the existing thorough Miniscript fuzz targets.

darosior and others added 6 commits February 18, 2026 14:09
This introduces a new Script operation exclusively available in Tapscript context: OP_TEMPLATEHASH.
This operation pushes the hash of the spending transaction on the stack. See BIP 446 for details.

This operation is introduced as replacing OP_SUCCESS206 (0xce).

Co-Authored-By: Greg Sanders <gsanders87@gmail.com>
Sanity check the template hash by using it to commit to the transaction that must spend an output.
Malleating committed fields must lead to a consensus failure, and changing non-committed fields is
fine.

We also add the option to generate test vectors from this unit test.
We introduce one specialized target focused on exercising the new `GetTemplateHash()` logic
introduced for OP_TEMPLATEHASH, and one broader fuzz target which exercises using OP_TEMPLATEHASH
on a variety of transactions while asserting invariants.
This leverages the extensive feature_taproot.py test framework to generate coverage for the numerous
scenarii and mutations exercised there.

Additionally, a separate feature_templatehash.py functional test is introduced for end-to-end
testing of the commit-to-next-transaction use case, which does not fit nicely into the
feature_taproot.py framework (assumes input independence).
We use the 's' property in two ways:
1. To reason about malleability, as per the Miniscript specifications;
2. To sanity check that a top-level Miniscript fragment requires a
   transaction signature to be spent.

This is fine as long as the only way to fix the transaction is by using
a signature, and as long as all signatures are over the transaction. But
in the following commits we are going to introduce fragments that either
fix the transaction but do not require access to a private key (hence
should not be 's' when reasoning about malleability), or allow to sign
messages that are no the transaction itself and therefore should not be
considered for the sanity check.

Therefore, separate the two roles into two properties. Keep 's' for
reasoning about malleability, and introduce a 't' property that
determines whether the fragment's satisfaction fixes the transaction.
@darosior
Copy link
Author

Windows CI failure seems unrelated:

Completed submission of boost-type-traits:x64-windows-static@1.85.0#1 to 1 binary cache(s) in 100 ms
Installing 11/41 boost-throw-exception:x64-windows-static@1.85.0#1...
Building boost-throw-exception:x64-windows-static@1.85.0#1...
Downloading https://github.com/boostorg/throw_exception/archive/boost-1.85.0.tar.gz -> boostorg-throw_exception-boost-1.85.0.tar.gz
error: https://github.com/boostorg/throw_exception/archive/boost-1.85.0.tar.gz: failed: status code 503
note: If you are using a proxy, please ensure your proxy settings are correct.
Possible causes are:
...

@darosior darosior force-pushed the 2512_inquisition_templatehash_tooling branch from cafd902 to c064b18 Compare February 20, 2026 21:30
This is a bit convoluted since no other fragment relied on the Taproot
internal key yet, and it needs to be passed from the context, and
therefore all places where we create a context: in signing, descriptor
parsing / inference, unit tests and fuzz tests.

The approach taken is to only query the internal key once at parsing /
inference time and store it in the fragment's keys vector. This way it
naturally integrates with the existing code for pk_k(), such for
serialization, signing and duplicate key checks.

Care was taken in fuzz harnesses to not invalidate existing seeds, but
also use a meaningful key in TestNode() (i.e. in miniscript_smart and
miniscript_stable targets). A new optional internal_key parameter is
adding down the call chain as an "out" parameter: the first time the
fuzzer generates a pk_i() fragment (if any) the internal key is set and
reused for all pk_i() fragments. This way we can roundtrip ser/parsing,
make it available to the satisfier to sign, etc..
Likewise pk() and pkh(), this is syntactic sugar for the common case of
an internal public key signature check.
This fragment checks the spending transaction's template hash. Because
it commits to the spending transaction (with more malleable fields than
SIGHASH_ALL, but less than SIGHASH_SINGLE/NONE) then we give it the 's'
property. Note this breaks the invariant that an 's' fragment always
contains at least one key.

In the miniscript_smart and miniscript_stable fuzz harnesses we use the
message hash for dummy signatures as the template hash. We sometimes
create th() fragments with this hash (making them satisfiable), and
sometimes not.
The satisfaction algorithm checks for signatures when going over key
fragments, as a signature-checking fragment may have more than one key
fragment as "descendant" where not all of them are available or some are
more desirable to use than others.

The key fragments have therefore no knowledge of the message to be
signed to satisfy their signature-checking "ancestor" fragment. This was
not an issue up until now since Miniscript only supported transaction
signature checking, and therefore signatures had implicitly to be
provided for the transaction itself.

But we are about to introduce a Miniscript fragment that check
signatures for arbitrary messages in the upcoming commits. Therefore in
preparation make key fragments aware of the message their "ancestor"
signature-checking fragment expect them to be signing.
…trary message

A cms() fragment that takes as inputs a key expression and a message to
check the signature against. The message is forwarded to key expressions
and the satisfier made aware of a potential custom message to sign in
place of the customary sighash.

The chosen order of arguments did not require introduce more state to
the parser, but did require introducing more to the decoder (where
previously it was only necessary for thresh()).
This fragment is the equivalent of 'c:' but for rebindable signatures.
It is a specialization of the 'cms()' fragment for a specific message
that is the TEMPLATEHASH of the spending transaction.
This field allows a verifier to validate the transaction template(s)
committed to in a transaction output. One caveat is that transaction are
Bitcoin-serialized, which includes some field not committed to in a
template hash.
The existing PSBT output field for a Taproot internal key is not keyed,
which makes it so only a single Taproot internal key may be specified.
This makes sense since there may be at most a single one per Taproot
output, but since we introduce the capability of committing to the
template of a spending transaction, it is often useful for a verifier to
validate the outputs of the spending transaction.
We are going to reuse them in the following commit.
The rationale here is the same as for the additional Taproot internal
keys, be able to inspect the outputs of transaction templates committed
to in this output.
This is a specially crafted PSBT of a transaction that pays to a Taproot
with a leaf with a TEMPLATEHASH equality check for a transaction that
pays to 2 Taproot outputs. This highlights the use of all introduced
fields, as well as existing ones (BIP32 derivations).
@DrahtBot
Copy link
Collaborator

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #104 (TAPSCRIPT_V2 (Great Script Restoration) by jmoik)
  • #102 (OP_CHECKCONTRACTVERIFY by bigspider)

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants