Edit: check out our next blog post to discover how we solved the rangeproof issue.

TL;DR: LiquiDEX is a working protocol to perform 2-steps 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).

The post is structured as follow:

Introduction

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 -> 1   BTC Bob
0.5 BTC Alice    0.1 BTC Alice (change)

A simplified Liquid transaction, Alice sends 0.5 L-BTC and 1000 USDT to Bob:

1.1  L-BTC Alice -> 0.5  L-BTC Bob
5000 USDT  Alice    0.6  L-BTC Alice (change)
                    1000 USDT  Bob
                    4000 UDST  Alice (change)

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:

0.6  L-BTC Alice -> 0.5  L-BTC Bob
1000 USDT  Bob      0.1  L-BTC Alice (change)
                    600 USDT  Alice
                    400 UDST  Bob (change)

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-steps Atomic Swaps

The first implementation supporting Atomic Swaps on Liquid is Liquid Swap Tool, which uses a 3 steps protocol.

The 1st step consists in Alice proposing a swap:

liquidswap-cli propose L-BTC 0.5 USDT 600 --output proposal.txt

At 2nd step Bob accepts the proposal:

liquidswap-cli accept proposal.txt --output accepted.txt

However the transaction is not ready to be broadcasted, we need a 3rd step, where Alice finalizes the proposal:

liquidswap-cli finalize accepted.txt --send

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-steps Atomic Swaps

LiquiDEX is a 2-steps 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

A LiquiDEX proposal consists in a JSON object with the following attributes:

  • 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 add 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:

1    L-BTC maker1 -> 1000 USDT  maker1
2    L-BTC maker2    2000 USDT  maker2
3000 USDT  taker     3    L-BTC taker

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:

git clone https://github.com/LeoComandini/BEWallet-cli.git
cd BEWallet-cli
cargo install .

Maker list its coins to choose the one to be swapped:

$ bewallet-cli --mainnet --electrum-url blockstream.info:995 --data-root $PWD --mnemonic "$MNEMONIC" get-coins | jq
[
  ...
  {
    "txo": {
      "outpoint": "[elements]aa7c75c05f5944802eb4eda0c19fddbfcc230f032e5135bceeb56faf7af6e423:0",
      "script_pubkey": "a9145118eba7d058857216c7d190186fbb7b296cc05887",
      "height": 799437
    },
    "unblinded": {
      "asset": "beebee1a548fbb20280e539b697de076d87859a25c2983ebc55f2d8bec40abc3",
      "asset_blinder": "299999a36875bdb950a232de77f4d81fecf7fc073bb93e28940929e4b130edbc",
      "amount_blinder": "c44f9bdb17a0754231305b36fcd046de567210689cc6de1eb11778a51b921d52",
      "amount": 5
    }
  }
]

Then makes the proposal:

$ bewallet-cli --mainnet --electrum-url blockstream.info:995 --data-root $PWD --mnemonic "$MNEMONIC" liquidex-make --txid aa7c75c05f5944802eb4eda0c19fddbfcc230f032e5135bceeb56faf7af6e423 --vout 0 --asset 8026fa969633b7b6f504f99dde71335d633b43d18314c501055fcd88b9fcb8de --rate 1
{"version":0,"tx":"02000000010123e4f67aaf6fb5eebc35512e030f23ccbfdd9fc1a0edb42e8044595fc0757caa00000000171600149fec391992ea13a5e29fd55bd38050bfef83d5c4feffffff010aa630e1848d2c5853a6aaef9a36ab0e5f677ed3c9642825acc3f3e3cd2d5e2f63098af87f31f095ce416a8a32bac2a715f2d3920f511a93d9295fbf149010b1a38702d30da456e26ff055f8d69ea5cd144aaffc3e36314d1dea552b0c217d6ecb04f817a914b9eab96318ef4f7b60858a0586ff474a7024061b8700000000000002483045022100a91c9d744d19fe22231561b9ee89aa0674b6b876d63835fb807a441a041c0c5b02201e336469acad3ee94b0a73b609d1e73f5de93e6068a93e44a08720fb1bc6f6948321034a6c4485c1b5a26f61225f575a3ff0dda62bfb52c7008e9c592e68aa87d557f2000000","inputs":[{"asset":"beebee1a548fbb20280e539b697de076d87859a25c2983ebc55f2d8bec40abc3","asset_blinder":"299999a36875bdb950a232de77f4d81fecf7fc073bb93e28940929e4b130edbc","amount_blinder":"c44f9bdb17a0754231305b36fcd046de567210689cc6de1eb11778a51b921d52","amount":5}],"outputs":[{"asset":"8026fa969633b7b6f504f99dde71335d633b43d18314c501055fcd88b9fcb8de","asset_blinder":"ee390be3dd1e53d1f723e1007234fa3074691a7d5f1cfdbd75e0255284ee40b1","amount_blinder":"70b0d48bc015e4930d4688ca67379e4a637a78c70f573fc009cf1cc1bfe32e3a","amount":5}]}

And sends it to the Taker, which can accept it:

$ bewallet-cli --mainnet --electrum-url blockstream.info:995 --data-root $PWD --mnemonic "$MNEMONIC" liquidex-take --proposal '{"version":0,"tx":"02000000010123e4f67aaf6fb5eebc35512e030f23ccbfdd9fc1a0edb42e8044595fc0757caa00000000171600149fec391992ea13a5e29fd55bd38050bfef83d5c4feffffff010aa630e1848d2c5853a6aaef9a36ab0e5f677ed3c9642825acc3f3e3cd2d5e2f63098af87f31f095ce416a8a32bac2a715f2d3920f511a93d9295fbf149010b1a38702d30da456e26ff055f8d69ea5cd144aaffc3e36314d1dea552b0c217d6ecb04f817a914b9eab96318ef4f7b60858a0586ff474a7024061b8700000000000002483045022100a91c9d744d19fe22231561b9ee89aa0674b6b876d63835fb807a441a041c0c5b02201e336469acad3ee94b0a73b609d1e73f5de93e6068a93e44a08720fb1bc6f6948321034a6c4485c1b5a26f61225f575a3ff0dda62bfb52c7008e9c592e68aa87d557f2000000","inputs":[{"asset":"beebee1a548fbb20280e539b697de076d87859a25c2983ebc55f2d8bec40abc3","asset_blinder":"299999a36875bdb950a232de77f4d81fecf7fc073bb93e28940929e4b130edbc","amount_blinder":"c44f9bdb17a0754231305b36fcd046de567210689cc6de1eb11778a51b921d52","amount":5}],"outputs":[{"asset":"8026fa969633b7b6f504f99dde71335d633b43d18314c501055fcd88b9fcb8de","asset_blinder":"ee390be3dd1e53d1f723e1007234fa3074691a7d5f1cfdbd75e0255284ee40b1","amount_blinder":"70b0d48bc015e4930d4688ca67379e4a637a78c70f573fc009cf1cc1bfe32e3a","amount":5}]}' --broadcast
1980adeb659579e3d5fca673ab00e2f9a4fb933bfcabc0d52946ad3260552882

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-steps 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.