From 18baf003d48b80576fbcf9bd8994fbe473ae867b Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 14 Feb 2025 14:12:12 +0100 Subject: [PATCH] submarine swaps: backport changes from batch_payment_manager - add 'is_funded' method to SwapData - sign claim transactions using the 'make_witness' method --- electrum/submarine_swaps.py | 82 +++++++++++++++++-------------------- electrum/wallet.py | 7 ---- tests/test_sswaps.py | 6 +-- 3 files changed, 40 insertions(+), 55 deletions(-) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index a24ea11ce..cc81c0e4a 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -161,22 +161,31 @@ class SwapData(StoredObject): def payment_hash(self) -> bytes: return self._payment_hash + def is_funded(self) -> bool: + return self.funding_txid is not None + + def create_claim_tx( *, txin: PartialTxInput, - witness_script: bytes, - address: str, - amount_sat: int, - locktime: int, + swap: SwapData, + config: 'SimpleConfig', ) -> PartialTransaction: """Create tx to either claim successful reverse-swap, or to get refunded for timed-out forward-swap. """ - txin.nsequence = 0xffffffff - 2 - txin.script_sig = b'' - txin.witness_script = witness_script - txout = PartialTxOutput.from_address_and_value(address, amount_sat) + # FIXME the mining fee should depend on swap.is_reverse. + # the txs are not the same size... + amount_sat = txin.value_sats() - SwapManager._get_fee(size=CLAIM_FEE_SIZE, config=config) + if amount_sat < dust_threshold(): + raise BelowDustLimit() + txin, locktime = SwapManager.create_claim_txin(txin=txin, swap=swap, config=config) + txout = PartialTxOutput.from_address_and_value(swap.receive_address, amount_sat) tx = PartialTransaction.from_io([txin], [txout], version=2, locktime=locktime) + sig = tx.sign_txin(0, txin.privkey) + txin.script_sig = b'' + txin.witness = txin.make_witness(sig) + assert tx.is_complete() return tx @@ -316,7 +325,7 @@ class SwapManager(Logger): """ we must not have broadcast the funding tx """ if swap is None: return - if swap.funding_txid is not None: + if swap.is_funded(): self.logger.info(f'cannot cancel swap {swap.payment_hash.hex()}: already funded') return self._fail_swap(swap, 'user cancelled') @@ -330,7 +339,7 @@ class SwapManager(Logger): e = OnionRoutingFailure(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'') self.lnworker.save_forwarding_failure(payment_key.hex(), failure_message=e) self.lnwatcher.remove_callback(swap.lockup_address) - if swap.funding_txid is None: + if not swap.is_funded(): self.swaps.pop(swap.payment_hash.hex()) @log_exceptions @@ -443,7 +452,7 @@ class SwapManager(Logger): if spent_height is not None and not should_bump_fee: return try: - tx = self._create_and_sign_claim_tx(txin=txin, swap=swap, config=self.wallet.config) + tx = create_claim_tx(txin=txin, swap=swap, config=self.wallet.config) except BelowDustLimit: self.logger.info('utxo value below dust threshold') return @@ -485,7 +494,7 @@ class SwapManager(Logger): key = payment_hash.hex() if key in self.swaps: swap = self.swaps[key] - if swap.funding_txid is None: + if not swap.is_funded(): password = self.wallet.get_unlocked_password() for batch_rbf in [False]: # FIXME: tx batching is disabled, because extra logic is needed to handle @@ -795,6 +804,9 @@ class SwapManager(Logger): await asyncio.sleep(0.1) return swap.funding_txid + def create_funding_output(self, swap): + return PartialTxOutput.from_address_and_value(swap.lockup_address, swap.onchain_amount) + def create_funding_tx( self, swap: SwapData, @@ -806,7 +818,7 @@ class SwapManager(Logger): # note: rbf must not decrease payment # this is taken care of in wallet._is_rbf_allowed_to_touch_tx_output if tx is None: - funding_output = PartialTxOutput.from_address_and_value(swap.lockup_address, swap.onchain_amount) + funding_output = self.create_funding_output(swap) tx = self.wallet.create_transaction( outputs=[funding_output], rbf=True, @@ -1089,13 +1101,11 @@ class SwapManager(Logger): def is_lockup_address_for_a_swap(self, addr: str) -> bool: return bool(self._swaps_by_lockup_address.get(addr)) - def add_txin_info(self, txin: PartialTxInput) -> None: + @classmethod + def add_txin_info(cls, swap, txin: PartialTxInput) -> None: """Add some info to a claim txin. note: even without signing, this is useful for tx size estimation. """ - swap = self.get_swap_by_claim_txin(txin) - if not swap: - return preimage = swap.preimage if swap.is_reverse else 0 witness_script = swap.redeem_script txin.script_sig = b'' @@ -1106,45 +1116,27 @@ class SwapManager(Logger): txin.nsequence = 0xffffffff - 2 @classmethod - def sign_tx(cls, tx: PartialTransaction, swap: SwapData) -> None: - preimage = swap.preimage if swap.is_reverse else 0 - witness_script = swap.redeem_script - txin = tx.inputs()[0] - assert len(tx.inputs()) == 1, f"expected 1 input for swap claim tx. found {len(tx.inputs())}" - assert txin.prevout.txid.hex() == swap.funding_txid - txin.script_sig = b'' - txin.witness_script = witness_script - sig = tx.sign_txin(0, swap.privkey) - witness = [sig, preimage, witness_script] - txin.witness = construct_witness(witness) - - @classmethod - def _create_and_sign_claim_tx( + def create_claim_txin( cls, *, txin: PartialTxInput, swap: SwapData, config: 'SimpleConfig', ) -> PartialTransaction: - # FIXME the mining fee should depend on swap.is_reverse. - # the txs are not the same size... - amount_sat = txin.value_sats() - cls._get_fee(size=CLAIM_FEE_SIZE, config=config) - if amount_sat < dust_threshold(): - raise BelowDustLimit() if swap.is_reverse: # successful reverse swap locktime = 0 # preimage will be set in sign_tx else: # timing out forward swap locktime = swap.locktime - tx = create_claim_tx( - txin=txin, - witness_script=swap.redeem_script, - address=swap.receive_address, - amount_sat=amount_sat, - locktime=locktime, - ) - cls.sign_tx(tx, swap) - return tx + cls.add_txin_info(swap, txin) + txin.privkey = swap.privkey + def make_witness(sig): + # preimae not known yet + preimage = swap.preimage if swap.is_reverse else 0 + witness_script = swap.redeem_script + return construct_witness([sig, preimage, witness_script]) + txin.make_witness = make_witness + return txin, locktime def max_amount_forward_swap(self) -> Optional[int]: """ returns None if we cannot swap """ diff --git a/electrum/wallet.py b/electrum/wallet.py index f3c58de14..f733eb301 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2509,8 +2509,6 @@ class Abstract_Wallet(ABC, Logger, EventListener): if not is_mine: is_mine = self._learn_derivation_path_for_address_from_txinout(txin, address) if not is_mine: - if self.lnworker: - self.lnworker.swap_manager.add_txin_info(txin) return txin.script_descriptor = self.get_script_descriptor_for_address(address) txin.is_mine = True @@ -2596,11 +2594,6 @@ class Abstract_Wallet(ABC, Logger, EventListener): return if any(DummyAddress.is_dummy_address(txout.address) for txout in tx.outputs()): raise DummyAddressUsedInTxException("tried to sign tx with dummy address!") - # note: swap signing does not require the password - swap = self.get_swap_by_claim_tx(tx) - if swap: - self.lnworker.swap_manager.sign_tx(tx, swap) - return tx # check if signing is dangerous sh_danger = self.check_sighash(tx) diff --git a/tests/test_sswaps.py b/tests/test_sswaps.py index ff3cd206f..d14eb3e2a 100644 --- a/tests/test_sswaps.py +++ b/tests/test_sswaps.py @@ -1,7 +1,7 @@ from electrum import SimpleConfig from electrum.util import bfh from electrum.transaction import PartialTxInput, TxOutpoint -from electrum.submarine_swaps import SwapManager, SwapData +from electrum.submarine_swaps import SwapData, create_claim_tx from . import ElectrumTestCase @@ -35,7 +35,7 @@ class TestSwapTxs(ElectrumTestCase): prevout=TxOutpoint(txid=bfh(swap_data.funding_txid), out_idx=0), ) txin._trusted_value_sats = swap_data.onchain_amount - tx = SwapManager._create_and_sign_claim_tx( + tx = create_claim_tx( txin=txin, swap=swap_data, config=self.config, @@ -65,7 +65,7 @@ class TestSwapTxs(ElectrumTestCase): prevout=TxOutpoint(txid=bfh(swap_data.funding_txid), out_idx=0), ) txin._trusted_value_sats = swap_data.onchain_amount - tx = SwapManager._create_and_sign_claim_tx( + tx = create_claim_tx( txin=txin, swap=swap_data, config=self.config,