TL;DR: LiquiDEX is a working protocol to perform 2-step atomic swaps on Liquid. It requires a single interaction by the swap parties, which drastically improves the UX. It can be used as a building block for implementing more complex systems such as automated OTC desks, auctions or even a decentralized exchange (DEX).
Note: LiquiDEX is not production-ready yet!
Liquid Network and Atomic Swaps
The Liquid Network is a Bitcoin sidechain with Issued Assets and Confidential Transactions.
The native asset of Liquid is L-BTC (Liquid bitcoin), which has a cryptographic peg with Bitcoin.
New tokens can be issued on Liquid to represent digital assets (Issued Asset), one example is Tether USD (USDt) a stablecoin which units are worth $1.
As Bitcoin, Liquid uses a UTXO model and its transactions have a similar structure.
A simplified Bitcoin transaction, Alice sends 1 BTC to Bob:0.6 BTC Alice ->
A simplified Liquid transaction, Alice sends 0.5 L-BTC and 1000 USDt to Bob:
However with Confidential Transactions, inputs and outputs are blinded, so an external observer is not able to see the actual amounts and assets.
This is particularly useful for traders. Usually traders do not want to reveal their operations as such information may affect market prices.
In the above examples, all inputs belonged to Alice, but this does not have to be the case: some inputs and may belong to Alice and some inputs may belong to Bob.
Suppose Alice wants to swap some L-BTC for some USDt and Bob wants to do the opposite. Alice and Bob can cooperate to construct a transaction of this kind:
After the transaction, Alice has sent 0.5 L-BTC and received 600 USDt, while Bob has sent 600 USDt and received 0.5 L-BTC.
The transaction either happens or it doesn’t (it can’t happen partially), which makes the deal “atomic”. This is a P2P Atomic Swap. Alice and Bob swapped some asset, without trusting each other or the need of a trusted third party.
Liquid Swap Tool: 3-step Atomic Swaps
The first implementation supporting Atomic Swaps on Liquid is Liquid Swap Tool, which uses a 3-step protocol.
The 1st step consists in Alice proposing a swap:
At 2nd step Bob accepts the proposal:
However the transaction is not ready to be broadcasted, we need a 3rd step, where Alice finalizes the proposal:
These 3 steps are necessary to make the swap transaction indistinguishable from standard transactions.
However there are some drawbacks.
The protocol is more complex to analyze. It might fail at different steps for different reasons. Maybe the proposal is not well formatted, trade is not profitable anymore, and one party may abort the protocol.
Also Alice is required to be online to complete the protocol. If Alice takes too much time to finalize, Bob may do another swap and invalidate his accepted proposal.
This makes the UX cumbersome and it makes hard to integrate these swaps in other services.
A 2-step protocol would solve most of these issues. Indeed a 2-step protocol has a UX very similar to “send transaction”: Alice asks what she wants swap, then eventually the swap happens.
In the last months we build exactly that: LiquiDEX.
LiquiDEX: 2-Step Atomic Swaps
LiquiDEX is a 2-step protocol to perform atomic swaps on the Liquid Network.
The protocol involves 2 parties: the Maker and the Taker. Maker wants to send some assets and receive some other in exchange, it creates LiquiDEX proposal which is sent to the Taker. Taker accepts the proposal, and settles the swap on the Liquid Network.
Proposal Specification
- version: number, optional non negative integer, defaults to 0,
- tx: string, a hex-encoded signed Liquid transaction,
- inputs: array of objects, unblinded information for the transaction inputs; items have following attributes: asset: string, the hex-encoded asset, satoshi: int, the amount in satoshi, assetblinder: string, the hex-encoded asset blinder, amountblinder: string, the hex-encoded amount blinder,
- outputs: array of objects, unblinded information for the transaction outputs; items have the same format of inputs objects.
Asset, assetblinder and amountblinder are hex-serialized consistently with Elements Core, that is reversed w.r.t. their bytes serialization (as it’s done with txid).
The LiquiDEX proposal format defines what the Maker and Taker must agree on. Everything else can be chosen arbitrarily by the 2 parties to achieve the desired behavior.
Let’s analyze an example of what they might choose to do.
Step 1: Maker Makes
Maker has an UTXO U_xA
holding amount x
of asset A
, which he wants to swap with amount y
of asset B
.
Maker creates a transaction of spending a single UTXO U_xA
and receiving amount y
of asset B
.
Maker blinds the output. In practice this has some non trivial challenges, trade-offs and implementation details are discussed in a following section.
Maker signs the (only) input with SIGHASH_SINGLE | SIGHASH_ANYONECANPAY
. This allows the Taker to add more inputs and outputs without invalidating the Maker signature.
Makers creates a LiquiDEX proposal as specified above, and sends the proposal to the Taker.
Step 2: Taker Takes
Taker receives the proposal and does some verifications which might include:
- inputs and outputs arrays have length 1
- tx is a valid Liquid transaction
- tx has 1 input and 1 output
- tx input is unspent
- tx is signed, and the signature is valid with
SIGHASH_SINGLE | SIGHASH_ANYONECANPAY
- output commitments match outputs’ unblinded information
- previous output commitments match inputs’ unblinded information
Taker adds an output which receives x
of asset A
.
Taker funds the transaction, i.e. it adds inputs for asset B
and fee, changes outputs if needed, and the explicit fee output.
Taker blinds the transaction, using the unblinded information from the proposal.
Taker signs the newly added inputs with SIGHASH_ALL
.
Taker broadcasts the transaction, and once included in a block, the swap is settled.
Pros and Cons
LiquiDEX comes with some trade-offs. Here we summarize them.
Pros:
- Better UX.
- Less requirements for swap participants; Maker makes the proposal and can go offline, Taker takes and is settled few minutes after.
- Protocol is easier to analyze.
- Easier to integrate in more complex systems.
- Maker does not learn unblinding information from the Taker.
Cons:
- Maker sends a single UTXO, if the amount is not the one desired, an additional transaction it’s required.
- Less anonymity; LiquiDEX swaps are recognizable, since
SIGHASH_SINGLE | SIGHASH_ANYONECANPAY
is visible in the transaction.
Use Cases
Now we show some applications that LiquiDEX enables.
Make Mutually Exclusive Proposals
Suppose Maker wants to sell some L-BTC for USDt or L-CAD. It can make 2 proposals with the same UTXO, one sending L-BTC and receiving USDt and another sending L-BTC and receiving L-CAD. Since the UTXO can only be spent once, it is sure it will either receive USDT or L-CAD.
Take Batch Proposals
Taker can construct its transaction using multiple proposals. For instance:
Automated OTC Desks
One can implement a service that accepts proposals from users and takes care of matching them. It can both use other users funds or its own liquidity.
Auctions
Alice issued a new asset, let’s call it NFT. She can hold an auction for that. She publishes the asset, amount and blinders of the output she wants to send and will accept proposals swapping L-BTC (or any other asset she wants to receive) for her NFT. After some time has passed she will take the proposal that is more profitable for her.
If she wants to sell the asset at certain price, then she can make the proposal and hope that someone will take it.
DEX
A group of users may relay proposal among each other in peer-to-peer fashion, maintaining a decentralized order book. This would be exactly a decentralized exchange (DEX).
A working Implementation
Once we convinced ourselves that the idea was actually viable, we started iterating on prototypes of increasing complexity.
Prototype 1: Unblinded
The first iteration had all inputs and inputs unblinded.
It requires an Elements Core node and a small Python script with no extra dependencies to run the protocol.
Prototype 2: Makers Blinds
Then we added support for the taker to use blinded inputs and outputs.
This required an additional dependency, wally, to perform some cryptographic operations.
Prototype 3: Blinded Case (broken)
The remaining step was allowing the maker to use blinded inputs as well. We did so, but unfortunately the actual implementation was broken.
With Confidential Transactions, the unblinded information is encrypted in one of the output fields, the rangeproof. When a transaction is received, this unblinded information is decrypted and use for spending. However such field is not covered by the transaction signature. Thus the Taker can replace its value and the Maker won’t be able to blind the transaction.
A solution for this issue is to persist the unblinded information of each proposal locally, without relying on the rangeproof data. Unfortunately this is not compatible with elements-cli
. Therefore we were force to find a new idea to complete our implementation.
BEWallet
We needed a wallet that allows us to experiment with minimal overhead. We chose to start from Blockstream’s GDK Rust implementation that uses Electrum servers. The idea was to strip all unneeded features to a obtain a very simple but working Liquid Electrum wallet. This multi-months effort carried out in our evening and weekends resulted in BEWallet.
Then we added LiquiDEX support. While doing so we tried to minimize the amount of data that users should backup. An Electrum wallet (single sig) backup usually consists in a BIP39 mnemonic. But LiquiDEX requires a bit more. The trivial approach is to persist all the made proposals, but we can do better.
First we can derive the asset blinder and amount blinder deterministically. Moreover the transaction has a field that the Maker doesn’t use, the nonce commitment. This field is signed so it can be used to store some useful data. We’d like to store 32 bytes for the asset and 8 bytes for the amount, but we only have 32 bytes (and 1 bit) available.
We chose to encrypt the amount in the nonce field using AES GCM IV, since we already had that as a dependency to encrypt the local database.
The asset instead will be brute-forced against the asset commitment. When a proposal is made, the wallet will persist the asset that it might receive locally. When unblinding, it tries all the assets previously persisted until it finds a match.
What if we restore the wallet on another device? We can export the assets list from the previous wallet, if we lost it, we might remember it, since we probably only traded the most popular assets, or we could even find every asset ever issued on Liquid and try all of those.
Anyway this might change to something smarter in the future. We just wanted something that worked now. BEWallet remains a sideproject in its early days, There might be bugs and we’ll probably introduce breaking changes in the near future, so please test it with reasonably small amounts!
A Swap with BEWallet-cli
Install the wallet:
Maker list its coins to choose the one to be swapped:
Then makes the proposal:
And sends it to the Taker, which can accept it:
Which resulted in this transaction, which can be partially unblinded by the maker and fully unblinded by the taker.
Note that the Taker step can be performed with taker-cli.py
and a Elements node.
Possible Improvements
BEWallet LiquiDEX implementation works now, but it’s far from being perfect. We cut several corners, there are many possible optimizations and interface improvements that we might implement in the near future.
However there are a couple of improvements that we really like to have, but for which we have to wait a bit more.
We’d like to get rid of out custom JSON format to use Partially Signed Elements Transaction, however that is not ready yet.
Then we’d rather reduce the complexity for the Maker and its unblinding procedure. That can be done using SIGHASH_RANGEPROOF
, a new sighash type that covers also the rangeproof. However we need to wait for it to be deployed.
Once we have both, any wallet with PSET support can implement LiquiDEX with minimal changes.
Conclusions
The Liquid Network is blockchain with native assets, which allows two or more parties to cooperatively construct a transaction. This transaction may consist in swap of assets between the parties, in other words a P2P atomic swap.
The initial implementation of the swap protocol had 3 steps. However it has several problems, a non-trivial UX and annoying requirements for swap participants.
LiquiDEX is a protocol to perform 2-step P2P atomic swaps. This improves the UX by requiring a single interaction between the Maker and the Taker, with reasonable compromises.
LiquiDEX is easier to integrate in other systems, and can be the building block to implement OTC desks, auction systems, DEX and perhaps more.
BEWallet is a working Liquid Electrum wallet with support for LiquiDEX, which can be tried today.
Acknowledgements
I’d like to thank Riccardo Casatta, who had the original idea. Luca and Valerio Vaccaro who helped me testing, in particular Valerio set up liquidex.it, a website to upload proposals, and suggested the auction idea.
Note: This article was originally published at: https://leocomandini.github.io/2021/06/15/liquidex.html