Unspendable keys in descriptors

Thanks @sipa for the additional context!

One observation is that unspend(HEXCHAINCODE) from your gist is actually compatible with the approach (s4) if the HEXCHAINCODE is deterministically computed according to the specs above, or any similar ones.

So the TL:DR is: you do need some good entropy for the unspendable key, but you don’t need new entropy.

Why is (1) a desirable property? I can think of one example (BIP352 - Silent payments) where we would want to standardize the use of H in protocols that require a provably unspendable keypath. Curious if you have any counter-examples where a standard public NUMS is not desirable.

Especially in this setting, revealing a recognizably unspendable internal key at spending time (remember, internal key is revealed in script path spends) would instantly reveal to the world that this was a script-only taproot output.

1 Like

What Pieter said. Plus not revealing it to the whole world by default doesn’t make it unprovable.

1 Like

This part I understand; I’m asking for an example of why revealing this to the world is bad. It certainly feels safer to not reveal it, but when discussing this with @RubenSomsen in the context of BIP352 where it would be beneficial to reveal to the world that this was a script-only spend, I struggled to come up with examples of why it would be bad to reveal this.

It is a form of fingerprinting. You can always reveal this information yourself if you want/need it, but it’s great if the standards don’t force you to do so.

1 Like

More of a fingerprint than the script itself? My thinking here is that in these protocols that require a provably unspendable keypath, the scripts themselves are likely sufficiently complex to be a fingerprint and must be revealed with every spend, anyways.

It seems satisfying all those properties would prevent the possibility of creating addresses using partial descriptors. Since you necessarily need additional information which isn’t onchain.

I can certainly imagine specific use cases where this will be the case, and revealing that the key path was unspendable isn’t an additional privacy loss over revealing the script itself.

But I also don’t think this is universally true. Maybe the script is just covering some combination of participants unable to MuSig sign a key path. Maybe there are very distinguishable scripts in the script tree, but there are others which aren’t. And I hope you’d agree that we shouldn’t adopt a standard that forces participants to reveal their key path was unspendable.

FWIW, even with the P = H+rG approach mentioned in BIP341 (with secret r) you can prove to contract participants that the key is unspendable, even without revealing r (by producing a BIP340 signature for key P-H, which has private key r).

I believe that’s why we preferred an option where the entropy was just in the descriptor itself.

1 Like

Maybe we want different properties for descriptors and wallet policies. We could both have the entropy be explicitly described in a descriptor (unspend()), and omitted for wallet policies since this standard would mandate the content of unspend() be deterministically derivable from the rest of the descriptor.

So properties (1), (2), (3) for output descriptors. And property (4) in addition for wallet policies.

That’s a good point, I think you’re right!

The only issue is that people today backup descriptors and not wallet policies, so the discrepancy is a UX problem at registration time.

I think people should backup wallet policies to represent accounts in software wallets, except for the exotic use cases where wallet policies don’t work.

Certainly agree on this point, but also don’t want to leave wallet users with the impression that this is chosen as a standard because revealing that only the script path was usable is always bad for privacy.

Good to know! For BIP352, the scenario I had in mind for revealing that the keypath was unusable is a coinjoin, where Alice wants to coinjoin her provably unspendable keypath UTXO and Bob wants to make a payment to a SP address. It sounds like in this scenario Alice could provide a signature for P - H to a coordinator, instead of the coordinator requiring that her script path spend show that H was the internal key.

EDIT: nevermind, this doesn’t work. It’s about the receiver knowing that the taproot spend was a script-only spend, and AFAICT there is no way to do this in a non-interactive way outside of making it public that only the script path was usable.

Maybe the simple solution is to rot13 the xpub

xpub<rot13>

Approach s2, corresponding to the descriptor changes document linked by @sipa above, seems straightforward to implement and very simple to explain. Yes, it comes at the expense of having to include some extra information in the descriptor. This seems like a worthwhile tradeoff given its simplicity.

1 Like

sipa’s linked approach is similar but not identical to s2, as in his version the argument of unspend() alters the chaincode, while in the version described in my notes above I was instead generating a different pubkey.

They are identical in terms of security properties, but I think sipa’s approach is better as it’s more straightforward to verify that one such xpub is unspendable (just look at the pubkey), while in the approach in s2 one has to explicitly redo the computation to verify how the xpub is generated.

Anyway, my current thinking is that sipa’s approach is probably the cleanest for descriptors, while wallet policies (now in the process of being finalized as BIP-0388) could add a deterministic way of computing the HEXCHAINCODE from the remaining keys, as suggested above by @AntoineP.

1 Like

I’m now implementing Taproot support in Liana. Ideally i’d like to derive unspendable internal keys in a way which would be forward compatible with an eventual standard used in wallet policies. This way, once support is implemented in signing devices, our users won’t have to “verify” a meaningless internal key on their device’s screen. A friendlier “no keypath spend” UX could be presented.

It seems the less ugly way of getting all 4 properties is what Salvatore suggests in s4 (in the simpler version). From a wallet policy, the unspendable internal key is an xpub/<0;1>/* such as:

  • The xpub’s key is the NUMS suggested in BIP341: H = lift_x(0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0).
  • The xpub’s chaincode is the sha256 of the concatenation of the key part (as a compressed pubkey) of all the xpubs in the wallet policy. (In the wallet policy standard all key expressions must be xpubs with derivation path of the form /<m;n>/*.) “Left to right” order as they appear in the string representation, which is just a depth-first search.

Of course, this can also be expressed in descriptors using unspend(computed chaincode)/<0;1>/* as the internal key.

An issue with using left-to-write is that since wallet policies have a separate list of xpubs (that are explicitly referenced by @index in the descriptor template), the natural order of the keys would be the one in the list, which is not guaranteed to match the left-to-right order in the descriptor template.

E.g.:
descriptor_template: "tr(_,{pk(@1/**),pk(@0/**)})"
keys: ["xpubA", "xpubB"]

Here the left-to-right order doesn’t match with the natural order pubkeyA||pubkeyB. If the _ (or whichever other expression) to represent the deterministic NUMS key is only defined for wallet policies, I’d favor the wallet_policy-native approach.

Your approach requires to have the wallet policy at hand, whereas mine can be used on any wallet-policy-compatible descriptor. I think it’s a nice property to have.

1 Like

The _ placeholder would be a wallet policy feature, so expecting to have the wallet policy seems a natural assumption, and I don’t think the standard should be optimized for “not having it”.

Other orders push code complexity to the hardware signer, that will now have to add more code to parse the “descriptor template” just to find the right order of keys.

Finally, a wallet policy might have the same @i multiple times (especially on taproot); in your version you’d either be concatenating the corresponding pubkey multiple times, or would need a stateful parser to skip the ones that have been seen already.

1 Like