Skip to content

Prevent swap-out prepayment loss on opening tx failure#452

Draft
YusukeShimizu wants to merge 7 commits into
ElementsProject:masterfrom
YusukeShimizu:swap-out-opening-tx-precheck
Draft

Prevent swap-out prepayment loss on opening tx failure#452
YusukeShimizu wants to merge 7 commits into
ElementsProject:masterfrom
YusukeShimizu:swap-out-opening-tx-precheck

Conversation

@YusukeShimizu

@YusukeShimizu YusukeShimizu commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #324

In a swap out, the taker pays the fee invoice (prepayment) before the maker constructs the opening transaction. If the maker's wallet cannot fund or sign the transaction at that point (locked wallet, insufficient funds to fund PSBT, unspendable coins), the swap is canceled after the prepayment was already paid, and the taker loses it.

Maker side: dry-run precheck before sending the fee invoice

When a swap-out request is received, the maker now funds and signs a throwaway opening transaction — without broadcasting it — before creating the fee invoice. If this fails, the swap is canceled before the taker pays anything. Implemented for all wallet backends:

  • LND: FundPsbt + FinalizePsbt; utxo leases are released afterwards
  • CLN: txprepare + txdiscard
  • Elements: fundrawtransaction + blind + sign (utxos are not locked)
  • LWK: build + sign the PSET, skip the broadcast

testmempoolaccept (the second proposal in the issue) was left out: LND and CLN expose no equivalent API, and the fund+sign dry run already catches the failure classes reported in the issue.

Taker side: flag peers after a prepayment loss

If a peer cancels after the fee invoice was paid but before the opening transaction was broadcast — the exact loss scenario of the issue — the peer is added to the suspicious peer list. Since outgoing swaps to suspicious peers are already refused, an automated setup loses at most one prepayment per misbehaving peer instead of one per attempt. removesuspiciouspeer undoes the entry.

A peerswap maker only checks its flat onchain balance before sending
the fee invoice for a swap out. A locked wallet or unspendable coins
only surface when the opening transaction is funded, after the taker
has already paid the prepayment (issue ElementsProject#324).

Add PrecheckTransaction to the liquid wallet interface. The Elements
implementation funds, blinds and signs a throwaway transaction
without broadcasting it; fundrawtransaction does not lock utxos, so
nothing needs to be released. The LWK implementation builds and signs
the PSET and skips the broadcast. The tx template construction and
the fund-and-finalize steps are extracted into helpers shared with
the broadcast path.
The swap layer needs a wallet-level dry run of the opening
transaction so a maker can detect wallet problems before the taker
pays the fee invoice (issue ElementsProject#324).

Add PrecheckOpeningTransaction to LiquidOnChain, which derives the
confidential opening address and delegates to the liquid wallet's
PrecheckTransaction. The address derivation is extracted into a
helper shared with CreateOpeningTransaction.
A maker running lnd only notices funding problems such as
insufficient spendable coins or a failing signer when the opening
transaction is created, after the taker has already paid the fee
invoice (issue ElementsProject#324).

Add PrecheckOpeningTransaction, which funds and finalizes the same
psbt as the real opening transaction but never publishes it. Utxos
leased by FundPsbt are released again afterwards; a failed release is
only logged since the lease expires on its own.
A maker running cln only notices funding problems such as
insufficient spendable coins when the opening transaction is
prepared, after the taker has already paid the fee invoice
(issue ElementsProject#324).

Add PrecheckOpeningTransaction, which runs txprepare with the same
opening address and amount as the real opening transaction and then
releases the input reservation with txdiscard. A failed discard is
only logged since the reservation expires on its own.
A swap-out maker sends the fee invoice after checking only its flat
onchain balance. When its wallet is locked or coin selection fails,
the opening transaction fails after the taker paid the prepayment,
and the taker loses that payment (issue ElementsProject#324).

Require PrecheckOpeningTransaction from the onchain wallet and run it
in CreateSwapOutFromRequestAction before the fee invoice is created.
The dry run funds and signs a throwaway opening transaction with a
placeholder payment hash, since the claim preimage does not exist
yet; on failure the swap is canceled before the taker pays anything.
When a swap-out peer cancels after the fee invoice was paid but
before the opening transaction is broadcast, the sender loses the
prepayment. Repeated swap attempts against such a peer, e.g. from an
automated job, lose the prepayment on every attempt (issue ElementsProject#324).

Wrap the swap-out sender's canceled state with an action that adds
the peer to the suspicious peer list iff the prepayment was paid, no
opening transaction was received, and the cancel came from the peer.
Outgoing swaps to suspicious peers are already refused, so at most
one prepayment is lost per peer; removesuspiciouspeer undoes the
entry.
The opening transaction precheck on the maker side only had unit
coverage with a stubbed wallet. A regression in the real wallet
integration (coin selection, input release) would go unnoticed by the
existing e2e suites, which always run with spendable maker funds.

Add cln-cln and lnd-lnd swap-out tests that drain the maker's coins
into an unconfirmed change output, so the flat balance check passes
while funding the opening transaction fails. They assert that both
sides cancel with the precheck reason and that the taker never paid
the fee invoice. The tests are wired into the misc_2 CI shard.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fees get paid even opening tx for swap fails

1 participant