tlnchannel: store pre-signed sweep transactions after each new commitment - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit fde9f91902bcfa05bbc431f62f7d0bd7af3c92d2 DIR parent 3019aa35cf3b6deca9ec42d30af782d8a74a9389 HTML Author: ThomasV <thomasv@electrum.org> Date: Sat, 15 Dec 2018 11:38:46 +0100 lnchannel: store pre-signed sweep transactions after each new commitment Diffstat: M electrum/lnbase.py | 10 ++++++---- M electrum/lnchan.py | 27 +++++++++++++++++++-------- M electrum/lnsweep.py | 20 ++++++++++---------- M electrum/lnworker.py | 42 +++++++++++++------------------ M electrum/transaction.py | 3 +++ 5 files changed, 55 insertions(+), 47 deletions(-) --- DIR diff --git a/electrum/lnbase.py b/electrum/lnbase.py t@@ -499,9 +499,10 @@ class Peer(PrintError): "constraints": ChannelConstraints(capacity=funding_sat, is_initiator=True, funding_txn_minimum_depth=funding_txn_minimum_depth, feerate=feerate), "remote_commitment_to_be_revoked": None, } - chan = Channel(chan_dict, payment_completed=self.lnworker.payment_completed) + chan = Channel(chan_dict, + sweep_address=self.lnworker.sweep_address, + payment_completed=self.lnworker.payment_completed) chan.lnwatcher = self.lnwatcher - chan.sweep_address = self.lnworker.sweep_address chan.get_preimage_and_invoice = self.lnworker.get_invoice # FIXME hack. sig_64, _ = chan.sign_next_commitment() self.send_message("funding_created", t@@ -590,9 +591,10 @@ class Peer(PrintError): "constraints": ChannelConstraints(capacity=funding_sat, is_initiator=False, funding_txn_minimum_depth=min_depth, feerate=feerate), "remote_commitment_to_be_revoked": None, } - chan = Channel(chan_dict, payment_completed=self.lnworker.payment_completed) + chan = Channel(chan_dict, + sweep_adddress=self.lnworker.sweep_address, + payment_completed=self.lnworker.payment_completed) chan.lnwatcher = self.lnwatcher - chan.sweep_address = self.lnworker.sweep_address chan.get_preimage_and_invoice = self.lnworker.get_invoice # FIXME hack. remote_sig = funding_created['signature'] chan.receive_new_commitment(remote_sig, []) DIR diff --git a/electrum/lnchan.py b/electrum/lnchan.py t@@ -44,6 +44,7 @@ from .lnutil import funding_output_script, LOCAL, REMOTE, HTLCOwner, make_closin from .lnutil import ScriptHtlc, PaymentFailure, calc_onchain_fees, RemoteMisbehaving, make_htlc_output_witness_script from .transaction import Transaction from .lnsweep import create_sweeptxs_for_their_just_revoked_ctx +from .lnsweep import create_sweeptxs_for_our_latest_ctx, create_sweeptxs_for_their_latest_ctx class ChannelJsonEncoder(json.JSONEncoder): t@@ -146,10 +147,11 @@ class Channel(PrintError): except: return super().diagnostic_name() - def __init__(self, state, name = None, payment_completed : Optional[Callable[[HTLCOwner, UpdateAddHtlc, bytes], None]] = None): + def __init__(self, state, sweep_address = None, name = None, payment_completed : Optional[Callable[[HTLCOwner, UpdateAddHtlc, bytes], None]] = None): self.preimages = {} if not payment_completed: payment_completed = lambda this, x, y, z: None + self.sweep_address = sweep_address self.payment_completed = payment_completed assert 'local_state' not in state self.config = {} t@@ -203,9 +205,18 @@ class Channel(PrintError): for sub in (LOCAL, REMOTE): self.log[sub].locked_in.update(self.log[sub].adds.keys()) - # used in lnworker.on_channel_closed - self.local_commitment = self.current_commitment(LOCAL) - self.remote_commitment = self.current_commitment(REMOTE) + self.set_local_commitment(self.current_commitment(LOCAL)) + self.set_remote_commitment(self.current_commitment(REMOTE)) + + def set_local_commitment(self, ctx): + self.local_commitment = ctx + if self.sweep_address is not None: + self.local_sweeptxs = create_sweeptxs_for_our_latest_ctx(self, self.local_commitment, self.sweep_address) + + def set_remote_commitment(self, ctx): + self.remote_commitment = ctx + if self.sweep_address is not None: + self.remote_sweeptxs = create_sweeptxs_for_their_latest_ctx(self, self.remote_commitment, self.sweep_address) def set_state(self, state: str): if self._state == 'FORCE_CLOSING': t@@ -389,7 +400,7 @@ class Channel(PrintError): if self.constraints.is_initiator and self.pending_fee[FUNDEE_ACKED]: self.pending_fee[FUNDER_SIGNED] = True - self.local_commitment = self.pending_commitment(LOCAL) + self.set_local_commitment(self.pending_commitment(LOCAL)) def verify_htlc(self, htlc: UpdateAddHtlc, htlc_sigs: Sequence[bytes], we_receive: bool) -> int: _, this_point, _ = self.points() t@@ -438,7 +449,7 @@ class Channel(PrintError): feerate=new_feerate ) - self.local_commitment = self.pending_commitment(LOCAL) + self.set_local_commitment(self.pending_commitment(LOCAL)) return RevokeAndAck(last_secret, next_point), "current htlcs" t@@ -530,8 +541,8 @@ class Channel(PrintError): if self.constraints.is_initiator: self.pending_fee[FUNDEE_ACKED] = True - self.local_commitment = self.pending_commitment(LOCAL) - self.remote_commitment = self.pending_commitment(REMOTE) + self.set_local_commitment(self.pending_commitment(LOCAL)) + self.set_remote_commitment(self.pending_commitment(REMOTE)) self.remote_commitment_to_be_revoked = prev_remote_commitment return received_this_batch, sent_this_batch DIR diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py t@@ -143,7 +143,7 @@ def create_sweeptxs_for_our_latest_ctx(chan: 'Channel', ctx: Transaction, to_self_delay = chan.config[REMOTE].to_self_delay this_htlc_privkey = derive_privkey(secret=int.from_bytes(this_conf.htlc_basepoint.privkey, 'big'), per_commitment_point=our_pcp).to_bytes(32, 'big') - txs = [] + txs = {} # to_local sweep_tx = maybe_create_sweeptx_that_spends_to_local_in_our_ctx(ctx=ctx, sweep_address=sweep_address, t@@ -151,7 +151,7 @@ def create_sweeptxs_for_our_latest_ctx(chan: 'Channel', ctx: Transaction, remote_revocation_pubkey=other_revocation_pubkey, to_self_delay=to_self_delay) if sweep_tx: - txs.append(EncumberedTransaction('our_ctx_to_local', sweep_tx, csv_delay=to_self_delay, cltv_expiry=0)) + txs[sweep_tx.prevout(0)] = EncumberedTransaction('our_ctx_to_local', sweep_tx, csv_delay=to_self_delay, cltv_expiry=0) # HTLCs def create_txns_for_htlc(htlc: 'UpdateAddHtlc', is_received_htlc: bool) -> Tuple[Optional[Transaction], Optional[Transaction]]: if is_received_htlc: t@@ -184,16 +184,16 @@ def create_sweeptxs_for_our_latest_ctx(chan: 'Channel', ctx: Transaction, for htlc in offered_htlcs: htlc_tx, to_wallet_tx = create_txns_for_htlc(htlc, is_received_htlc=False) if htlc_tx and to_wallet_tx: - txs.append(EncumberedTransaction(f'second_stage_to_wallet_{bh2u(htlc.payment_hash)}', to_wallet_tx, csv_delay=to_self_delay, cltv_expiry=0)) - txs.append(EncumberedTransaction(f'our_ctx_htlc_tx_{bh2u(htlc.payment_hash)}', htlc_tx, csv_delay=0, cltv_expiry=htlc.cltv_expiry)) + txs[to_wallet_tx.prevout(0)] = EncumberedTransaction(f'second_stage_to_wallet_{bh2u(htlc.payment_hash)}', to_wallet_tx, csv_delay=to_self_delay, cltv_expiry=0) + txs[htlc_tx.prevout(0)] = EncumberedTransaction(f'our_ctx_htlc_tx_{bh2u(htlc.payment_hash)}', htlc_tx, csv_delay=0, cltv_expiry=htlc.cltv_expiry) # received HTLCs, in our ctx --> "success" # TODO consider carefully if "included_htlcs" is what we need here received_htlcs = list(chan.included_htlcs(LOCAL, REMOTE)) # type: List[UpdateAddHtlc] for htlc in received_htlcs: htlc_tx, to_wallet_tx = create_txns_for_htlc(htlc, is_received_htlc=True) if htlc_tx and to_wallet_tx: - txs.append(EncumberedTransaction(f'second_stage_to_wallet_{bh2u(htlc.payment_hash)}', to_wallet_tx, csv_delay=to_self_delay, cltv_expiry=0)) - txs.append(EncumberedTransaction(f'our_ctx_htlc_tx_{bh2u(htlc.payment_hash)}', htlc_tx, csv_delay=0, cltv_expiry=0)) + txs[to_wallet_tx.prevout(0)] = EncumberedTransaction(f'second_stage_to_wallet_{bh2u(htlc.payment_hash)}', to_wallet_tx, csv_delay=to_self_delay, cltv_expiry=0) + txs[htlc_tx.prevout(0)] = EncumberedTransaction(f'our_ctx_htlc_tx_{bh2u(htlc.payment_hash)}', htlc_tx, csv_delay=0, cltv_expiry=0) return txs t@@ -232,7 +232,7 @@ def create_sweeptxs_for_their_latest_ctx(chan: 'Channel', ctx: Transaction, other_payment_privkey = derive_privkey(other_payment_bp_privkey.secret_scalar, their_pcp) other_payment_privkey = ecc.ECPrivkey.from_secret_scalar(other_payment_privkey) - txs = [] + txs = {} if per_commitment_secret: # breach # to_local other_revocation_privkey = derive_blinded_privkey(other_conf.revocation_basepoint.privkey, t@@ -250,7 +250,7 @@ def create_sweeptxs_for_their_latest_ctx(chan: 'Channel', ctx: Transaction, sweep_address=sweep_address, our_payment_privkey=other_payment_privkey) if sweep_tx: - txs.append(EncumberedTransaction('their_ctx_to_remote', sweep_tx, csv_delay=0, cltv_expiry=0)) + txs[sweep_tx.prevout(0)] = EncumberedTransaction('their_ctx_to_remote', sweep_tx, csv_delay=0, cltv_expiry=0) # HTLCs # from their ctx, we can only redeem HTLCs if the ctx was not revoked, # as old HTLCs are not stored. (if it was revoked, then we should have presigned txns t@@ -286,13 +286,13 @@ def create_sweeptxs_for_their_latest_ctx(chan: 'Channel', ctx: Transaction, for htlc in received_htlcs: sweep_tx = create_sweeptx_for_htlc(htlc, is_received_htlc=True) if sweep_tx: - txs.append(EncumberedTransaction(f'their_ctx_sweep_htlc_{bh2u(htlc.payment_hash)}', sweep_tx, csv_delay=0, cltv_expiry=htlc.cltv_expiry)) + txs[prevout] = EncumberedTransaction(f'their_ctx_sweep_htlc_{bh2u(htlc.payment_hash)}', sweep_tx, csv_delay=0, cltv_expiry=htlc.cltv_expiry) # offered HTLCs, in their ctx --> "success" offered_htlcs = chan.included_htlcs_in_their_latest_ctxs(REMOTE)[ctn] # type: List[UpdateAddHtlc] for htlc in offered_htlcs: sweep_tx = create_sweeptx_for_htlc(htlc, is_received_htlc=False) if sweep_tx: - txs.append(EncumberedTransaction(f'their_ctx_sweep_htlc_{bh2u(htlc.payment_hash)}', sweep_tx, csv_delay=0, cltv_expiry=0)) + txs[prevout] = EncumberedTransaction(f'their_ctx_sweep_htlc_{bh2u(htlc.payment_hash)}', sweep_tx, csv_delay=0, cltv_expiry=0) return txs DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py t@@ -39,7 +39,6 @@ from .lnutil import (Outpoint, calc_short_channel_id, LNPeerAddr, from .i18n import _ from .lnrouter import RouteEdge, is_route_sane_to_use from .address_synchronizer import TX_HEIGHT_LOCAL -from .lnsweep import create_sweeptxs_for_our_latest_ctx, create_sweeptxs_for_their_latest_ctx if TYPE_CHECKING: from .network import Network t@@ -78,11 +77,9 @@ class LNWorker(PrintError): self.peers = {} # type: Dict[bytes, Peer] # pubkey -> Peer self.channels = {} # type: Dict[bytes, Channel] for x in wallet.storage.get("channels", []): - c = Channel(x, payment_completed=self.payment_completed) + c = Channel(x, sweep_address=self.sweep_address, payment_completed=self.payment_completed) self.channels[c.channel_id] = c - c.lnwatcher = network.lnwatcher - c.sweep_address = self.sweep_address self.invoices = wallet.storage.get('lightning_invoices', {}) # type: Dict[str, Tuple[str,str]] # RHASH -> (preimage, invoice) for chan_id, chan in self.channels.items(): self.network.lnwatcher.watch_channel(chan.get_funding_address(), chan.funding_outpoint.to_str()) t@@ -314,34 +311,28 @@ class LNWorker(PrintError): self.network.trigger_callback('channel', chan) # remove from channel_db self.channel_db.remove_channel(chan.short_channel_id) - # sweep - our_ctx = chan.local_commitment - their_ctx = chan.remote_commitment - if txid == our_ctx.txid(): + # detect who closed + if txid == chan.local_commitment.txid(): self.print_error('we force closed', funding_outpoint) - # we force closed - encumbered_sweeptxs = create_sweeptxs_for_our_latest_ctx(chan, our_ctx, chan.sweep_address) - elif txid == their_ctx.txid(): + encumbered_sweeptxs = chan.local_sweeptxs + elif txid == chan.remote_commitment.txid(): self.print_error('they force closed', funding_outpoint) - # they force closed - encumbered_sweeptxs = create_sweeptxs_for_their_latest_ctx(chan, their_ctx, chan.sweep_address) + encumbered_sweeptxs = chan.remote_sweeptxs else: - # cooperative close or breach self.print_error('not sure who closed', funding_outpoint) - encumbered_sweeptxs = [] - - local_height = self.network.get_local_height() - for e_tx in encumbered_sweeptxs: - txin = e_tx.tx.inputs()[0] - prev_txid = txin['prevout_hash'] - txin_outpoint = Transaction.get_outpoint_from_txin(txin) - spender = spenders.get(txin_outpoint) + return + # sweep + for prevout, spender in spenders.items(): + e_tx = encumbered_sweeptxs.get(prevout) + if e_tx is None: + continue if spender is not None: - self.print_error('prev_tx already spent', prev_txid) + self.print_error('outpoint already spent', prevout) continue - num_conf = self.network.lnwatcher.get_tx_height(prev_txid).conf + prev_txid, prev_index = prevout.split(':') broadcast = True if e_tx.cltv_expiry: + local_height = self.network.get_local_height() if local_height > e_tx.cltv_expiry: self.print_error(e_tx.name, 'CLTV ({} > {}) fulfilled'.format(local_height, e_tx.cltv_expiry)) else: t@@ -349,13 +340,14 @@ class LNWorker(PrintError): .format(e_tx.name, local_height, e_tx.cltv_expiry, funding_outpoint[:8], prev_txid[:8])) broadcast = False if e_tx.csv_delay: + num_conf = self.network.lnwatcher.get_tx_height(prev_txid).conf if num_conf < e_tx.csv_delay: self.print_error(e_tx.name, 'waiting for {}: CSV ({} >= {}), funding outpoint {} and tx {}' .format(e_tx.name, num_conf, e_tx.csv_delay, funding_outpoint[:8], prev_txid[:8])) broadcast = False if broadcast: if not await self.network.lnwatcher.broadcast_or_log(funding_outpoint, e_tx): - self.print_error(e_tx.name, f'could not publish encumbered tx: {str(e_tx)}, prev_txid: {prev_txid}, local_height', local_height) + self.print_error(e_tx.name, f'could not publish encumbered tx: {str(sweep_tx)}, prev_txid: {prev_txid}') @log_exceptions DIR diff --git a/electrum/transaction.py b/electrum/transaction.py t@@ -933,6 +933,9 @@ class Transaction: prevout_n = txin['prevout_n'] return prevout_hash + ':%d' % prevout_n + def prevout(self, index): + return self.get_outpoint_from_txin(self.inputs()[index]) + @classmethod def serialize_input(self, txin, script): # Prev hash and index