
It has been a long requested feature for Unchained to support native segwit addresses. While spending to segwit has been supported for years (and more recently spend to taproot), having segwit support in our platform has been a more complex endeavor. One of the advantages of Unchained’s collaborative custody model also represents a technical challenge.
For a custodial service to upgrade to a new script version like segwit, while still not without its technical complexities, from a user’s perspective should have very little impact other than a new address format when you click to get a new address. Even for non-custodial and non-collaborative systems once the underlying libraries have support, it’s relatively simple to let users create new wallets with the new format, which is why our open source coordinator Caravan has had segwit support for years.
In introducing segwit support we wanted to make sure the upgrade path was as seamless as possible. In this post we’ll explain some of the technical decisions we made to get there, how we aimed to minimize risks in upgrading our backend data models, and the design decisions made to avoid complex migrations for users.
What is segwit anyway?
Segregated Witness or “Segwit” was a protocol upgrade activated via soft fork on the Bitcoin network back in 2017. It brought several technical enhancements and UX improvements to bitcoin, notably larger blocks, which helped with fee congestion, and a transaction malleability fix, which was required to enable the Lightning Network.
Relevant to the subject of this post, segwit also introduced a new signing algorithm by which signers are expected to sign transaction data as well as how the signed transaction data itself should be serialized. In fact, this latter change is where the name comes from as the “witness” information, i.e. the program that satisfies the locking conditions of any given output, is now “segregated” from the other parts of the transaction in a separate data structure.
Segwit and derivation paths
The different signing algorithm is relevant because when signers receive a transaction, they need to know how to hash the data they’re expected to sign which is now dependent on the script version (i.e. non-segwit, segwit v0 and now segwit v1, aka Taproot). This information is also required for generating addresses:
Given just the private keys, it is not possible for restored wallets to know which kinds of output scripts and addresses to produce. This has led to incompatibilities between wallets when restoring a backup or exporting data for a watch only wallet.
Source: BIP380
Historically this has been solved by leveraging BIP43 purpose fields. That is that the extended public key origin used to generate public keys for a given redeem script would be derived from an HD (Hierarchical Deterministic) path, and the path itself signaled a script type “purpose”. Based on this purpose, the signer could know the signing algorithm to use. For example, a key derived from m/45’/**
according to BIP45 would indicate a key used in a P2SH multisig wallet, m/84’/**
for P2WPKH accounts, m/48’/**
for P2WSH or P2SH-P2WSH nested segwit multisig wallets, etc.
It is important to note however that none of this is enforceable at the protocol level and is nowhere encoded in an actual transaction. Just because Alice gives Bob an xpub to be used in a P2SH multisig quorum, doesn’t necessarily mean that xpub is derived from a m/45’
root derivation path.
For all intents and purposes, a public key derived from any path can be used in any valid redeem script. In fact, how an extended public key is used once exported from a device cannot be controlled, a feature that is leveraged in the blinded xpub protocol for example. Where this becomes a risk is that you could derive and export an extended public key from a seed, derive an unhardened child key from this and use that derived key in a multisig wallet quorum of any script type. You can use this wallet to store your bitcoin for years only to find that when you try to sign for a withdrawal that you are using a “restricted” or non-standard path and be blocked from signing as a result.
In other words, there is no way to prevent an address generated from a non-compliant path from receiving a deposit. Enforcement is only possible when you come into contact with the private key material: when you export or when you sign.
The UX of hierarchical keys at Unchained
For most of its history, Unchained has used non-standard paths for defining and deriving keys used in wallets. Despite introducing some possibility for compatibility issues as outlined above, it does provide some unique UX advantages for operating in a collaborative custody environment.
When you get started on the Unchained platform, one of the first operations you are asked to do is upload what we refer to as an Account Key. In the language of the braid model, this can be thought of as a “spool”, which is a hardened xpub under the m/45’/**
path from your seed. From this we derive a child xpub, which can be thought of as a “strand”, from an additional HD path depth. This child xpub is what we then use to generate wallets.
So, for example, we might import an account key spool from path m/45’/0’/0’
from your device. That xpub is then stored in our system and whenever a new wallet is created, e.g. when you create a vault, a new unhardened depth is appended, m/45’/0’/0’/0
, and the xpub from this path is used in a wallet. While this 5-depth path is non-standard, it allows us to non-interactively generate child xpubs, and new wallets from them, on-demand. So in the event of a key replacement where a new wallet needs to be generated and one key swapped out, or if you want to share a key with someone for them to use in their own vaults, there is no requirement to reconnect the device - the platform can use the existing information to derive the new keys and wallet.
The risks of this approach
While there are some clear usability benefits of this approach (including for our segwit upgrade path described below), there are some downsides worth acknowledging.
Compatibility
Notably, as already mentioned, any signer that requires adherence to the BIP32 path standards could block use of your key at signing time. Incidentally, this is a risk even if standards are being followed or if standards happen to change since these are not protocol-level requirements. Some devices could continue to support signing but issue warnings about strange derivation paths, which can be confusing and scary for some users.
Compatibility also makes recovery potentially harder since most automated recovery tools will check for keys at known paths. This is why it is important to back up your wallet configurations to avoid this risk.
Key Reuse
By not using existing standards, it is possible that exporting extended public keys with other coordinators could result in key reuse in different quorums or wallets. Key reuse is not advised due to some privacy and security risks. While the risk of this is minimal and is explicitly avoided in all wallets on the Unchained platform, it can be harder to account for when not using standard paths. Furthermore, there is the chance when deriving unhardened children that a leaked private key for a child exposes all private keys up to the last hardened path.
Additionally, reusing the same private key in different cryptographic algorithms, e.g. for ECDSA and Schnorr, could theoretically result in compromised data. Since everything in bitcoin prior to Taproot uses ECDSA, this risk does not apply in the context of what's being discussed here.
Descriptors and wallet registrations
Luckily, many of the drawbacks of both the BIP32 path approach to documenting key purpose as well as the lack of a cohesive standard are well understood problems and key motivators for developing output descriptors.
From BIP380 (emphasis added):
Output Script Descriptors introduces [sic] a generic solution to these issues. Script types are specified explicitly through the use of Script Expressions. Key derivation paths are specified explicitly in Key Expressions. These allow for creating wallet backups and exports which specify the exact scripts, subscripts (redeemScript, witnessScript, etc.), and keys to produce. With the general structure specified in this BIP, new Script Expressions can be introduced as new script types are added. Lastly, the use of common terminology and existing standards allow for Output Script Descriptors to be engineer readable so that the results can be understood at a glance.
In short, descriptors, which is a formalized language for representing wallet configurations, provide transparency into how to generate redeem scripts and their corresponding addresses. On the signing side, descriptors can also inform signers how to generate and verify scripts and transactions as well as how to sign, i.e. what hashing algorithm to use.
From a usability standpoint, wallet registration with descriptors or custom configuration formats allows for a precommitment to the wallets you will be generating addresses from and signing for. This provides greater guarantees around the addresses that are generated for wallets where your seed’s keys are involved by requiring user interactions to verify a descriptor in advance of usage. This is also an important protection against possible swap attacks.
Finally, because the descriptor does not contain any sensitive information from a security standpoint, you can add redundancy to your recovery processes with different backup strategies, including the backup that is automatically done by Unchained for every wallet in our system.
What about segwit?
Once we’ve decoupled ourselves from the artificial constraints of BIP32 paths for defining script type and address generation, we gain much more flexibility in what we can do with existing key material.
An important constraint that must be adhered to is that since a descriptor defines a wallet and includes in it the definition of script type, a wallet MUST have a consistent script type across all addresses. This limitation, however, does not carry over to new wallets since every wallet has a unique wallet configuration.
As noted earlier, Unchained supports generating new wallets without requiring directly interacting with your seed or signing devices. If we decided to use a different path for segwit wallets, like m/48’/
, then this would require accessing private key material. Instead, defining script type only via the descriptors/wallet configuration allows us to use existing spools within our system from already imported m/45’/**
paths.
From a technical implementation standpoint, this meant that the script type had to be explicitly defined, either P2SH (pay-to-script-hash) or P2WSH (pay-to-witness-script-hash), at wallet creation time. All existing wallets simply had to make the implicit type of P2SH explicitly defined in our database and wallet configurations and the creation of P2WSH would similarly define their script type upon wallet creation.
User upgrade path
To simplify the implementation of upgrading to segwit, we opted to start by locking in script type at the vault level. This means that if you want to create a segwit wallet, all you need to do is create a new vault. This will define the script types it supports and apply this constraint to all wallets it creates (including in the event of key replacements).
Our new data model does support multiple script types per product (vault or loan), and in theory we could have allowed for an upgrade by simply sweeping funds within the product to a new wallet. The biggest concern here was around UX complexity, requiring explicit upgrade pathways that the application does not currently have. Guarantees around data consistency and integrity would also have been more complex. Instead, a user can clearly see if their vault is using legacy script types and choose to upgrade by creating a new vault and migrating funds at their convenience, or use both in parallel.
Conclusion
While BIP43 purpose paths historically served an important utility of describing how a particular key was expected to be used, as a standard it left a lot to be desired. The BIP32 design space is infinitely large and requiring a BIP for every use case could discourage innovation and introduce unnecessary confusion especially as the standards are so difficult to enforce. Collaborative custody and blinded xpubs are just two examples of use cases that are hamstrung by path restrictions. Output descriptors, wallet registrations, and configuration files offer similar guarantees with much more flexibility for as yet unknown uses of the BIP32 HD key system. We believe that the tradeoffs are more than worth it, allowing us to pursue an upgrade path for segwit support in the Unchained ecosystem that reduced complexity both at the implementation level as well as for users.