LWK Overview and Examples

The Liquid Wallet Kit (LWK) is a lightweight, easy to install toolkit for building single and multi-signature Liquid wallets. It supports the Jade HWW and also contains a software signer allowing developers to build physical custody solutions, automated software signers, or combinations of the two.

The LWK Rust libraries can be used directly, via software wrappers or through an RPC client. LWK is split into different components that can be used independently or as a whole. Mobile app developers may only want to use the wallet and signer while back end developers might want to directly use the RPC client in their systems. LWK uses Blockstream's Electrum server to serve transaction and other blockchain data, meaning you do not need to sync an Elements node yourself, although you can set up your own Electrum and Elements if you want to not rely on another data provider.

The LWK GitHub site provides an overview of installation and use so here we will focus on breaking each step down and providing a worked through example. Our example will cover:

  1. Installation.
  2. Setting up signers.
  3. Creating a wallet (based on a 2-of-2 multi-signature descriptor).
  4. Importing and using the wallet.
  5. Spending from the wallet.

We use a 2-of-2 here just for simplicity but of course it can be any n-of-m scheme such as 2-of-3, 3-of-5 etc.

Installation

πŸ“˜

Prerequisites

You need to install Rust before proceeding. You can download Rust from the Rust website: https://www.rust-lang.org/tools/install.

You will also need to install "libudev-dev":

apt install -y libudev-dev

Once those are set up you can clone the LWK repository and build the binaries:

git clone [email protected]:Blockstream/lwk.git
cd lwk
cargo build --release
alias cli="$(pwd)/target/release/lwk_cli"

If you already have Rust installed and the build fails due to the version you have installed not being current enough you can use rustup to update and set the version of rustc to use using commands such as:

rustup update  
rustup default 1.75.0-x86_64-unknown-linux-gnu

Setting up signers

In our example, the 2 signers decide who will act as the proposer/coordinator for asset actions. Note that although any member can initiate an asset action it is probably easier to designate one member to act as the proposer/coordinator. During the initial setup phase both 2 signers will need to input their Xpub. We assume that 1 of the 2 signers will take on the co-ordinator role and: define the transaction including the L-BTC fee, coordinate the exchange of partially signed transactions, and finalize and broadcast the transaction. We'll call the signers S1 and S2 during the example.

Both signers need to start their LWK client and generate a new signer (a mnemonic will be output). You can either run cli server start in one terminal and then use another terminal to run cli signer generate (remember to move to the right directory and set the cli alias) or run them both from the same terminal using the & command as shown below. Running in separate terminals means you can view the server's logs as they are processed.

LWK uses Blockstream's Electrum server to provide blockchain data and carry out blockchain actions like transaction broadcasts etc, although you can set up your own Electrum instance if you want and provide the URL using the --electrum-url optional argument that can be passed to cli server start.

cli server start &
cli signer generate

Example output from Signer 1 running the above:

{
  "mnemonic": "sheriff pass mechanic old near spring over pioneer rural wealth symptom cook"
}

The mnemonic that has been generated now needs loading as a named signer. Both signers need to do this. Here's what Signer 1 does, note they name the signer "S1":

cli signer load-software -s S1 --mnemonic "sheriff pass mechanic old near spring over pioneer rural wealth symptom cook"

By naming the signer both Signer 1 and Signer 2 can reference the correct signer to use when actions are later needed. This helps if they have multiple signers for multiple wallets they may control with other entities and groups of signers.

To see what signers are currently loaded in LWK you can run:

cli signer list

Creating a wallet

In order to create a wallet based on a 2-of-2 multi-signature descriptor we need to extract the xpub from each of the two signers. The xpubs are then used to create the descriptor.

Signer 1 would run the following and Signer 2 would run the same but with the name (the -s argument) of S2:

cli signer xpub -s S1 --kind bip84

Example output:

{
  "keyorigin_xpub": "[2a0b5159/84h/1h/0h]tpubDDdqx3Ytv8SHAvQYqnh3NgoixyND49wSwcpMiaLMGuGFLC7gUZ4ibabz4R3qtTFYvR3G1n8MxFMtpue1qmKBz4i61J54chUxeTJ9Ma8f16M"
}

The coordinator imports the 2 xpubs into LWK to create a descriptor for the multisig. We will refer to subsequent funds held in transaction outputs that are locked by the conditions set out in the descriptor as being within the multi-signature wallet from now on.

πŸ“˜

Notes on descriptors

Notice that keyorigin-xpub is used as many times as needed to fulfil the m from the n-of-m scheme. In our example, we need to use it twice. The descriptor-blinding-key is elip151, the kindis wsh and the threshold for our example is 2 (i.e. 2 signatures from the provided 2 possible signatures are needed to make a transaction valid).

