1
0

swaps: add sanity check to reverse swap mining fee

This commit is contained in:
f321x
2025-08-20 15:10:04 +02:00
parent a29125f5c5
commit 71c71a96f3
4 changed files with 29 additions and 8 deletions

View File

@@ -1975,7 +1975,7 @@ class Commands(Logger):
arg:decimal_or_dryrun:onchain_amount:Amount to be sent, in BTC. Set it to 'dryrun' to receive a value
"""
sm = wallet.lnworker.swap_manager
with sm.create_transport() as transport:
async with sm.create_transport() as transport:
await sm.is_initialized.wait()
if lightning_amount == 'dryrun':
onchain_amount_sat = satoshis(onchain_amount)
@@ -2002,37 +2002,46 @@ class Commands(Logger):
}
@command('wnpl')
async def reverse_swap(self, lightning_amount, onchain_amount, password=None, wallet: Abstract_Wallet = None):
async def reverse_swap(self, lightning_amount, onchain_amount, provider_mining_fee, password=None, wallet: Abstract_Wallet = None):
"""
Reverse submarine swap: send on Lightning, receive on-chain
arg:decimal_or_dryrun:lightning_amount:Amount to be sent, in BTC. Set it to 'dryrun' to receive a value
arg:decimal_or_dryrun:onchain_amount:Amount to be received, in BTC. Set it to 'dryrun' to receive a value
arg:decimal_or_dryrun:provider_mining_fee:Mining fee required by the swap provider, in BTC. Set it to 'dryrun' to receive a value
"""
sm = wallet.lnworker.swap_manager
with sm.create_transport() as transport:
async with sm.create_transport() as transport:
await sm.is_initialized.wait()
if onchain_amount == 'dryrun':
lightning_amount_sat = satoshis(lightning_amount)
onchain_amount_sat = sm.get_recv_amount(lightning_amount_sat, is_reverse=True)
assert provider_mining_fee == "dryrun", f"Cannot use {provider_mining_fee=} in dryrun. Set it to 'dryrun'."
provider_mining_fee = sm.mining_fee
funding_txid = None
elif lightning_amount == 'dryrun':
onchain_amount_sat = satoshis(onchain_amount)
lightning_amount_sat = sm.get_send_amount(onchain_amount_sat, is_reverse=True)
assert provider_mining_fee == "dryrun", f"Cannot use {provider_mining_fee=} in dryrun. Set it to 'dryrun'."
provider_mining_fee = sm.mining_fee
funding_txid = None
else:
lightning_amount_sat = satoshis(lightning_amount)
claim_fee = sm.get_fee_for_txbatcher()
onchain_amount_sat = satoshis(onchain_amount) + claim_fee
assert provider_mining_fee != "dryrun", "Provide the 'provder_mining_fee' obtained from the dryrun."
provider_mining_fee = satoshis(provider_mining_fee)
funding_txid = await wallet.lnworker.swap_manager.reverse_swap(
transport=transport,
lightning_amount_sat=lightning_amount_sat,
expected_onchain_amount_sat=onchain_amount_sat,
server_mining_fee_sat=provider_mining_fee,
)
return {
'funding_txid': funding_txid,
'lightning_amount': format_satoshis(lightning_amount_sat),
'onchain_amount': format_satoshis(onchain_amount_sat),
'provider_mining_fee': format_satoshis(provider_mining_fee)
}
@command('n')

View File

@@ -719,6 +719,7 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
transport=self.swap_transport,
lightning_amount_sat=lightning_amount,
expected_onchain_amount_sat=onchain_amount + swap_manager.get_fee_for_txbatcher(),
server_mining_fee_sat=self.serverMiningfee.satsInt,
)
try: # swaphelper might be destroyed at this point
if txid:

View File

@@ -94,6 +94,7 @@ class SwapDialog(WindowModalDialog, QtEventListener):
self.swap_limits_label = QLabel()
self.fee_label = QLabel()
self.server_fee_label = QLabel()
self.last_server_mining_fee_sat = None
h = QGridLayout()
h.addWidget(self.description_label, 0, 0, 1, 3)
h.addWidget(self.toggle_button, 0, 3)
@@ -153,7 +154,12 @@ class SwapDialog(WindowModalDialog, QtEventListener):
@qt_event_listener
def on_event_swap_offers_changed(self, recent_offers: Sequence['SwapOffer']):
self.set_server_button_text(len(recent_offers))
self.update()
if not self.ok_button.isEnabled():
# only update the dialog with the new offer if the user hasn't entered an amount yet.
# if the user has already entered an amount we prefer the swap to fail due to outdated
# fees than the possibility of a swap happening with fees the user hasn't seen
# due to an update happening just before the user initiated the swap
self.update()
def set_server_button_text(self, offer_count: int):
button_text = f' {offer_count} ' + (_('providers') if offer_count != 1 else _('provider'))
@@ -282,8 +288,8 @@ class SwapDialog(WindowModalDialog, QtEventListener):
f"{self.window.format_amount(max_swap_limit)} {w_base_unit}")
self.swap_limits_label.setText(swap_limit_str)
self.swap_limits_label.repaint() # macOS hack for #6269
server_mining_fee = sm.mining_fee
server_fee_str = '%.2f'%sm.percentage + '% + ' + self.window.format_amount(server_mining_fee) + ' ' + w_base_unit
self.last_server_mining_fee_sat = sm.mining_fee
server_fee_str = '%.2f'%sm.percentage + '% + ' + self.window.format_amount(sm.mining_fee) + ' ' + w_base_unit
self.server_fee_label.setText(server_fee_str)
self.server_fee_label.repaint() # macOS hack for #6269
self.needs_tx_update = True
@@ -332,6 +338,7 @@ class SwapDialog(WindowModalDialog, QtEventListener):
transport=transport,
lightning_amount_sat=lightning_amount,
expected_onchain_amount_sat=onchain_amount + self.swap_manager.get_fee_for_txbatcher(),
server_mining_fee_sat=self.last_server_mining_fee_sat,
)
try:
# we must not leave the context, so we use run_couroutine_dialog

View File

@@ -904,6 +904,7 @@ class SwapManager(Logger):
transport: 'SwapServerTransport',
lightning_amount_sat: int,
expected_onchain_amount_sat: int,
server_mining_fee_sat: int,
channels: Optional[Sequence['Channel']] = None,
) -> Optional[str]:
"""send on Lightning, receive on-chain
@@ -920,6 +921,9 @@ class SwapManager(Logger):
- Server fulfills HTLC using preimage.
Note: expected_onchain_amount_sat is BEFORE deducting the on-chain claim tx fee.
Note: server_mining_fee_sat is passed as argument instead of accessing self.mining_fee to ensure
the mining fees the user sees in the GUI are also the values used for the checks performed here.
We commit to server_mining_fee_sat as it limits the max fee pre-payment amt, which the server is trusted with.
"""
assert self.network
assert self.lnwatcher
@@ -977,16 +981,16 @@ class SwapManager(Logger):
invoice_amount = int(lnaddr.get_amount_sat())
if lnaddr.paymenthash != payment_hash:
raise Exception("rswap check failed: inconsistent RHASH and invoice")
# check that the lightning amount is what we requested
if fee_invoice:
fee_lnaddr = self.lnworker._check_bolt11_invoice(fee_invoice)
if fee_lnaddr.get_amount_sat() > self.mining_fee * 2:
if fee_lnaddr.get_amount_sat() > server_mining_fee_sat * 2:
raise SwapServerError(_("Mining fee requested by swap-server larger "
"than what was announced in their offer."))
invoice_amount += fee_lnaddr.get_amount_sat()
prepay_hash = fee_lnaddr.paymenthash
else:
prepay_hash = None
# check that the lightning amount is what we requested
if int(invoice_amount) != lightning_amount_sat:
raise Exception(f"rswap check failed: invoice_amount ({invoice_amount}) "
f"not what we requested ({lightning_amount_sat})")