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.
- Peer Protocol for PeerSwap Swaps
The protocol_version is included to allow for possible changes in the future. The protocol_version of this document is 5.
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_idthat MUST be mapped to the peerspubkeyand 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: 42069payloadjson 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_versionto the version the implementation it is using. - MUST ensure the
swap_idis unique from any other swap the node has participated in. - If requesting a swap using the Liquid chain:
- MUST leave the
networkfield blank. - MUST set the
assetfield to the asset id of the networks Bitcoin asset.
- MUST leave the
- otherwise requesting a swap using the Bitcoin chain:
- MUST set the
networkfield to the network the bitcoin node operates on, one ofmainnet,testnet,regtest. - MUST leave the
assetfield blank.
- MUST set the
- MUST set
amountgreater than 0 and smaller than or equal to the channel size. - SHOULD ensure that it can spend the asked
amounton the desirednetworkandassetfrom the on-chain wallet. - MUST set the
scidin desired format for an existing channel between the peers. - SHOULD use a fresh random private key to generate the
pubkeyper swap request. - MUST set a 33 byte sized
pubkeyfor 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_idis already used - MUST fail_the_swap if both or neither
assetandnetworkare set. - if
assetis 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
networkis 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
amountexceeds channel size. - MUST fail the swap if the channel with
sciddoes not exist to the peer. - MUST keep the
swap_in_requestmessage field values for later use.
type: 42073payloadjson encoded:
{
protocol_version: uint64,
swap_id: string,
pubkey: string,
premium: int64
}
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 paid in order to participate in the swap.
The sending node (swap taker/responder):
- MUST set the swap_id to the
swap_idreceived from theswap_in_requestmessage. - MUST set a 33 byte sized
pubkeyfor 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
premiumto 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 idis unknown. - MUST keep the
pubkeyfor later use in the case of a failing swap. - if the
premiumexceeds its expectations:- MUST fail_the_swap
- otherwise:
- MUST add the
premiumto the on-chain amount of theopening_transaction.
- MUST add the
The next steps are the same for both kind of swaps and are laid 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: 42071payloadjson 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_versionto the version the implementation it is using. - MUST ensure the
swap_idis unique from any other swap the node has participated in. - If requesting a swap using the Liquid chain:
- MUST leave the
networkfield blank. - MUST set the
assetfield to the asset id of the networks Bitcoin asset.
- MUST leave the
- otherwise requesting a swap using the Bitcoin chain:
- MUST set the
networkfield to the network the bitcoin node operates on, one ofmainnet,testnet,regtest,signet. - MUST leave the
assetfield blank.
- MUST set the
- MUST set
amountgreater than 0 and smaller than or equal to the capacity of the channel. - MUST set the
scidin desired format for an existing channel between the peers. - SHOULD use a fresh random private key to generate the
pubkeyper swap request. - MUST set a 33 byte sized compressed
pubkeyfor 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_idis already used. - MUST fail_the_swap if both or neither
assetandnetworkare set. - if
assetis 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
networkis 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
amountexceeds channel size. - MUST ensure that it can dispose the asked
amounton the desirednetworkandasset. - MUST fail the swap if the channel with
sciddoes not exist to the peer. - MUST keep the
swap_out_requestmessage field values for later use.
type: 42075payloadjson encoded:
{
protocol_version: uint64,
swap_id: string,
pubkey: string,
payreq: 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 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.
premium is a compensation in Sats that the swap partner wants to be paid in order to participate in the swap.
The sending node (swap maker/responder):
- MUST set the
protocol_versionto the version the implementation is using. - MUST set the swap_id to the
swap_idreceived from theswap_out_requestmessage. - SHOULD use a fresh random private key to generate the
pubkey. - MUST set a 33 byte sized
pubkeyfor the taker node to build the swap bitcoin script for verification of theopening transaction. - MUST set
payreqto a valid BOLT#11 invoice - SHOULD set the
amountof the invoice to the fee of the to be createdopening_transaction. - SHOULD resend the message periodically until one of the following is true:
- fee invoice with
payreqhas been paid. - fee invoice with
payreqexpired, in this case MUST fail the swap. - received a
cancelorcoop_closemessage.
- fee invoice with
- SHOULD set
premiumto the desired compensation in Sats. - if the
premiumis set:- MUST add
premiumon payreq ofopening_tx_broadcastedmessage.
- MUST add
The receiving node (swap initiator):
- MUST fail the swap on an incompatible
protocol_version. - MUST ignore the message if the
swap_idis unknown. - MUST fail the swap if
payreqis not a valid BOLT#11 invoice; - SHOULD fail the swap if the
amountasked for in thepayreqis exceeding own expectations. - MUST fail the swap if the
amountasked for in thepayreqadded to theamountasked for in theswap_out_requestexceeds the peers channel balance. - MUST try to pay the fee invoice and fail the swap if this fails.
- if the
premiumexceeds its expectations:- MUST fail_the_swap
When the fee invoice was paid, the next steps are the same for both kind of swaps and are laid 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: 42077payloadjson 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_idmatching the ongoing swap. - MUST set
payreqto a valid BOLT#11 invoice with the amount that was requested inamountof the swap request and the payment_hash of theopening_transactionand a reasonableexpiry. - MUST set
tx_idto the id of the broadcastedopening_transaction. - MUST set
script_outto the output index that contains the script for theopening_transaction - if
assetwas set in the request:- MUST set the
blinding_keyto the blinding key that is used to blind theopening_transactionoutputs.
- 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 paid until the swap invoice was paid.
- SHOULD resend the message until one of the following is true:
- swap invoice with
payreqhas been paid. - swap invoice with
payreqexpired, in this case MUST fail the swap. - received a
cancelorcoop_closemessage.
- swap invoice with
- if the invoice with
payreqgets paid:- 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_idchanged. - MUST check that the
payreqamount matches the negotiatedamountof 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 paid until the swap invoice was paid.
- MUST wait for the opening_transaction with
tx_idto be confirmed. - MUST verify the opening_transaction output with index
script_out:- if
assetwas 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
amountof 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_HEIGHTis the height of the block at which theopening_transactioncounts as confirmed, andRis the relative locktime of theclaim_by_csvpath 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_preimageclaim_transactionwith a fee high enough to ensure that the transaction is confirmed before theclaim_by_csvspending path is possible. - MUST consider the swap finished after the
claim_transactionhas 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 paid), 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: 42079payloadjson 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_idmatching 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 makerand theopening_transactionwas already broadcasted:- MUST broadcast the
claim_transaction, with theclaim_by_csvspending path, after the CSV has passed. - MUST allow for new swaps on the channel as soon as the
claim_transactionis 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 makerand theopening_transactionwas already broadcasted:- MUST broadcast the
claim_transaction, with theclaim_by_csvspending path, after the CSV has passed. - MUST allow for new swaps on the channel as soon as the
claim_transactionis confirmed.
- MUST broadcast the
- otherwise:
- MUST allow for new swaps on the channel.
type: 42081payloadjson 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_idmatching the ongoing swap. - SHOULD set a meaningful
message. - if the node is the initiator:
- MUST set
privkeyto the random private key that was used to generate the pubkey that was set in the request message.
- MUST set
- otherwise:
- MUST set
privkeyto 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_transactionwas already broadcasted:- MUST consider the swap canceled and ignore all future messages with
swap_id. - MUST broadcast the
claim_transactionwith theclaim_by_coopspending path using theprivkey. - if this fails:
- MUST broadcast the
claim_transactionwith theclaim_by_csvspending 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
cancelmessage.
- 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_idandscript_outputfrom theopening_tx_broadcastedmessage - 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_idandscript_outputfrom theopening_tx_broadcastedmessage - 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_idandscript_outputfrom theopening_tx_broadcastedmessage - txin[0] sequence:
- for
btcas asset: 0x3F0 corresponding to the CSV of 1008 - for
lbtcas 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:

