Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BIP Draft: unspendable() Descriptor Key Expression #1746

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
111 changes: 111 additions & 0 deletions bip-xxxx.mediawiki
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<pre>
BIP: ?
Layer: Applications
Title: unspendable() Descriptor Key Expression
Author: Andrew Toth <[email protected]>
Kewde <[email protected]>
Comments-Summary: No comments yet.
Comments-URI: https://github.com/bitcoin/bips/wiki/Comments:BIP-?
Status: Draft
Type: Standards Track
Created: 2024-12-12
License: BSD-2-Clause
</pre>

==Abstract==

This document specifies a <tt>unspendable()</tt> key expression for output script descriptors. The <tt>unspendable()</tt> expression operates on the root <tt>TREE</tt> expression and produces an unspendable public key that can be independently verified by anyone with knowledge of all the constituent public keys.

==Copyright==

This BIP is licensed under the BSD 2-clause license.

==Motivation==

When creating a multi-party Taproot transaction spending only from the script path, it is useful to be able to prove to all cosigners that they keypath is unspendable. Otherwise a malicious participant could use an internal key which they have the private key for and spend the transaction out from the rest of the participants.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
When creating a multi-party Taproot transaction spending only from the script path, it is useful to be able to prove to all cosigners that they keypath is unspendable. Otherwise a malicious participant could use an internal key which they have the private key for and spend the transaction out from the rest of the participants.
When creating a multi-party Taproot transaction spending only from the script path, it is useful to be able to prove to all cosigners that they keypath is unspendable. Otherwise, a malicious participant could use an internal key for which they have the private key to spend the transaction out from the rest of the participants.


This document introduces a mechanism to compute a NUMS (Nothing Up My Sleeve) point for use in the Taproot key path that:
* Allows active participants involved in constructing the output script to independently verify the unspendable key.
* Prevents passive observers from recognizing that the key path is unspendable.
* Enables signers with limited information, such as hardware wallets, to verify unspendability without requiring user interaction.

==Specification==

