Swaproot: cheaper and more private on-chain deposits on Phoenix

Published February 23, 2024

TL; DR: Depositing on-chain funds to Phoenix is now cheaper(*) and more private, thanks to a combination of powerful new features added to Bitcoin and Lightning in the last few years.

(*) by 16% if the swap transaction has one input, 23% for 2 inputs, 27% for 3 inputs.


Last year, we published a major upgrade to our Phoenix wallet: users now have a single channel that will grow and shrink on-demand.

One of the key features included in this upgrade was trustless, instant “swap-ins”: you can send funds to your wallet’s on-chain address, and they will be “spliced” into your existing channel.

But the swap-in protocol that we designed for this upgrade could be improved:

  • the swap-in address displayed by Phoenix was static, which is a privacy issue;
  • swap-in transactions sent to Phoenix had specific features that made them easy to track on-chain, which is also a privacy issue.

Using Taproot, MuSig2 and bitcoin descriptors, we designed and implemented a new swap-in protocol: swap-in transactions are now cheaper, harder to track on-chain, and Phoenix will generate a new swap-in address every time you receive a transaction.

What is a swap-in ?

A swap-in is an on-chain transaction that can be used to add funds to a Lightning channel. Phoenix uses a trustless, instant swap-in protocol based on swap-in potentiam.

This protocol is compatible with the “zero–conf” philosophy that is a key to the Phoenix UX: the channel is still usable while Phoenix waits for swap-in transactions to confirm. As soon as they do, Phoenix triggers a zero-conf splice.

We previously used a pay-to-script construction, the script being user key + server key OR user key + delay. Here, user is a Phoenix user and server is the ACINQ node.

There are 2 options to spend this script:

  • normal use-case: user and server agree to “splice-in” the transaction into a new or existing channel.
  • refund use-case: the swap-in transaction remains unspent, and after a delay the user can spend it with their key (this is how users would recover their funds if ACINQ disappears).

But this design has some limitations:

  • Pay-to-script transactions are expensive, and the cost is paid not when they are published but when they are spent, because the spending transaction must include the complete script that is used.
  • They lack privacy: pay-to-script transactions are different from regular pay-to-public-key transactions, and can be tracked on-chain. In this case, the script is fairly specific, and there is a very good chance that transactions that include this script are Phoenix swap-in transactions.
  • A generic recovery procedure for the refund case is hard to set up. This is why a Phoenix user’s swap-in address is static, which is also a privacy issue.

What is Taproot and how can it help ?

Taproot (see BIP 340 and BIP 341) was a major Bitcoin upgrade that brought many improvements to the Bitcoin protocol, including:

  • Schnorr signatures, which can be composed much more easily than ECDSA signatures;
  • A new design for pay-to-script transactions, where you do not have to reveal the complete script anymore;
  • The ability, in some cases, to make pay-to-script transactions impossible to distinguish from pay-to-public-key transactions.

This last item is the key to our new swap-in protocol. Taproot introduced the concept of key-path spending (spend with a key) and script-path spending (spend with a script).

But unlike older bitcoin transactions where you had to choose between pay-to-public-key and pay-to-script, here you can do both. You can implement protocols like pay-to-public-key OR pay-to-script, where in the pay-to-public-key case, the on-chain transaction will look exactly like any other pay-to-public-key transaction.

So let's get back to our swap-in script: user key + server key OR user key + delay. To use the key-path spend for the mutual case, we need to replace user key + server key with a single public key. How is that possible?

What is MuSig2 and how can it help ?

With the introduction of Schnorr signatures, combining public keys and signatures becomes very easy. In fact, if you have a bunch of signatures created with N different private keys for the same message, you can basically add all signatures together, add all public keys, and you’ll get a valid signature for the aggregate public key.

This is dead simple… and also completely unsafe!

It turns out that it is so easy to add things together that if you use public key P then an attacker can simply use -P and “cancel” it.

Enters MuSig2, an algorithm for aggregating signatures and public keys that is provably secure and production ready.

So, with MuSig2, we can actually aggregate user key and server key into a single public key that can be used in Taproot key-path spends, and our script becomes: aggregated user + server key OR user key + delay:

Breakdown of our swap-in script

In the mutual case, the splice-in transaction that uses the swap-in output becomes cheaper and indistinguishable from any other pay-to-taproot-address transaction. Nice!

But what about swap-in address rotation? How can we implement it and still provide a generic recovery procedure that would scan all potential swap-in addresses?

What are descriptors, and how can they help?

Descriptors is a simple language that can be used to describe standard wallet patterns, including BIP32 key generation.

Descriptors can use miniscript to describe most standard script patterns. Miniscript supports Taproot, but not MuSig2 (yet): it can be used to explicitly specify key-spend public keys and script-path script (including address generation).

To make our swap-in protocol compatible with miniscript descriptors, we just need to use a fixed user key for the mutual case (*), and a different user refund key for the refund case, which we can rotate using a BIP32 scheme:

  • We get a different swap-in address for each new refund key
  • Swap-in outputs used in channel splices are still indistinguishable from regular p2tr transactions
  • We can describe our recovery wallet with a single, compact descriptor that understands key rotation

This is what a swap-in recovery descriptor looks like:

tr(
  // the fixed key-path spend key
  1fc559d9c96c5953895d3150e64ebf3dd696a0b0...48ff6251d7e60d1,
  // the script-path spend script
  and_v(
    // the xprv with the derivation path
    v:pk(xprvA1EfxcCy5HJnYBfPmwi9iXAyCktUSN...tvYsWqFTu29/...),
    // CLTC timeout
    older(2590)
  )
// checksum
)#sv8ug44m

(*) each Phoenix wallet gets a unique user key and a unique server key (both derived from your 12-words seed).

Conclusion

Our swap-in protocol is standing on the shoulder of giants:

  • The dual-funding and the interactive transaction protocol that was merged into the Lightning specifications;
  • The Taproot upgrade, which makes bitcoin script more powerful and more private;
  • MuSig2, which enables key and signature aggregation;
  • The work on descriptors and miniscript, with extensive support added to Bitcoin Core 26.

All these features and protocols may seem daunting at first, and it may be difficult to see how they benefit end users. Here it becomes obvious: they are tools to build better, less expensive, more private protocols that are also easier to use. Which is what Phoenix is all about!