cli wallet multisig-desc --descriptor-blinding-key elip151 --kind wsh --threshold 2 --keyorigin-xpub "[2a0b5159/84h/1h/0h]tpubDDdqx3Ytv8SHAvQYqnh3NgoixyND49wSwcpMiaLMGuGFLC7gUZ4ibabz4R3qtTFYvR3G1n8MxFMtpue1qmKBz4i61J54chUxeTJ9Ma8f16M" --keyorigin-xpub "[6295429d/84h/1h/0h]tpubDDm7RGVu5vLmSfT5tq7gJx6KsYk9Veg6Ytvj3MNL1jTpPFjvM6j185YjXRvgET6owS6PSSkmStHb5bTjCwBMtTH2zp6WrGyN2S3rjbSzuYn"

An example of the output from the above is shown below:

{  
  "descriptor": "ct(elip151,elwsh(multi(2,[2a0b5159/84h/1h/0h]tpubDDdqx3Ytv8SHAvQYqnh3NgoixyND49wSwcpMiaLMGuGFLC7gUZ4ibabz4R3qtTFYvR3G1n8MxFMtpue1qmKBz4i61J54chUxeTJ9Ma8f16M/\<0;1>/_,[6295429d/84h/1h/0h]tpubDDm7RGVu5vLmSfT5tq7gJx6KsYk9Veg6Ytvj3MNL1jTpPFjvM6j185YjXRvgET6owS6PSSkmStHb5bTjCwBMtTH2zp6WrGyN2S3rjbSzuYn/\<0;1>/_)))"  
}

Importing and using the wallet

The coordinator shares the descriptor with the other signer. They must both import it into LWK so that their wallets use the same multi-signature descriptor and can display balances, create transactions, and sign transactions. Notice how in the following, Signer 1 loads the wallet with the name "W1" for future use.

cli wallet load -d "ct(elip151,elwsh(multi(2,[2a0b5159/84h/1h/0h]tpubDDdqx3Ytv8SHAvQYqnh3NgoixyND49wSwcpMiaLMGuGFLC7gUZ4ibabz4R3qtTFYvR3G1n8MxFMtpue1qmKBz4i61J54chUxeTJ9Ma8f16M/<0;1>/*,[6295429d/84h/1h/0h]tpubDDm7RGVu5vLmSfT5tq7gJx6KsYk9Veg6Ytvj3MNL1jTpPFjvM6j185YjXRvgET6owS6PSSkmStHb5bTjCwBMtTH2zp6WrGyN2S3rjbSzuYn/<0;1>/*)))" -w W1

Either of the participants can then generate a receive address, and send an initial L-BTC balance to that address. This will be used to pay the network fees for any subsequent transactions. Here we show Signer 1 getting the address. Notice how the wallet "W1" is referenced:

cli wallet address -w W1

An example output is:

{
"address": "tlq1qqvexwnnwkessvc5xukhytv7we5p0td4fsyx7e2lpfm7s7evrmcwcxwawvmrkjpc38uqa650ds5vxkmgu7gq93lnv5dcyx895pwxwg0andrnpr40e42mt",
  "index": 0
}

To check the balance of the descriptor wallet:

cli wallet balance -w W1

This will show the balance of the wallet by asset id. An example output showing 1,000,000 sats of the testnet L-BTC asset is shown below. You can get testnet L-BTC sent to your wallet to use for testing using https://liquidtestnet.com/faucet.

{
  "balance": {
    "144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49": 1000000
  }
}

Spending from the wallet

To send from the descriptor wallet, for example to send some L-BTC from it to another address we first have one of the wallet owners create the spending transaction. Note that the recipient argument is of the format address:amount:asset and that amount is in sats.

cli wallet send -w W1 --recipient "tlq1qqd4er47y4kh4gc2vc6lfh45ead5h89tuxdxglgwdlek5lg8renysvzmvh0zq5gg3l39rvzffqp56lcks5tykkfm4x8p5mwzfh:50000000:144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49" >> tx1

The output will be a very long PSET of the format:

{
  "pset": "cHNldP8B..etc..TBQBBAAAβ€œ
}

πŸ“˜

Handling PSET data

In the code line above (the wallet sendcommand) we save the returned JSON data to a file named "tx1" by appending >> tx1 to the end of the command. The hex string of the PSET is very long, care must be taken when sending the hex to the signers and it should be noted that some text editors may not handle long continuous strings very well. Make sure the signer can copy the PSET and signs the entire hex.

The creator sends the PSET to the other signer and they each sign it like this (note that they specify the name of their signer, "S1" is used below):

cli signer sign -s S1 --pset "cHNldP8B..etc..TBQBBAAAβ€œ >> signed1

The creator then combines the two signed PSET:

cli wallet combine -w W1 --pset β€œfirst signed pset here” --pset β€œsecond signed pset here” >> combined

Any party can then broadcasts the signed and completed transaction.

cli wallet broadcast -w W1 --pset "cHNldP8B..etc..AQQAAA=="

The above will show the Liquid transaction ID.

When the users have finished they should shut the server down:

cli server stop