From 1364e7538acf37d1b649d2ca8ef647858540b227 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 19 Mar 2022 09:36:50 +0100 Subject: [PATCH] bump fee of swap claim transactions Note: This adds a new field, spent_txid, to PartialTxOutput --- electrum/address_synchronizer.py | 14 +++++++--- electrum/submarine_swaps.py | 44 ++++++++++++++++++++------------ electrum/transaction.py | 1 + electrum/wallet.py | 15 +++++++++-- 4 files changed, 52 insertions(+), 22 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index 8d213dac5..aaed906a7 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -761,10 +761,9 @@ class AddressSynchronizer(Logger): for tx_hash, height in h: l = self.db.get_txi_addr(tx_hash, address) for txi, v in l: - sent[txi] = height + sent[txi] = tx_hash, height return received, sent - def get_addr_outputs(self, address: str) -> Dict[TxOutpoint, PartialTxInput]: coins, spent = self.get_addr_io(address) out = {} @@ -775,7 +774,13 @@ class AddressSynchronizer(Logger): utxo._trusted_address = address utxo._trusted_value_sats = value utxo.block_height = tx_height - utxo.spent_height = spent.get(prevout_str, None) + if prevout_str in spent: + txid, height = spent[prevout_str] + utxo.spent_txid = txid + utxo.spent_height = height + else: + utxo.spent_txid = None + utxo.spent_height = None out[prevout] = utxo return out @@ -816,7 +821,8 @@ class AddressSynchronizer(Logger): else: u += v if txo in sent: - if sent[txo] > 0: + sent_txid, sent_height = sent[txo] + if sent_height > 0: c -= v else: u -= v diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index dd7d38327..405e2e491 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -99,7 +99,6 @@ def create_claim_tx( txin: PartialTxInput, witness_script: bytes, preimage: Union[bytes, int], # 0 if timing out forward-swap - privkey: bytes, address: str, amount_sat: int, locktime: int, @@ -117,10 +116,7 @@ def create_claim_tx( txin.witness_script = witness_script txout = PartialTxOutput.from_address_and_value(address, amount_sat) tx = PartialTransaction.from_io([txin], [txout], version=2, locktime=locktime) - #tx.set_rbf(True) - sig = bytes.fromhex(tx.sign_txin(0, privkey)) - witness = [sig, preimage, witness_script] - txin.witness = bytes.fromhex(construct_witness(witness)) + tx.set_rbf(True) return tx @@ -169,21 +165,23 @@ class SwapManager(Logger): return current_height = self.network.get_local_height() delta = current_height - swap.locktime - if not swap.is_reverse and delta < 0: - # too early for refund - return txos = self.lnwatcher.get_addr_outputs(swap.lockup_address) for txin in txos.values(): if swap.is_reverse and txin.value_sats() < swap.onchain_amount: self.logger.info('amount too low, we should not reveal the preimage') continue + swap.funding_txid = txin.prevout.txid.hex() spent_height = txin.spent_height if spent_height is not None: + swap.spending_txid = txin.spent_txid if spent_height > 0 and current_height - spent_height > REDEEM_AFTER_DOUBLE_SPENT_DELAY: self.logger.info(f'stop watching swap {swap.lockup_address}') self.lnwatcher.remove_callback(swap.lockup_address) swap.is_redeemed = True continue + if not swap.is_reverse and delta < 0: + # too early for refund + return # FIXME the mining fee should depend on swap.is_reverse. # the txs are not the same size... amount_sat = txin.value_sats() - self.get_claim_fee() @@ -201,17 +199,12 @@ class SwapManager(Logger): txin=txin, witness_script=swap.redeem_script, preimage=preimage, - privkey=swap.privkey, address=address, amount_sat=amount_sat, locktime=locktime, ) + self.sign_tx(tx, swap) await self.network.broadcast_transaction(tx) - # save txid - if swap.is_reverse: - swap.spending_txid = tx.txid() - else: - self.wallet.set_label(tx.txid(), 'Swap refund') def get_claim_fee(self): return self.wallet.config.estimate_fee(136, allow_fallback_to_static_rates=True) @@ -307,7 +300,7 @@ class SwapManager(Logger): dummy_output = PartialTxOutput.from_address_and_value(ln_dummy_address(), expected_onchain_amount_sat) tx.outputs().remove(dummy_output) tx.add_outputs([funding_output]) - tx.set_rbf(False) + tx.set_rbf(True) # rbf must not decrease payment self.wallet.sign_transaction(tx, password) # save swap data in wallet in case we need a refund swap = SwapData( @@ -321,7 +314,7 @@ class SwapManager(Logger): lightning_amount = lightning_amount_sat, is_reverse = False, is_redeemed = False, - funding_txid = tx.txid(), + funding_txid = None, spending_txid = None, ) self.swaps[payment_hash.hex()] = swap @@ -541,3 +534,22 @@ class SwapManager(Logger): if is_reverse and send_amount is not None: send_amount += self.get_claim_fee() return send_amount + + def get_swap_by_tx(self, tx): + # determine if tx is spending from a swap + txin = tx.inputs()[0] + for key, swap in self.swaps.items(): + if txin.prevout.txid.hex() == swap.funding_txid: + return swap + return None + + def sign_tx(self, tx, swap): + preimage = swap.preimage if swap.is_reverse else 0 + witness_script = swap.redeem_script + txin = tx.inputs()[0] + txin.script_type = 'p2wsh' + txin.script_sig = b'' + txin.witness_script = witness_script + sig = bytes.fromhex(tx.sign_txin(0, swap.privkey)) + witness = [sig, preimage, witness_script] + txin.witness = bytes.fromhex(construct_witness(witness)) diff --git a/electrum/transaction.py b/electrum/transaction.py index aa071fbfb..57cb89995 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -1210,6 +1210,7 @@ class PartialTxInput(TxInput, PSBTSection): self._trusted_address = None # type: Optional[str] self.block_height = None # type: Optional[int] # height at which the TXO is mined; None means unknown self.spent_height = None # type: Optional[int] # height at which the TXO got spent + self.spent_txid = None # type: Optional[str] # txid of the spender self._is_p2sh_segwit = None # type: Optional[bool] # None means unknown self._is_native_segwit = None # type: Optional[bool] # None means unknown diff --git a/electrum/wallet.py b/electrum/wallet.py index 5a7d1dd07..f056762fd 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -591,10 +591,14 @@ class Abstract_Wallet(AddressSynchronizer, ABC): return any([chan.funding_outpoint.txid == txid for chan in self.lnworker.channels.values()]) + def is_swap_tx(self, tx: Transaction) -> bool: + return bool(self.lnworker.swap_manager.get_swap_by_tx(tx)) if self.lnworker else False + def get_tx_info(self, tx: Transaction) -> TxWalletDetails: tx_wallet_delta = self.get_wallet_delta(tx) is_relevant = tx_wallet_delta.is_relevant is_any_input_ismine = tx_wallet_delta.is_any_input_ismine + is_swap = self.is_swap_tx(tx) fee = tx_wallet_delta.fee exp_n = None can_broadcast = False @@ -629,7 +633,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC): size = tx.estimated_size() fee_per_byte = fee / size exp_n = self.config.fee_to_depth(fee_per_byte) - can_bump = is_any_input_ismine and not tx.is_final() + can_bump = (is_any_input_ismine or is_swap) and not tx.is_final() can_dscancel = (is_any_input_ismine and not tx.is_final() and not all([self.is_mine(txout.address) for txout in tx.outputs()])) try: @@ -640,7 +644,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC): else: status = _('Local') can_broadcast = self.network is not None - can_bump = is_any_input_ismine and not tx.is_final() + can_bump = (is_any_input_ismine or is_swap) and not tx.is_final() else: status = _("Signed") can_broadcast = self.network is not None @@ -1961,6 +1965,8 @@ class Abstract_Wallet(AddressSynchronizer, ABC): for k in self.get_keystores(): if k.can_sign_txin(txin): return True + if self.is_swap_tx(tx): + return True return False def get_input_tx(self, tx_hash: str, *, ignore_network_issues=False) -> Optional[Transaction]: @@ -2013,6 +2019,11 @@ class Abstract_Wallet(AddressSynchronizer, ABC): return if not isinstance(tx, PartialTransaction): return + # note: swap signing does not require the password + swap = self.lnworker.swap_manager.get_swap_by_tx(tx) if self.lnworker else None + if swap: + self.lnworker.swap_manager.sign_tx(tx, swap) + return # add info to a temporary tx copy; including xpubs # and full derivation paths as hw keystores might want them tmp_tx = copy.deepcopy(tx)