Extensions to standard tooling for TEMPLATEHASH-CSFS-IK support
1 message, 20 Feb 2026. Started by Antoine Poinsot.
Antoine Poinsot
Hi everyone,
Last year Greg Sanders and i presented what we think is a good stopping point in terms of expanding scripting capabilities[^0], and then introduced along with Steven Roose a proposal implementing those capabilities[^1].
I'm following up on this topic today by proposing some extensions to standard tooling to support the new primitives introduced by our proposal. All the additions to the PSBT and Miniscript descriptors specifications described here have been implemented in a proof-of-concept PR to Bitcoin Inquisition[^2], on top of the OP_TEMPLATEHASH implementation[^3]. This is very much exploratory at this stage. I am seeking feedback on my design choices and on whether anything is missing.
Let me start with the integration of the new primitives in Miniscript descriptors. This wasn't completely straightforward, since some new capabilities like template hash checks break some invariants, and some primitives like rebindable signatures require additional context. With some adjustment however, all the new capabilities fit reasonably well in this framework.
The goal was to enable the following:
- ability to use the Taproot internal key as a key fragment in place of
hardcoded public keys;
- ability to assert that the spending transaction matches a hardcoded template
in a given spending path;
- ability to check a signature for an arbitrary message in a given spending
path;
- ability to use a rebindable signature check in place of the regular
transaction signature check ('c:' fragment).
The Taproot internal key was introduced as a new `pk_i()` fragment, along with a `pki()` alias for `c:pk_i()` in the same vein as `pk()` and `pkh()`. The fragment corresponds to the Bitcoin Script `OP_INTERNALKEY`, has type 'K', type properties 'o', 'n', 'd', 'u', 'k' and malleability properties 's', 'e' (the very same properties as the `pk_k` fragment). Implementing this fragment only introduces a requirement on the Script and string parsers to have access to the Taproot internal key corresponding to the (mini)script being parsed. See the commit message in the PR linked above for the details of how this was done in the Bitcoin Core implementation.
Something i think noteworthy is how before the introduction of these primitives, it was always the case that a fragment that has the 's' malleability property ("satisfying this expression always requires a signature") also guaranteed that all spending paths were encumbered by at least one signature check. This in turn limited third-party tempering to the extent permitted by the chosen sighash mode at signing time. Both the Bitcoin Core and Rust-Bitcoin implementations rely on this invariant[^4][^5], using the presence of the 's' property to verify that no spending path lacks a transaction signature check.
But some of the new capabilities introduce signature checks (which require access to a private key, and therefore should have the 's' property) that are not over the transaction. And vice-versa, some checks that encumber the spending transactions' outputs, and therefore should make the Script "sane" or "safe to spend" but (arguably) not have the 's' property. Therefore i split the 's' property into 's' (private key knowledge demonstrated) and 't' (all spending paths encumber the spending transaction) as a preparation step for the following fragments. All pre-existing fragment that have the 's' property (`0`, `pk_k()`, `pk_h()`, `multi()`, `multi_a()`, `c:`) also get the 't' property. (nit: the 't' property is technically not really a type property, since no other fragment depend on it, but also not a "satisfaction malleability" property, it's more of a third type of "safety" property. In this writeup i'll use it interchangeably with type properties for the sake of brevity.)
A new `th(h)` fragment is introduced. It corresponds to the Script `<h> OP_TEMPLATEHASH OP_EQUAL`. It is of type 'B', with type properties 'z', 'u', 't' (i.e. encumbers the spending transaction), 'k' and with no (specified) malleability property. The main design decision here was to pass the template hash, and not its preimage, as argument to the `th()` fragment. This is closer in effect to the four hash fragments, while `th()` is conceptually closer to `older()` and `after()` in that it enables limited introspection on the spending transaction. Since the preimage is not necessary to build/parse the Script, and since having a full transaction in the descriptor would be unreasonable anyways, i went with the template hash as the argument. This seems relevant to the discussion of whether we want the descriptor language to encode all (public) information necessary to spend an output, or if it's fine for it to only partially contain that information[^6]. Note that `th()` does not have the 's' malleability property, because we can't assume its satisfaction is not always available, and does not have the 'f' malleability property because, unlike `older()` and `after()`, a dissatisfaction may be available.
Up until now, all signing in Miniscript implicitly happened over the spending transaction's sighash. The Miniscript satisfier relied on this invariant to support composable `K`-typed fragments in addition to simpler leaf key fragments (`pk_k`, `pk_h`, and now `pk_i`), by triggering the signing logic directly at the key fragment level, such that the availability of a spending path could be immediately propagated upward the tree, all the way toward the signature-checking fragment.
But we are now going to introduce fragments that check a signature against different messages (rebindable signatures, and arbitrary message signatures), so the leaf key fragment needs to have the context of which type of signature is expected. Since there is always exactly 1 signature-checking fragment for N `K`-typed sub-fragments, this context can simply be propagated downward the tree from the signature-checking fragment all the way to the leaf key fragment(s).
We introduce a new `cms(X,m)` fragment, which takes a sub-fragment of type `K`, an arbitrarily-sized (within the consensus limits) message `m`, and corresponds to the Script `[X] <m> OP_SWAP OP_CHECKSIGFROMSTACK`. It always has type properties 'u' and 's'. Its type properties 'o', 'n', 'd', 't, 'h', 'i', 'j' and 'k' are equal to that of its sub-fragment. Same for its malleability properties 'f' and 'e'. Interestingly, the order of arguments in BIP 348 is such that an additional `OP_SWAP` is necessary to support composable keys (in place of a hardcoded key expression) in Miniscript. This does not mean that BIP 348 should necessarily be amended, as the Miniscript framework could equally be adapted to support "wrapped key" types (the equivalent of 'W' but for 'K' instead of 'B'). I'm ambivalent about setting the 's' property (which is explicitly set here, but importantly also propagated from the key fragment leaves), because it seems that for this fragment a third party should be able to get ahold of a signature for the arbitrary message more easily than for transaction signature checks. The distinction does not seem quite tractable at the Miniscript level, but always ripping-off the 's' property for this fragment may be a saner default, since a spending path that contains an arbitrary message signature should likely always also contain a transaction signature check. Or maybe a `th()`? In which case it wouldn't get the 's' property at all. At the end of the day this is only about analyzing satisfaction malleability, not correctness, but i am curious what others think about this.
Finally, we introduce an 'r:' wrapper, the analog of 'c:' but for rebindable signature checks. It corresponds to the Bitcoin Script `[X] OP_TEMPLATEHASH OP_SWAP OP_CHECKSIGFROMSTACK`. It has type 'B' and takes an argument X of type 'K'. It always has type properties 'u', 's', and 't'. Its type properties 'o', 'n', 'd', 'g', 'h', 'i', 'j' and 'k' are the same as that of its sub-fragment. Same for its malleability properties 's' and 'f'. This is essentially `cms(X,m)` with `m` fixed to the transaction's template hash such that it can have the 't' property (and a neater, 'c:'-like, syntax).
Now regarding PSBTs, i think the only extension that really makes sense is to make available information to verify the outputs of a transaction committed in an output via `th()`. Since i expect `cms(X,m)` would be used with `m` set to the hash of the actual message being signed, for obvious onchain efficiency and privacy reasons, or simply because the message is larger than 520 bytes, i considered a PSBT input field that would map arbitrary message hashes to their preimage. But we already have such fields for SHA256/HASH256/RIPEMD160/HASH160 preimages, and i don't really see a reason why not use one of these hash functions, so an extra field seems unnecessary. Similarly, rebindable signatures can simply be provided through the `PSBT_IN_TAP_SCRIPT_SIG` mapping, since the specs already assume that keys are not repeated within a Script anyways.
We introduce a new `PSBT_OUT_COMMITTED_TXS` field, which is a mapping from template hash to Bitcoin-serialized transaction. The alternative, as Ademan pointed out in private discussions, is to use a mapping for each field committed to by the template hash (version, locktime, sequences, input index, annex, outputs). This could be a small space saving in case the committed transaction has many inputs, but for smaller transactions the repeated map keys would actually be less efficient. It does have the advantage of not including non-committed fields in the mapping, which could be a footgun in the simpler version. Of course another alternative would be to keep a single mapping and create a custom serialization of only the committed fields to use as value in place of the regular Bitcoin serialization, but i don't think the complexity would be worth it. I'm curious what people think about that.
The existing `PSBT_OUT_TAP_INTERNAL_KEY` is not keyed, and therefore may only be used once per output. Since we may want verifiers to be able to inspect the output(s) of transaction(s) committed in this output, we introduce a new `PSBT_OUT_TAP_INTERNAL_KEYS` field. It is a mapping from Taproot output key to Taproot internal key.
For the same reason, we introduce a `PSBT_OUT_TAP_TREES` field, which is a mapping from Taproot output key to a list of tuples representing the depth, version and Script for a leaf in a Taproot tree (same format as the value for `PSBT_OUT_TAP_TREE`).
Other fields useful to verify the outputs of committed transactions, such as `PSBT_OUT_TAP_BIP32_DERIVATION`, are already keyed and can just be reused directly.
Do these extensions to PSBTs and descriptors make sense to support the "Taproot-native (re-)bindable transaction bundle" proposal? Is there anything you think should be included that i'm missing?
Best, Antoine Poinsot
[^0]: https://gnusha.org/pi/bitcoindev/F5vsDVNGXP_hmCvp4kFnptFLBCXOoRxWk9d05kSInq_kXj0ePqVAJGADkBFJxYIGkjk8Pw1gzBonTivH6WUUb4f6mwNCmJIwdXBMrjjQ0lI=@protonmail.com/ [^1]: https://gnusha.org/pi/bitcoindev/26b96fb1-d916-474a-bd23-920becc3412cn@googlegroups.com/ [^2]: https://github.com/bitcoin-inquisition/bitcoin/pull/108 [^3]: https://github.com/bitcoin-inquisition/bitcoin/pull/100 [^4]: https://github.com/bitcoin/bitcoin/blob/241ad5853bcac94b0fcbe5c67199e5a226da5218/src/script/miniscript.h#L1697 [^5]: https://github.com/rust-bitcoin/rust-miniscript/blob/411b651b7805c781839096832be2ea0e81ed3588/src/miniscript/analyzable.rs#L186-L187 [^6]: To my knowledge this has been discussed a number of times and the latest iteration is https://github.com/bitcoin/bitcoin/issues/24114