Liquid
The following walkthrough demonstrates how to use libwally to create a transaction spending a confidential liquid utxo. For documentation of the Blockstream Liquid network please go to Blockstream
The example code here is written in python using the generated python swig wrappers.
Generating a confidential address
Start by creating a standard p2pkh address. Assume that we have defined
mnemonic
as a 24 word mnemonic for the wallet we want to use. From this we
can derive bip32 keys depending on the requirements of the wallet.
seed = wally.bip39_mnemonic_to_seed512(mnemonic, '')
wallet_master_key = wally.bip32_key_from_seed(
seed,
wally.BIP32_VER_TEST_PRIVATE, 0)
wallet_derived_key = wally.bip32_key_from_parent(
wallet_master_key,
1,
wally.BIP32_FLAG_KEY_PRIVATE)
address = wally.bip32_key_to_address(
wallet_derived_key,
wally.WALLY_ADDRESS_TYPE_P2PKH,
wally.WALLY_ADDRESS_VERSION_P2PKH_LIQUID_REGTEST)
For each new receive address a blinding key should be deterministically
derived from a master blinding key, itself derived from the bip39
mnemonic for the wallet. wally provides the function
wally_asset_blinding_key_from_seed()
which can be used to derive a master
blinding key from a mnemonic.
master_blinding_key = wally.asset_blinding_key_from_seed(seed)
script_pubkey = wally.address_to_scriptpubkey(
address,
wally.WALLY_NETWORK_LIQUID_REGTEST)
private_blinding_key = wally.asset_blinding_key_to_ec_private_key(
master_blinding_key,
script_pubkey)
public_blinding_key = wally.ec_public_key_from_private_key(
private_blinding_key)
Finally call the wally function wally_confidential_addr_from_addr()
to combine
the non-confidential address with the public blinding key to create a
confidential address. We also supply a blinding prefix indicating the
network version.
confidential_address = wally.confidential_addr_from_addr(
address,
wally.WALLY_CA_PREFIX_LIQUID_REGTEST,
public_blinding_key)
The confidential address can now be passed to liquid-cli to receive funds. We’ll send 1.1 BTC to our confidential address and save the raw hex transaction for further processing.
$ liquid-cli getrawtransaction $(sendtoaddress <confidential address> 1.1)
Receiving confidential assets
On receiving confidential (blinded) utxos you can use libwally to unblind and inspect them. Take the hex transaction returned by getrawtransaction above and create a libwally tx.
tx = wally.tx_from_hex(
tx_hex,
wally.WALLY_TX_FLAG_USE_ELEMENTS | wally.WALLY_TX_FLAG_USE_WITNESS)
The transaction will likely have three outputs: the utxo paying to our confidential address, a change output and an explicit fee output(Liquid transactions differ from standard bitcoin transaction in that the fee is an explicit output). Iterate over the transaction outputs and unblind any addressed to us.
asset_generators_in = b''
asset_ids_in = b''
values_in = []
abfs_in = b''
vbfs_in = b''
script_pubkeys_in = []
vouts_in = []
num_outputs = wally.tx_get_num_outputs(tx)
for vout in range(num_outputs):
script_pubkey_out = wally.tx_get_output_script(tx, vout)
if script_pubkey_out != script_pubkey:
continue
vouts_in.append(vout)
sender_ephemeral_pubkey = wally.tx_get_output_nonce(tx, vout)
rangeproof = wally.tx_get_output_rangeproof(tx, vout)
script_pubkey = wally.tx_get_output_script(tx, vout)
asset_commitment = wally.tx_get_output_asset(tx, vout)
value_commitment = wally.tx_get_output_value(tx, vout)
script_pubkeys_in.append(script_pubkey)
value, asset_id, abf, vbf = wally.asset_unblind(
sender_ephemeral_pubkey,
private_blinding_key,
rangeproof,
value_commitment,
script_pubkey,
asset_commitment)
asset_generator = wally.asset_generator_from_bytes(asset_id, abf)
# In Liquid, an asset commitment is used as a generator
assert(asset_generator == asset_commitment)
asset_generators_in += asset_generator
asset_ids_in += asset_id
values_in.append(value)
abfs_in += abf
vbfs_in += vbf
We have now unblinded the values and asset ids of the utxos. We’ve also saved the abfs (asset blinding factors) and vbfs (value binding factors) because they are needed for the next step: spending the utxos.
Spending confidential assets
The wallet logic will define the transaction outputs, values and fees. Here we
assume that we’re only dealing with a single asset and a single confidential
recipient address destination_address
to which we’ll pay the input amount
less some fixed fee
.
total_in = sum(values_in)
output_values = [total_in - fee,]
confidential_output_addresses = [destination_address,]
output_asset_ids = asset_ids_in[:wally.ASSET_TAG_LEN]
Generate blinding factors for the outputs. These are asset blinding
factors (abf) and value blinding factors (vbf). The blinding factors are
random except for the final vbf which must be calculated by calling
wally_asset_final_vbf()
.
num_inputs = len(values_in)
num_outputs = 1
abfs_out = os.urandom(32 * num_outputs)
vbfs_out = os.urandom(32 * (num_outputs - 1))
vbfs_out += wally.asset_final_vbf(
values_in + output_values,
num_inputs,
abfs_in + abfs_out,
vbfs_in + vbfs_out)
A confidential output address can be decomposed into a standard address
plus the public blinding key, and the libwally function
wally_address_to_scriptpubkey()
will provide the corresponding script pubkey.
blinding_pubkeys = [
wally.confidential_addr_to_ec_public_key(
confidential_address, address_prefix)
for confidential_address in confidential_output_addresses]
non_confidential_addresses = [
wally.confidential_addr_to_addr(
confidential_address, address_prefix)
for confidential_address in confidential_output_addresses]
script_pubkeys = [
wally.address_to_scriptpubkey(
non_confidential_address, network)
for non_confidential_address in non_confidential_addresses]
Create a new transaction
version = 2
locktime = 0
output_tx = wally.tx_init(version, locktime, 0, 0)
Iterate over the outputs and calculate the value commitment, rangeproof and surjectionproofs. This requires generating a random ephemeral public/private ec key pair for each blinded output.
for value, blinding_pubkey, script_pubkey in zip(
output_values, blinding_pubkeys, script_pubkeys):
abf, abfs_out = abfs_out[:32], abfs_out[32:]
vbf, vbfs_out = vbfs_out[:32], vbfs_out[32:]
asset_id, output_asset_ids = output_asset_ids[:32], output_asset_ids[32:]
generator = wally.asset_generator_from_bytes(asset_id, abf)
value_commitment = wally.asset_value_commitment(
value, vbf, generator)
ephemeral_privkey = os.urandom(32)
ephemeral_pubkey = wally.ec_public_key_from_private_key(
ephemeral_privkey)
rangeproof = wally.asset_rangeproof(
value,
blinding_pubkey,
ephemeral_privkey,
asset_id,
abf,
vbf,
value_commitment,
script_pubkey,
generator,
1, # min_value
0, # exponent
36 # bits
)
surjectionproof = wally.asset_surjectionproof(
asset_id,
abf,
generator,
os.urandom(32),
asset_ids_in,
abfs_in,
asset_generators_in
)
wally.tx_add_elements_raw_output(
output_tx,
script_pubkey,
generator,
value_commitment,
ephemeral_pubkey,
surjectionproof,
rangeproof,
0
)
Finally the fee output must be explicitly added (unlike standard Bitcoin transactions where the fee is implicit). The fee is always payable in the bitcoin asset. The wallet logic will determine the fee amount.
BITCOIN = "5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225"
BITCOIN = wally.hex_to_bytes(BITCOIN)[::-1]
wally.tx_add_elements_raw_output(
output_tx,
None,
bytearray([0x01]) + BITCOIN,
wally.tx_confidential_value_from_satoshi(fee),
None, # nonce
None, # surjection proof
None, # range proof
0)
Sign the transaction inputs.
flags = 0
prev_txid = wally.tx_get_txid(tx)
for vout in vouts_in:
wally.tx_add_elements_raw_input(
output_tx,
prev_txid,
vout,
0xffffffff,
None, # scriptSig
None, # witness
None, # nonce
None, # entropy
None, # issuance amount
None, # inflation keys
None, # issuance amount rangeproof
None, # inflation keys rangeproof
None, # pegin witness
flags)
for vin, script_pubkey in enumerate(script_pubkeys_in):
privkey = wally.bip32_key_get_priv_key(wallet_derived_key)
pubkey = wally.ec_public_key_from_private_key(privkey)
sighash = wally.tx_get_elements_signature_hash(
output_tx, vin, script_pubkey, None, wally.WALLY_SIGHASH_ALL, 0)
signature = wally.ec_sig_from_bytes(
privkey, sighash, wally.EC_FLAG_ECDSA)
scriptsig = wally.scriptsig_p2pkh_from_sig(
pubkey, signature, wally.WALLY_SIGHASH_ALL)
wally.tx_set_input_script(output_tx, vin, scriptsig)
The transaction is now ready to be broadcast, the hex value is easily
retrieved by calling wally_tx_to_hex()
.
tx_hex = wally.tx_to_hex(output_tx, wally.WALLY_TX_FLAG_USE_WITNESS)