BIP352 private key formats

I was mulling over SP support for things like BTCPayServer (which normally takes an xpub).

This would require some sort of encoding of the scan private + spend public combo.

I was thinking of all sorts of hacks, like using the xpub chaincode as the scan private (LOL) but obviously this breaks a lot of stuff with BIP352 so I binned the idea.

I think it would be a good idea to define a string format for:

  1. B_scan + B_m (currently defined in BIP352)
  2. b_scan + B_spend (for delegated scanning and generating new B_m SP addresses)
  3. b_scan + b_spend (for spending)

I was thinking we could just modify the HRP of sp and tsp. For private keys, leave the 0x00 byte in place of the 0x02/0x03 pubkey header bytes to keep the same length.

Delegated scan should be spscan and tspscan

All private should be sppriv and tsppriv

Thoughts? I apologize if this has been brought up.

There is a transcript of a previous discussion related to descriptors that has similar ideas on encoding: Bitcoin Core Dev Tech 2024: Silent Payment Descriptors.

For (3), let me add that b_scan and b_spend are not enough to spend outputs by themselves, as they require the tweak material from the original transaction creating the output. Shouldn’t we also consider a format for “self contained” spending material for individual outputs? like tweak + b_spend , where tweak = label + b_scan + shared secret or even b_scan + b_m + shared secret.

For (2), it may also be useful to encode a range or limit for the number of applicable labels on the string itself.

For 2 and 3 regarding labels and per-tx info (aggregate pubkey and smallest outpoint)

Per-tx info requiring a scan action is already stated in the BIP, so the assumption should be that the wallet will start a scan on import. Perhaps 2 and 3 should encode a block number for their “birthday” which would be the key asserting “no need to check below this block”

For labels, this could definitely be encoded, like an array of labels at the end. But I do think that even with no label the “0” (change) label should be checked.

If a wallet restores from BIP39 then the birthday should be assumed to be the latest block at the time which the BIP was published, and the only label should be the change label 0.

just some ideas. Might submit a PR to modify the BIP with the new encoding.

I have created a BIP PR to get the ball rolling: BIP352: Add extra encoding formats for scanning and spending by junderw · Pull Request #2026 · bitcoin/bips · GitHub

is this format intented to be used as output descriptor?

it could be defined using output descriptor language, yes.

but output descriptors describe outputs… it’s in their name…

without the information contained in the scanned transactions, we cannot describe any outputs… so the definition of the output descriptor would need to be very clear that no outputs can be generated by the descriptor alone.

It’s true that SP descriptors can’t really encode outputs, but this is orthogonal to the question of private vs public. It’s already the case that (public) SP descriptors work in kind of a meta way where the corresponding scriptPubKey is still a function of the actual transaction inputs, not just the contents of the descriptor string.

If we accept that meta-aspect, it’s a very small leap to permit private keys too. In all other settings, output descriptors with private key material (whether that’s WIP or xprv) are seen as effectively syntactic sugar for the corresponding public-only variant (hex pubkey instead of WIF, xpub instead of xprv) + also conveying the private key for it as additional out-of-band information.

It seems reasonable to follow the same approach for SP descriptors; allowing some encoding that includes private key material, permitting it in descriptors, and treating it as equivalent to the corresponding public version, but also conveying the included private keys.

1 Like

Thanks for the comment, Pieter.

Re-reading the BTC Transcripts link from earlier in this topic…

It seems like my proposal modification is essentially number 3. except without using descriptors and sppub/spprv. Also I add a birthday and a max label value.

I agree that including UTXO info and ECDH info to speed up recovery is overkill for a descriptor. But I think birthday and max label should be included as a hint to aide in scanners.

Whether the serialization is a descriptor or not is not a big issue for me, however, I think that wallets MUST output a standardized version and not have some wallets output sp(xprv/352h/0h,xpub/352h/0h,birthday,maxlabel) and some output sp(spprv,sppub,birthday,maxlabel)…

Whether core wants to accept all those as input for deserialization is fine, but I don’t think we should encourage wallet devs to choose their own combinations of keys. It will confuse users, and I can see a “smarter than the average guy” type that shoots themself in the foot by swapping an xpub without thinking it through.

In contrast, sppub or spscan or something like that is pretty obvious to the end user that “this is one logical unit of encoded info, don’t try and swap the internal bits around with other similar looking (xpub) data from other wallets” should prevent more accidents.

I feel like the scan and spend coupling is something that should be strongly coupled (discourage swapping parts by making the scan+spend keys (prv+pub and prv+prv) into one bech32m encoded string, then comma separated birthday block and max label as an integer. Since it would be possible and reasonable to tell a user that “increasing the birthday will make initial scan faster but maybe miss some UTXOs etc.

  1. Thoughts on birthday and maxlabel?
  2. Thoughts on encoding scan+spend into one unit while leaving birthday and maxlabel as ints?

I’m still very early in my thoughts on this. My initial thought was that birthday and maxlabel should be discouraged from being modified by end user, but I’m starting to think the opposite now if we’re going to move toward descriptors.

afaik others commonly used descriptors types don’t convey these informations (birthday & look_ahead/gap_limit), while often also (not strictly) necessary, I think it could be interesting to find a way that fit w/ all descriptors types rather having it as a special case for SP.

Also, these informations are not strictly necessary in SP context, their purpose is to rescan faster, and about max_label, I think we can reasonably assume that scanning with default quite hight value (100? 1000? 10_000?) cames at with nearly no noticeable performance cost.

I think keys shoud be represented as separated Key Expression as defined in BIP-0380 (in descriptor context)

Thoughts on birthday and maxlabel?

While no doubt useful for scanning, I think it may be disturbing for users to see their “xpub” or output descriptor change if they change their wallet birthday. The same is true for adding a label. That said, I believe the the use case of labels (publishing multiple SP addresses that differentiate incoming payments but trivially identify as the same entity) is limited to specialised applications like exchanges. Wallets should in general rather use SP keys derived from different BIP32 accounts.

Thoughts on encoding scan+spend into one unit while leaving birthday and maxlabel as ints?

I think there is merit to the idea of encoding the sp_version+scan_key+spend_key into a single unit. While it would have been possible to list a chaincode and key separately, xpubs have proven to be a useful convenience, and they eliminate “swapping” of keys as you suggest. As to leaving birthday and maxlabel as ints in an output descriptor format, I think these would be useful but suggest making them optional, where reasonable defaults (block height 842579 and one change label) are assumed if not supplied.

I think we can reasonably assume that scanning with default quite hight value (100? 1000? 10_000?) cames at with nearly no noticeable performance cost

I don’t believe this is true. Each additional label means at minimum an EC point addition and comparison against all eligible outputs for each transaction scanned. Here is are some benchmarking figures using Frigate on an 8x RTX 3060 GPU machine testing over a 64 week period:

Time (ms) Tx/sec
No label 16,949 4,199,074
Change label 17,042 4,176,159
10 labels 19,334 3,681,086
30 labels 25,109 2,834,446

Thanks for this numbers proving me wrong!