Submarine Swap - On-chain to Off-chain

To follow along this tutorial:

  • You need to have at least two LND nodes linked by a working payment channel

  • One LND node is the merchant (lncli-merchant)

  • One LND node is the swap provider (lncli-sp)

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.

submarine swap - paying merchant on2off

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

The merchant creates a Lightning invoice (also called payment request) for 1000 satoshis.
$ 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.

Import libraries, test wallets and set the network.
const bitcoin = require('bitcoinjs-lib')
const { alice, bob } = require('./wallets.json')
const witnessStackToScriptWitness = require('./tools/witnessStackToScriptWitness')
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(swapProviderClaimPublicKey, userRefundPublicKey, PAYMENT_HASH, timelock) {
  return bitcoin.script.fromASM(
    `
      OP_HASH160
      ${bitcoin.crypto.ripemd160(Buffer.from(PAYMENT_HASH, 'hex')).toString('hex')}
      OP_EQUAL
      OP_IF
        ${swapProviderClaimPublicKey.toString('hex')}
      OP_ELSE
        ${bitcoin.script.number.encode(timelock).toString('hex')}
        OP_CHECKLOCKTIMEVERIFY
        OP_DROP
        ${userRefundPublicKey.toString('hex')}
      OP_ENDIF
      OP_CHECKSIG
    `
      .trim()
      .replace(/\s+/g, ' '),
  );
}
We prepare the key pairs for our three personas.
// 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 (1)
1 "blocks" property
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'))
We can decode the swap contract (witness script) in Bitcoin Core CLI.
decodescript SWAP_CONTRACT
Generate the bitcoin address of our 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.

Alice_1 sends 1200 satoshis to the P2WSH swap smart contract 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.
Get the output index (TX_VOUT). The swap provider (or the user in the refund case) will need it 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)
Create the PSBT.
const psbt = new bitcoin.Psbt({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.
nLocktime must be <= present time and >= CLTV timelock

if (IS_REFUND) {
  const timelock = bip65.encode({blocks: TIMELOCK})
  psbt.setLocktime(timelock)
  console.log('Timelock expressed in block height:')
  console.log(timelock)
  console.log()
}
Set the transaction input by pointing to the swap contract UTXO we are spending.
psbt
  .addInput({
    hash: TX_ID,
    index: TX_VOUT,
    sequence: 0xfffffffe,
    witnessUtxo: {
      script: Buffer.from('0020' +
        bitcoin.crypto.sha256(Buffer.from(WITNESS_SCRIPT, 'hex')).toString('hex'),
        'hex'),
      value: 12e2
    },
    witnessScript: Buffer.from(WITNESS_SCRIPT, 'hex')
  })
Set the transaction output.
if (!IS_REFUND) {
  // Happy case: swap provider redeems the funds to his address.
  psbt
    .addOutput({
      address: p2wpkhSwapProvider.address,
      value: 1e3,
    })
} else {
  // Refund case: the user redeems the funds to his address
  psbt
    .addOutput({
      address: p2wpkhUser.address,
      value: 1e3,
    })
}

The bitcoin user alice_1 has paid the swap contract 1200 satoshis and the redeemer is only taking 1000 satoshis.
We leave 200 satoshis in mining fees.

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.

Signing
if (!IS_REFUND) {
// Only in happy case
  psbt.signInput(0, keyPairSwapProvider)
} else {
  // Only in refund case
  psbt.signInput(0, keyPairUser)
}
Finalize the PSBT.
const getFinalScripts = (inputIndex, input, script) => {
  // Step 1: Check to make sure the meaningful locking script matches what you expect.
  const decompiled = bitcoin.script.decompile(script)
  if (!decompiled || decompiled[0] !== bitcoin.opcodes.OP_HASH160) {
    throw new Error(`Can not finalize input #${inputIndex}`)
  }

  // Step 2: Create final scripts
  if (!IS_REFUND) {
    // 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([
          input.partialSig[0].signature,
          Buffer.from(PREIMAGE, 'hex'),
        ]),
        output: Buffer.from(WITNESS_SCRIPT, 'hex')
      }
    })
    console.log('First branch witness stack:')
    console.log(witnessStackClaimBranch.witness.map(x => x.toString('hex')))

    return {
      finalScriptWitness: witnessStackToScriptWitness(witnessStackClaimBranch.witness)
    }
  } else {
    // 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([
          input.partialSig[0].signature,
          Buffer.from('', 'hex'),
        ]),
        output: Buffer.from(WITNESS_SCRIPT, 'hex')
      }
    })
    console.log('Second branch witness stack:')
    console.log(witnessStackRefundBranch.witness.map(x => x.toString('hex')))

    return {
      finalScriptWitness: witnessStackToScriptWitness(witnessStackRefundBranch.witness)
    }
  }
}

psbt.finalizeInput(0, getFinalScripts)
Preimage’s HASH160
console.log('Preimage\'s HASH160')
console.log(bitcoin.crypto.hash160(Buffer.from(PREIMAGE, 'hex')).toString('hex'))
console.log()
Print the redeem transaction.
console.log('Transaction hexadecimal:')
console.log(psbt.extractTransaction().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