The PeerSwap protocol describes on-chain to off-chain atomic swaps between direct Lightning peers. A swap consists of two on-chain transactions: a opening transaction and a spending transaction. The opening transaction can be spent by revealing a payment preimage, received by paying a Lightning invoice. The primary use case of PeerSwap is efficient balancing of Lightning channels.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
The protocol_version
is included to allow for possible changes in the future. The protocol_version
of this document is 1
.
PeerSwap utilizes custom messages as described in BOLT#1. The types are in range 42069
-42085
. The payload
is JSON encoded.
- Both nodes MUST ignore unexpected Messages.
- During a swap the involved peers MUST ensure, that there is only one active swap per channel.
- Swaps are identified by a unique
swap_id
that MUST be mapped to the peerspubkey
and MUST be checked on every message.
Currently PeerSwap supports atomic swaps via the following chains, both main and testnets:
- Bitcoin
- Liquid Network
-
- The peer requesting a swap.
-
- The peer participating in a swap requested by the Initiator.
-
- The peer that makes the on-chain commitment in the swap.
-
- The peer that pays the Lightning invoice in the swap.
-
- A swap where the initiator is the maker, shifting Lightning balance towards itself.
-
- A swap where the initiator is the taker, shifting Lightning balance towards the Responder.
Before Alice After
+-------+-------------------------------+ Swap In +-------------------+-------------------+
| 0.2 | 0.8 | -------> | 0.5 | 0.5 |
+-------+-------------------------------+ +-------------------+-------------------+
Alice Bob Alice Bob
0.5 btc 0.5 btc 0.2 btc 0.8 btc
on-chain on-chain on-chain on-chain
The aim of a Swap In
is to shift the channel balance towards the initiators side. Therefore, the initiator asks its channel peer for a swap on the desired chain. If the peer agrees, the initiator publishes the opening transaction
to the mempool and sends a Lightning invoice to the peer. Once the transaction has received enough confirmations, the responder pays the invoice and publishes the claim transaction
, revealing the preimage of the invoice to claim the transaction output of the opening transaction.
A swap in is initiated by the initiator (Alice) sending a swap_in_request
message to the channel peer (Bob). Bob checks if the request is valid and if he can perform the swap. Bob then responds with the swap_in_agreement
message. Alice then broadcasts the opening_transaction
and sends the opening_tx_broadcasted
message with an invoice back to Bob. After the opening_transaction
is confirmed, Bob pays the invoice and can then broadcast the claim_transaction
with the preimage
to send the on-chain funds of the swap to an address of his desire.
This phase of the swap is used to agree on the swap conditions.
type
: 42069payload
json encoded:
{
protocol_version: uint64,
swap_id: string,
asset: string,
network: string,
scid: string,
amount: uint64,
pubkey: string
}
protocol_version
is the version of the PeerSwap peer protocol the sending node uses.
swap_id
is a randomly generated 32 byte string that must be kept the same through the whole process of a swap and serves as an identifier for a specific swap.
asset
is the desired on-chain asset to use. This can be:
- Bitcoin: The field is left blank.
- Liquid: The asset id of the networks Bitcoin asset.
network
is the desired on-chain network to use. This can be:
- Bitcoin:
mainnet
,testnet
,signet
,regtest
- Liquid: The field is left blank as the asset id also identifies the network.
scid
is the short channel id in human readable format, defined by BOLT#7 with x
as separator, i.e. 539268x845x1
.
amount
is the amount in Sats
that is asked for.
pubkey
is a 33 byte compressed public key generated by the swap initiator. It is used for the spending paths in the opening_transaction
.
The sending node (swap maker/initiator):
- MUST set the
protocol_version
to the version the implementation it is using. - MUST ensure the
swap_id
is unique from any other swap the node has participated in. - If requesting a swap using the Liquid chain:
- MUST leave the
network
field blank. - MUST set the
asset
field to the asset id of the networks Bitcoin asset.
- MUST leave the
- otherwise requesting a swap using the Bitcoin chain:
- MUST set the
network
field to the network the bitcoin node operates on, one ofmainnet
,testnet
,regtest
. - MUST leave the
asset
field blank.
- MUST set the
- MUST set
amount
greater than 0 and smaller than or equal to the channel size. - SHOULD ensure that it can spend the asked
amount
on the desirednetwork
andasset
from the on-chain wallet. - MUST set the
scid
in desired format for an existing channel between the peers. - SHOULD use a fresh random private key to generate the
pubkey
per swap request. - MUST set a 33 byte sized
pubkey
for the receiving node to build the swap bitcoin script in order to verify the broadcastedopening transaction
. - SHOULD fail the swap after a reasonable time without receiving an answer.
The receiving node (swap taker/responder):
- MUST fail the swap on an incompatible
protocol_version
. - MUST fail_the_swap if the
swap_id
is already used - MUST fail_the_swap if both or neither
asset
andnetwork
are set. - if
asset
is set:- MUST fail the swap if it does not support the asked
asset
.
- MUST fail the swap if it does not support the asked
- if
network
is set:- MUST fail the swap if it does not support the asked
network
.
- MUST fail the swap if it does not support the asked
- MUST fail the swap if the
amount
exceeds channel size. - MUST fail the swap if the channel with
scid
does not exist to the peer. - MUST keep the
swap_in_request
message field values for later use.
type
: 42073payload
json encoded:
{
protocol_version: uint64,
swap_id: string,
pubkey: string,
premium: uint64
}
protocol_version
is the version of the PeerSwap peer protocol the sending node uses.
swap_id
is the unique identifier of the swap.
pubkey
is a 33 byte compressed public key generated by the swap responder. It is used for the spending paths in the opening_transaction
.
premium
is a compensation in Sats that the swap partner wants to be payed in order to participate in the swap.
The sending node (swap taker/responder):
- MUST set the swap_id to the
swap_id
received from theswap_in_request
message. - MUST set a 33 byte sized
pubkey
for the maker node to build the bitcoin script for theopening transaction
. - SHOULD use a fresh random private key to generate the
pubkey
. - SHOULD fail the swap after a reasonable time without receiving an answer.
- SHOULD set
premium
to the desired compensation in Sats.
The receiving node (swap maker/initiator):
- MUST fail the swap on an incompatible protocol_version.
- MUST ignore the message if the
swap id
is unknown. - MUST keep the
pubkey
for later use in the case of a failing swap. - if the
premium
exceeds its expectations:- MUST fail_the_swap
- otherwise:
- MUST add the
premium
to the on-chain amount of theopening_transaction
.
- MUST add the
The next steps are the same for both kind of swaps and are layed out under Doing the Swap.
Before Alice After
+-------------------------------+-------+ Swap Out +-------------------+-------------------+
| 0.8 | 0.2 | -------> | 0.5 | 0.5 |
+-------------------------------+-------+ +-------------------+-------------------+
Alice Bob Alice Bob
0.5 btc 0.5 btc 0.8 btc 0.2 btc
on-chain on-chain on-chain on-chain
The aim of a Swap Out
is to shift the channel balance towards the responders side. Therefore, the initiator asks its channel peer for a swap on the desired chain. If the responder agrees, it responds with a fee invoice to cover the on-chain fees for claiming the on-chain funds, in case the initiator does not pay the later swap invoice. When the initiator pays the fee invoice, the responder broadcasts the opening transaction
to the mempool and sends a Lightning invoice to the initiator. Once the transaction has enough confirmations, the initiator pays the invoice and creates the claim transaction
with the preimage.
This phase of the swap is used to agree on the swap conditions. It involves one more invoice than the swap in negotiation, to compensate for the on-chain fees the swap responder and maker in this case has to account for.
type
: 42071payload
json encoded:
{
protocol_version: uint64,
swap_id: string,
asset: string,
network: string,
scid: string,
amount: uint64,
pubkey: string
}
protocol_version
is the version of the PeerSwap peer protocol the sending node uses.
swap_id
is a randomly generated 32 byte string, that must be kept the same through the whole process of a swap and serves as an identifier for a specific swap.
asset
is the desired on-chain asset to use. This can be:
- Bitcoin: The field is left blank.
- Liquid: The bitcoin asset hash of the desired network.
network
is the desired on-chain network to use. This can be:
- Bitcoin:
mainnet
,testnet
,signet
,regtest
- Liquid: The field is left blank as the asset id also defines the network.
scid
is the short channel id in human readable format, defined by BOLT#7 with x
as separator, i.e. 539268x845x1
.
amount
is the amount in satoshi
that is asked for.
pubkey
is a 33 byte compressed public key generated by the initiator. It is used for the spending paths in the opening_transaction
.
The sending node (swap taker/initiator):
- MUST set the
protocol_version
to the version the implementation it is using. - MUST ensure the
swap_id
is unique from any other swap the node has participated in. - If requesting a swap using the Liquid chain:
- MUST leave the
network
field blank. - MUST set the
asset
field to the asset id of the networks Bitcoin asset.
- MUST leave the
- otherwise requesting a swap using the Bitcoin chain:
- MUST set the
network
field to the network the bitcoin node operates on, one ofmainnet
,testnet
,regtest
,signet
. - MUST leave the
asset
field blank.
- MUST set the
- MUST set
amount
greater than 0 and smaller than or equal to the capacity of the channel. - MUST set the
scid
in desired format for an existing channel between the peers. - SHOULD use a fresh random private key to generate the
pubkey
per swap request. - MUST set a 33 byte sized compressed
pubkey
for the receiving node to build the swap bitcoin script in order to verify the broadcastedopening transaction
. - SHOULD fail the swap after a reasonable time without receiving an answer.
The receiving node (swap responder):
- MUST fail the swap on an incompatible
protocol_version
. - MUST fail_the_swap if the
swap_id
is already used. - MUST fail_the_swap if both or neither
asset
andnetwork
are set. - if
asset
is set:- MUST fail the swap if it does not support the asked
asset
.
- MUST fail the swap if it does not support the asked
- if
network
is set:- MUST fail the swap if it does not support the asked
network
.
- MUST fail the swap if it does not support the asked
- MUST fail the swap if the
amount
exceeds channel size. - MUST ensure that it can dispose the asked
amount
on the desirednetwork
andasset
. - MUST fail the swap if the channel with
scid
does not exist to the peer. - MUST keep the
swap_out_request
message field values for later use.
type
: 42075payload
json encoded:
{
protocol_version: uint64,
swap_id: string,
pubkey: string,
payreq: string,
}
protocol_version
is the version of the PeerSwap peer protocol the sending node uses.
swap_id
is the unique identifier of the swap.
pubkey
is a 33 byte compressed public key generated by the responder. It is used by the initiator to build the bitcoin script in order to verify the opening_transaction
.
payreq
is a BOLT#11 invoice with an amount that covers the fee expenses for the on-chain transactions.
The sending node (swap maker/responder):
- MUST set the
protocol_version
to the version the implementation is using. - MUST set the swap_id to the
swap_id
received from theswap_out_request
message. - SHOULD use a fresh random private key to generate the
pubkey
. - MUST set a 33 byte sized
pubkey
for the taker node to build the swap bitcoin script for verification of theopening transaction
. - MUST set
payreq
to a valid BOLT#11 invoice - SHOULD set the
amount
of the invoice to the fee of the to be createdopening_transaction
and MAY add a premium for a possible refund transaction. - SHOULD resend the message periodically until one of the following is true:
- fee invoice with
payreq
has been paid. - fee invoice with
payreq
expired, in this case MUST fail the swap. - received a
cancel
orcoop_close
message.
- fee invoice with
The receiving node (swap initiator):
- MUST fail the swap on an incompatible
protocol_version
. - MUST ignore the message if the
swap_id
is unknown. - MUST fail the swap if
payreq
is not a valid BOLT#11 invoice; - SHOULD fail the swap if the
amount
asked for in thepayreq
is exceeding own expectations. - MUST fail the swap if the
amount
asked for in thepayreq
added to theamount
asked for in theswap_out_request
exceeds the peers channel balance. - MUST try to pay the fee invoice and fail the swap if this fails.
When the fee invoice was payed, the next steps are the same for both kind of swaps and are layed out under Doing the Swap.
After the peers agreed to the swap conditions, the swap maker
broadcasts the opening_transaction
and sends the opening_tx_broadcasted
message to the swap taker
.
type
: 42077payload
json encoded:
{
swap_id: string,
payreq: string,
tx_id: string,
script_out: uint64,
blinding_key: string,
}
swap_id
is the unique identifier of the swap.
payreq
is the invoice as described in BOLT#11 that the responder is requested to pay.
tx_id
is the transaction id of the opening_transaction
broadcasted by the initiator.
script_out
is the transaction output that contains the opening_transaction
output script for the swap.
blinding_key
:
- Bitcoin: Blank.
- Liquid Network: Is the 32 byte blinding key to un-blind the outputs of the
opening_transaction
.
The sending node (swap maker):
- MUST set
swap_id
matching the ongoing swap. - MUST set
payreq
to a valid BOLT#11 invoice with the amount that was requested inamount
of the swap request and the payment_hash of theopening_transaction
and a reasonableexpiry
. - MUST set
tx_id
to the id of the broadcastedopening_transaction
. - MUST set
script_out
to the output index that contains the script for theopening_transaction
- if
asset
was set in the request:- MUST set the
blinding_key
to the blinding key that is used to blind theopening_transaction
outputs.
- MUST set the
- otherwise:
- MUST NOT set the
blinding_key
.
- MUST NOT set the
- SHOULD fail any htlc that would change the channel into a state, where the swap invoice can not be payed until the swap invoice was payed.
- SHOULD resend the message until one of the following is true:
- swap invoice with
payreq
has been paid. - swap invoice with
payreq
expired, in this case MUST fail the swap. - received a
cancel
orcoop_close
message.
- swap invoice with
- if the invoice with
payreq
gets payed:- MUST consider the swap finished and ignore all future messages with
swap_id
.
- MUST consider the swap finished and ignore all future messages with
- otherwise after the invoice is expired:
- MUST fail the swap.
The receiving node (swap taker):
- MUST fail the swap if the
swap_id
changed. - MUST check that the
payreq
amount matches the negotiatedamount
of the swap and fail the swap if not. - SHOULD fail any htlc that would change the channel into a state, where the swap invoice can not be payed until the swap invoice was payed.
- MUST wait for the opening_transaction with
tx_id
to be confirmed. - MUST verify the opening_transaction output with index
script_out
:- if
asset
was set in the request:- MUST un-blind the amount with the
blinding_key
- MUST un-blind the amount with the
- MUST check that the amount matches the negotiated
amount
of the swap. - MUST check that the script is built as described by opening_transaction.
- if
- if this fails:
- MUST fail the swap.
- otherwise:
- MUST pay the swap invoice over the channel with the negotiated
scid
.- if this fails:
- SHOULD retry to pay the invoice, but MUST NOT pay the invoice if current block height exceeds
CONFIRMATION_HEIGHT + R/2
, whereCONFIRMATION_HEIGHT
is the height of the block at which theopening_transaction
counts as confirmed, andR
is the relative locktime of theclaim_by_csv
path of theopening_transaction
. - if this fails:
- MUST fail the swap.
- SHOULD retry to pay the invoice, but MUST NOT pay the invoice if current block height exceeds
- otherwise:
- MUST broadcast the
claim_by_preimage
claim_transaction
with a fee high enough to ensure that the transaction is confirmed before theclaim_by_csv
spending path is possible. - MUST consider the swap finished after the
claim_transaction
has been confirmed.
- MUST broadcast the
- if this fails:
- MUST pay the swap invoice over the channel with the negotiated
When a node cancels a swap, the most effective way should be used to avoid unnecessary long on-chain locks for the swap partner. This means that if the swap fails after the opening_transaction
was broadcasted (e.g. because the invoice can not be payed), the swap taker should be cooperative and fail the swap via the coop_close
message.
Both nodes MUST expect cancel
and coop_close
at any time during an active swap.
type
: 42079payload
json encoded:
{
swap_id: string,
message: string,
}
swap_id
is the unique identifier of the swap.
message
is a hint to why the swap was canceled.
The sending node:
- MUST set
swap_id
matching the ongoing swap. - SHOULD set a meaningful
message
. - MUST consider the swap canceled and ignore all future messages with
swap_id
. - if it is the
swap maker
and theopening_transaction
was already broadcasted:- MUST broadcast the
claim_transaction
, with theclaim_by_csv
spending path, after the CSV has passed. - MUST allow for new swaps on the channel as soon as the
claim_transaction
is confirmed.
- MUST broadcast the
- otherwise:
- MUST allow for new swaps on the channel.
The receiving node:
- MUST consider the swap canceled and ignore all future messages with
swap_id
. - if it is the
swap maker
and theopening_transaction
was already broadcasted:- MUST broadcast the
claim_transaction
, with theclaim_by_csv
spending path, after the CSV has passed. - MUST allow for new swaps on the channel as soon as the
claim_transaction
is confirmed.
- MUST broadcast the
- otherwise:
- MUST allow for new swaps on the channel.
type
: 42081payload
json encoded:
{
swap_id: string,
message: string,
privkey: string,
}
swap_id
is the unique identifier of the swap.
message
is a hint to why the swap was closed cooperatively.
privkey
is the private key to the pubkey that is used to build the opening_transaction
.
The sending node (swap taker):
- MUST set
swap_id
matching the ongoing swap. - SHOULD set a meaningful
message
. - if the node is the initiator:
- MUST set
privkey
to the random private key that was used to generate the pubkey that was set in the request message.
- MUST set
- otherwise:
- MUST set
privkey
to the random private key that was used to generate the pubkey that was set in the agreement message.
- MUST set
The receiving node (swap maker):
- if the
opening_transaction
was already broadcasted:- MUST consider the swap canceled and ignore all future messages with
swap_id
. - MUST broadcast the
claim_transaction
with theclaim_by_coop
spending path using theprivkey
. - if this fails:
- MUST broadcast the
claim_transaction
with theclaim_by_csv
spending path, after the CSV has passed.
- MUST broadcast the
- MUST consider the swap canceled and ignore all future messages with
- otherwise:
- MUST consider this to be a
cancel
message.
- MUST consider this to be a
Timings are critical to the PeerSwap protocol. The goal is to provide a safe swap while maintaining a reasonable time frame. The timings differ for the supported networks.
In the case of Bitcoin:
- CSV = 1008
- Min required confirmations for the
opening_transaction
= 3
In The case of Liquid:
- CSV = 60
- Min required confirmations for the
opening_transaction
= 2
The difference in timings is due to the different fee and consensus models of the Liquid Network and Bitcoin.
The expiry of the swap invoice
MUST be less than or equal to half the CSV time to ensure a secure swap.
The opening transaction has a pay-to-witness-script-hashBIP141 (P2WSH) output that locks the on-chain part of the swap. This script has three different spending paths. These are the claim_by_invoice
, claim_by_csv
and the claim_by_coop
paths. The transaction maker may use inputs to his desire.
If this is a Liquid transaction, it MUST be blinded.
- The opening transaction policy:
or(and(pk(A),or(pk(B),sha256(H))),and(pk(B),older(144)))
- The opening transaction output script is a P2WSH:
<B> OP_CHECKSIG OP_NOTIF
<B> OP_CHECKSIG OP_NOTIF
OP_SIZE <20> OP_EQUALVERIFY OP_SHA256 <H> OP_EQUALVERIFY
OP_ENDIF
<A> OP_CHECKSIG
OP_ELSE
<N> OP_CHECKSEQUENCEVERIFY
OP_ENDIF
With:
<A>
the pubkey of thetaker
<B>
the pubkey of themaker
<H>
the payment_hash<N>
the number of confirmations before the refund to the maker is possible. See CSV Times
The claim transaction finishes the atomic swap. There are three different variants for the claiming transaction, depending on how the swap finishes.
This is the desired way to finish a swap. The taker sends the funds to its address by revealing the preimage of the swap invoice.
- txin count: 1
- txin[0] outpoint:
tx_id
andscript_output
from theopening_tx_broadcasted
message - txin[0] sequence: 0
- txin[0] script bytes: 0
- txin[0] witness:
<signature_for_A> <preimage> <> <> <redeem_script>
- txin[0] outpoint:
This is the way to cooperatively finish a swap. The maker refunds to its address without waiting for the CSV.
- txin count: 1
- txin[0] outpoint:
tx_id
andscript_output
from theopening_tx_broadcasted
message - txin[0] sequence: 0
- txin[0] script bytes: 0
- txin[0] witness:
<signature_for_A> <signature_for_B> <> <redeem_script>
- txin[0] outpoint:
This is the way to finish a swap if the invoice was not paid and the taker did not send a coop_close
message. After the relative locktime has passed, the maker refunds to them.
- txin count: 1
- txin[0] outpoint:
tx_id
andscript_output
from theopening_tx_broadcasted
message - txin[0] sequence:
- for
btc
as asset: 0x3F0 corresponding to the CSV of 1008 - for
lbtc
as asset: 0x3C corresponding to the CSV of 60
- for
- txin[0] script bytes: 0
- txin[0] witness:
<signature_for_B> <redeem_script>
- txin[0] outpoint: