Submarine Swap - On-chain to Off-chain
To follow along this tutorial:
|
A bitcoin user (alice_1) would like to pay on-chain a merchant selling a good off-chain, using a swap provider (bob_1). Technically alice_1 could operate the swap provider herself, but we will suppose here that the swap provider is a trustless third party.
This animation sums up the process.
Explanation:
-
The merchant starts by generating a Lightning invoice.
-
The merchant transmits the payment hash of this invoice to swap provider.
-
The swap provider creates the P2WSH HTLC swap smart contract and generates its bitcoin address.
-
The swap provider prompts the user to pay this bitcoin address.
-
Once the swap contract has been paid and confirmed, the swap provider pay the Lightning invoice.
-
The payment preimage is revealed which allows him to redeem the funds locked in the swap contract.
-
Lastly, if the swap provider fails to pay the Lightning invoice, the user can redeem the funds after a timelock.
Generating a Lightning invoice
$ lncli-merchant addinvoice 1000
PAYMENT_REQUEST and PAYMENT_HASH
This payment hash is the SHA256 hash of the *payment preimage*, the secret revealed when the invoice is paid.
addinvoice
returns the payment hash, but we can also get it by decoding the payment request.
$ lncli-merchant decodepayreq PAYMENT_REQUEST
PAYMENT_HASH
We can now imagine that the merchant sends this PAYMENT_HASH to the swap provider.
Creating and Funding the P2WSH Swap Contract
The goal now is for the swap provider to create the swap P2WSH smart contract, generate its bitcoin address and ask the bitcoin user to pay this address.
const bitcoin = require('bitcoinjs-lib')
const { alice, bob } = require('./wallets.json')
const network = bitcoin.networks.regtest
const bip65 = require('bip65')
Here is the swap smart contract that we will use. This is technically a witness script. This contract is a Hash Time Locked Contract (HTLC) The LN payment hash is the SHA256 hash of the preimage. In order to save bytes, the swap contract hashlock is a HASH160, the RIPEMD160 of the payment hash.
const swapContractGenerator = function(claimPublicKey, refundPublicKey, preimageHash, cltv) {
return bitcoin.script.compile([
bitcoin.opcodes.OP_HASH160,
bitcoin.crypto.ripemd160(Buffer.from(PAYMENT_HASH, 'hex')),
bitcoin.opcodes.OP_EQUAL,
bitcoin.opcodes.OP_IF,
claimPublicKey,
bitcoin.opcodes.OP_ELSE,
bitcoin.script.number.encode(cltv),
bitcoin.opcodes.OP_CHECKLOCKTIMEVERIFY,
bitcoin.opcodes.OP_DROP,
refundPublicKey,
bitcoin.opcodes.OP_ENDIF,
bitcoin.opcodes.OP_CHECKSIG,
])
}
// Signers
const keyPairUser = bitcoin.ECPair.fromWIF(alice[1].wif, network)
const keyPairSwapProvider = bitcoin.ECPair.fromWIF(bob[1].wif, network)
We have to choose a timelock expressed in block height.
Check the current block height and add 10 blocks to it. It means that the refund transaction will only be available 10 blocks after that the funding of the swap contract is confirmed.
getblockchaininfo
const timelock = bip65.encode({ blocks: TIMELOCK })
console.log('Timelock expressed in block height:')
console.log(timelock)
We now have all the elements to generate the swap contract, namely the swap provider’s public key, the user’s public key, the Lightning invoice’s payment hash and the timelock.
const swapContract = swapContractGenerator(keyPairSwapProvider.publicKey, keyPairUser.publicKey, PAYMENT_HASH, timelock)
console.log('Swap contract (witness script):')
console.log(swapContract.toString('hex'))
decodescript SWAP_CONTRACT
const p2wsh = bitcoin.payments.p2wsh({redeem: {output: swapContract, network}, network})
console.log('P2WSH swap smart contract address:')
console.log(p2wsh.address) (1)
1 | The p2wsh method will generate an object that contains the P2WSH address. |
The swap provider asks our bitcoin user alice_1 to pay this address.
sendtoaddress SWAP_CONTRACT_ADDRESS 0.000012
The user is paying 200 satoshis more than what is asked by the merchant to compensate for the mining fees that the swap provider will have to pay to redeem the funds. |
gettransaction TX_ID
or
getrawtransaction TX_ID
The output script of our funding transaction is a versioned witness program. It is composed as follow: <00 version byte>
+ <32-byte hash witness program>
.
The 32-byte witness program is the SHA256 hash of the witness script, which we will provide when redeeming the funds.
console.log(bitcoin.crypto.sha256(SWAP_CONTRACT).toString('hex'))
or
bx sha256 SWAP_CONTRACT
Creating the Redeem Transaction
Now that the swap contract is funded, the swap provider must pay the merchant’s invoice in order to get the payment preimage that allows him to redeem the swap contract on-chain funds.
$ lncli-sp payinvoice PAYMENT_REQUEST
PAYMENT_PREIMAGE
Prepare the bitcoin addresses of the potential recipients.
Either the swap provider in the happy case, or the user in the refund case.
const p2wpkhSwapProvider = bitcoin.payments.p2wpkh({pubkey: keyPairSwapProvider.publicKey, network})
console.log('Swap provider redeem address:')
console.log(p2wpkhSwapProvider.address)
const p2wpkhUser = bitcoin.payments.p2wpkh({pubkey: keyPairUser.publicKey, network})
console.log('Swap provider redeem address:')
console.log(p2wpkhUser.address)
const txb = new bitcoin.TransactionBuilder(network)
For the refund case we need to set the transaction-level locktime in our redeem transaction in order to spend a CLTV timelock. You can use the same value as before.
Because CLTV actually uses nLocktime enforcement consensus rules the time is checked indirectly by comparing redeem transaction-level nLocktime with the CLTV value. |
txb.setLockTime(timelock)
// txb.addInput(prevTx, prevOut, sequence, prevTxScript)
txb.addInput(TX_ID, TX_VOUT, 0xfffffffe)
// Happy case: swap provider redeems the funds to his address.
txb.addOutput(p2wpkhSwapProvider.address, 1e3)
// Refund case: the user redeems the funds to his address
txb.addOutput(p2wpkhUser.address, 1e3)
The bitcoin user alice_1 has paid the swap contract 1200 satoshis and the redeemer is only taking 1000 satoshis. |
const tx = txb.buildIncomplete()
Generate the signature hash, the actual message that we will sign.
Amongst other things, it commits to the witness script, the bitcoin amount of the UTXO we are spending and the sighash type.
const sigHash = bitcoin.Transaction.SIGHASH_ALL
signatureHash = tx.hashForWitnessV0(0, buffer.from(WITNESS_SCRIPT, 'hex'), 12e2, sigHash)
console.log('Signature hash:')
console.log(signatureHash.toString('hex'))
Adding the witness data
Our redeem transaction is almost ready, we just need to add the witness data that will unlock the swap contract output script.
Happy case: Swap Provider is able to spend the P2WSH.
The swap provider provides a valid signature and the payment preimage.
const witnessStackClaimBranch = bitcoin.payments.p2wsh({
redeem: {
input: bitcoin.script.compile([
bitcoin.script.signature.encode(keyPairSwapProvider.sign(signatureHash), sigHash),
buffer.from(PREIMAGE, 'hex')
]),
output: buffer.from(WITNESS_SCRIPT, 'hex')
}
}).witness
console.log('Happy case witness stack:')
console.log(witnessStackClaimBranch.map(x => x.toString('hex')))
Failure case: User ask a refund after the timelock has expired.
The user provides a valid signature and any invalid preimage in order to trigger the else branch of the swap contract.
const witnessStackRefundBranch = bitcoin.payments.p2wsh({
redeem: {
input: bitcoin.script.compile([
bitcoin.script.signature.encode(keyPairUser.sign(signatureHash), sigHash),
Buffer.from('', 'hex')
]),
output: buffer.from(WITNESS_SCRIPT, 'hex')
}
}).witness
console.log('Refund case witness stack:')
console.log(witnessStackRefundBranch.map(x => x.toString('hex')))
tx.setWitness(0, witnessStackClaimBranch)
// tx.setWitness(0, witnessStackRefundBranch)
console.log('Redeem transaction:')
console.log(tx.toHex())
Observations
If the swap provider do not fail to pay the merchant, our bitcoin user has paid on-chain, in a trustless manner, a merchant that is selling a good off-chain.
For both scenarios we note that our scriptSig is empty.
For the first scenario, we note that our witness stack contains:
-
Bob_1 swap provider signature
-
The LN payment preimage
-
The witness script, that we can decode with
decodescript
For the second scenario, we note that our witness stack contains:
-
Alice_1 user signature
-
A dummy LN payment preimage
-
The witness script, that we can decode with
decodescript