A new key expression is defined: <tt>unspendable()/NUM/.../*</tt>.

===<tt>unspendable()/NUM/.../*</tt>===

The <tt>unspendable</tt> expression can only be used as the first argument of a BIP386 <tt>tr(KEY, TREE)</tt> expression. All other <tt>KEY</tt> expressions in the descriptor must be <tt>xpub</tt> encoded extended public keys with exactly 2 unhardened derivation steps. The derivation steps may include <tt>/*</tt> or a BIP389 multipath expression, but still must have only unhardened steps. BIP390 <tt>musig(KEY, KEY, ..., KEY)</tt> expressions are allowed, but the variant with derivation after the expression <tt>musig(KEY, KEY, ..., KEY)/NUM/.../*</tt> is forbidden.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this limitation over musig.
musig(KEY, KEY, ..., KEY)/<M,N>/* is the scheme supported for MuSig2 in wallet policies.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If musig(KEY, KEY, ..., KEY) resolves to AGG_KEY, then tr(unspendable()/0,musig(KEY, KEY, ..., KEY)/<M,N>/*)) and tr(unspendable()/0,AGG_KEY/<M,N>/*)) would both resolve to the same merkle root but different internal keys. If using just tr(unspendable()/0,musig(KEY, KEY, ..., KEY))), the corresponding tr(unspendable()/0,AGG_KEY)) is invalid since AGG_KEY is not an xpub with 2 derivation paths.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per the previous discussions, the goal was to come up with something that works for both descriptors and wallet policies, and doesn't require any complicated parsing. Aggregating musig() before using its key (rather than using the individual keys like you'd do for multi/sortedmulti fragments) would completely negate the advantages of sorting and removing duplicates - namely, being able to compute the deterministic chaincode with a simple function of the involved xpubs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the goals is also to not have any malleability - two descriptors or policies that produce the same merkle root should always produce the same internal key.
We cannot know if an xpub in a descriptor or wallet policy is actually an aggregate xpub, which could also be represented with the musig expression.

Copy link
Member

@sipa sipa Jan 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andrewtoth You're imposing an (arbitrary) distinction though at the xpub level. If you expand a description in any specific position, and replace all xpub/keypath expression with their hex pubkey at that point, the merkle root will still be the same, but since you've lost the xpub data, you can't generate the same unspendable keys anymore.

(I don't have an opinion either way here, just pointing out that what kinds of changes you incorporate in the calculation and which you don't is inherently arbitrary)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replace all xpub/keypath expression with their hex pubkey

The proposal here though prohibits any key expression that is not an xpub with exactly unhardened depth of 2.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, sure, that's what you need to do to make sure you don't accidentally derive an incompatible result.

But for every piece of information in a descriptor you can either ignore it in the unspendable computation, or use it and require it to be present. What those pieces of information are is arbitrary. For example, you could use origin information for all keys for example, and reject any descriptor that doesn't have it. It's a tradeoff between the sensitivity of the unspendable keys, and restrictiveness on the set of descriptors it's allowed to be used in.


The <tt>unspendable</tt> expression resolves to an extended public key, which is then further derived. As there is no private key for an unspendable key, only unhardened derivation is allowed.

The extended public key is computed by first collecting the public key from all the extended public keys in all the <tt>KEY</tt> expressions. The collection of public keys then has all duplicates removed and the remaining public keys are sorted lexicographically.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorted with what serialization? Textual descriptor serialization? Binary BIP32 serialization? Uncompressed? Compressed? X-only?

The vector of keys is processed in the following sequence: deduplication, compression, sorting, concatenation, and finally, SHA256 hashing to generate a chaincode <tt>c</tt>.
The unspendable BIP32 extended key is constructed by using the NUMS point <tt>H</tt>, suggested in BIP341, as the public key, and the chaincode <tt>c</tt> is computed as follows:
* The public keys are collected from all extended public keys in all <tt>KEY</tt> expressions.
* All duplicate public keys are removed from the collection and the public keys are sorted lexicographically.
* Let ''P<sub>0</sub> ... P<sub>n</sub>'' be the sorted and deduplicated public keys. Using the notation from BIP340, ''c = hash<sub>BIP0???/chaincode</sub>(bytes(P<sub>0</sub>) || ... || bytes(P<sub>n</sub>))''.

==Test Vectors==

Valid descriptors containing the <tt>unspendable</tt> expression followed by the chaincode of the unspendable extended public key they expand to and then the scripts they produce.
Todo: These will be filled in when the BIP number is assigned for the tagged hash.

The following produce identical extended public keys and scripts:
* <tt>tr(unspendable()/0, pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/0))</tt>
* <tt>tr(unspendable()/0, pk(xpub661MyMwAqRbcFXsZHGwUFzya6zhjaLUoKt2jKZTsWEoHAPjUERUbW215Fy6DGNLZdNDyMo8WJLgouGNRypxvDFc3MgW8TvRJdpbzsxuyfvr/0/0))</tt>
* <tt>tr(unspendable()/0, pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/<0;1>/*))</tt>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since unspendable() is treated like an xpub, in order to be compatible with BIP-388 you would need to have for example unspendable()/<0;1>/*.
In general, mixing public keys with /* and other public keys without /* in the same descriptor always leads to key reuse across UTXOs.


The following has two identical public keys which are deduplicated:
* <tt>tr(unspendable()/0, {pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/0),pk(xpub661MyMwAqRbcFXsZHGwUFzya6zhjaLUoKt2jKZTsWEoHAPjUERUbW215Fy6DGNLZdNDyMo8WJLgouGNRypxvDFc3MgW8TvRJdpbzsxuyfvr/0/0)})</tt>

The following has two identical public keys which are deduplicated, and then the remaining two public keys are sorted:xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw
* <tt>tr(unspendable()/0, {pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/0),{pk(xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw),pk(xpub661MyMwAqRbcFXsZHGwUFzya6zhjaLUoKt2jKZTsWEoHAPjUERUbW215Fy6DGNLZdNDyMo8WJLgouGNRypxvDFc3MgW8TvRJdpbzsxuyfvr/0/0)}})</tt>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems a bit unusual to have examples of xpubs with the same pubkey in a descriptor (except, of course, NUMS xpubs), although it should indeed be mentioned as a pathological case if it matters for deduplication.

I think this wouldn't really happen in practice, as xpubs would usually come from some BIP32 derivation.

What would happen is to have the same xpub with different multipath derivations in alternative spending paths:

tr(unspendable()/<0;1>/*, {multi_a(2,xpub_A/<0;1>/*,xpub_B/<0;1>/*),and_v(v:pk(xpub_A/<2;3>/*),older(12960))})


Invalid descriptors:

No <tt>TREE</tt> expression:
* <tt>tr(unspendable()/0)</tt>

No derivation path for the unspendable key:
* <tt>tr(unspendable(), pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/0))</tt>

Hardened derivation path for the unspendable key:
* <tt>tr(unspendable()/0'/0, pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/0))</tt>

Hardened derivation path in a <tt>KEY</tt> expression:
* <tt>tr(unspendable()/0, pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0'/0))</tt>

Greater than two derivation paths in a <tt>KEY</tt> expression:
* <tt>tr(unspendable()/0, pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/0/0))</tt>

Less than two derivation paths in a <tt>KEY</tt> expression:
* <tt>tr(unspendable()/0, pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0))</tt>

A <tt>KEY</tt> expression is not an <tt>xpub</tt>:
* <tt>tr(unspendable()/0, pk(0260b2003c386519fc9eadf2b5cf124dd8eea4c4e68d5e154050a9346ea98ce600))</tt>

A <tt>musig</tt> expression with derivation paths is used:
* <tt>tr(unspendable()/0, musig(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/0)/0/0)</tt>

==Rationale==

The restrictions on <tt>KEY</tt> expressions is necessary to not allow multiple <tt>TREE</tt> expressions which would all produce the same merkle root to produce different internal keys.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either "restrictions" should be singular, or "is" should be "are"

s/expressions which/expressions, which/

* Using different lengths of derivation paths would allow a descriptor using a child xpub to generate a different key, while the merkle root would be identical.
* Not sorting the public keys would allow <tt>sortedmulti</tt> expressions to generate different keys depending on the order specified in the descriptor, while the merkle roots would be identical.

This proposal ensures:
* Compatibility with existing Taproot functionality by leveraging NUMS points.
* Verifiability of unspendable constructions by participants, without exposing this property to outside observers.
* Security and simplicity for signers with limited information (e.g., hardware wallets).

==Backwards Compatibility==

This is backwards compatible with BIP386 by computing the unspendable key as a BIP380 <tt>KEY</tt> expression and replacing the <tt>unspendable</tt> expression as the first argument of the <tt>tr()</tt> expression.

This is backwards compatible with BIP388, since the public keys are deduplicated. The key information vector will contain all the necessary public keys.

==Acknowledgements==

Thanks to Salvatore Ingala, Pieter Wuille, Antoine Poinsot, Andrew Kozlik and all others who
participated in discussions on this topic.
Loading