From 9bfeffcdd17817ac068cc0ab09d085d94c4286b1 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 13 Sep 2021 14:31:01 +0200 Subject: [PATCH] lnsweep: update sweeps to_remote and htlcs * sweep to_remote output, as this is now a p2wsh (previously internal wallet address) * sweep htlc outputs with new scripts --- electrum/lnsweep.py | 110 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 92 insertions(+), 18 deletions(-) diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py index 42de48cd5..d76efd6ee 100644 --- a/electrum/lnsweep.py +++ b/electrum/lnsweep.py @@ -18,7 +18,7 @@ from .lnutil import (make_commitment_output_to_remote_address, make_commitment_o LOCAL, REMOTE, make_htlc_output_witness_script, get_ordered_channel_configs, get_per_commitment_secret_from_seed, RevocationStore, extract_ctn_from_tx_and_chan, UnableToDeriveSecret, SENT, RECEIVED, - map_htlcs_to_ctx_output_idxs, Direction) + map_htlcs_to_ctx_output_idxs, Direction, make_commitment_output_to_remote_witness_script) from .transaction import (Transaction, TxOutput, PartialTransaction, PartialTxInput, PartialTxOutput, TxOutpoint) from .simple_config import SimpleConfig @@ -147,7 +147,7 @@ def create_sweeptx_for_their_revoked_htlc( htlc_tx: Transaction, sweep_address: str) -> Optional[SweepInfo]: - x = analyze_ctx(chan, ctx) + x = extract_ctx_secrets(chan, ctx) if not x: return ctn, their_pcp, is_revocation, per_commitment_secret = x @@ -187,10 +187,18 @@ def create_sweeptxs_for_our_ctx( *, chan: 'AbstractChannel', ctx: Transaction, sweep_address: str) -> Optional[Dict[str, SweepInfo]]: - """Handle the case where we force close unilaterally with our latest ctx. - Construct sweep txns for 'to_local', and for all HTLCs (2 txns each). + """Handle the case where we force-close unilaterally with our latest ctx. + + We sweep: + to_local: CSV delayed + htlc success: CSV delay with anchors, no delay otherwise + htlc timeout: CSV delay with anchors, CLTV locktime + second-stage htlc transactions: CSV delay + 'to_local' can be swept even if this is a breach (by us), but HTLCs cannot (old HTLCs are no longer stored). + + Outputs with CSV/CLTV are redeemed by LNWatcher. """ ctn = extract_ctn_from_tx_and_chan(ctx, chan) our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True) @@ -315,7 +323,7 @@ def create_sweeptxs_for_our_ctx( return txs -def analyze_ctx(chan: 'Channel', ctx: Transaction): +def extract_ctx_secrets(chan: 'Channel', ctx: Transaction): # note: the remote sometimes has two valid non-revoked commitment transactions, # either of which could be broadcast our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True) @@ -351,14 +359,23 @@ def create_sweeptxs_for_their_ctx( """Handle the case when the remote force-closes with their ctx. Sweep outputs that do not have a CSV delay ('to_remote' and first-stage HTLCs). Outputs with CSV delay ('to_local' and second-stage HTLCs) are redeemed by LNWatcher. + + We sweep: + to_local: if revoked + to_remote: CSV delay with anchors, otherwise sweeping not needed + htlc success: CSV delay with anchors, no delay otherwise, or revoked + htlc timeout: CSV delay with anchors, CLTV locktime, or revoked + second-stage htlc transactions: CSV delay + + Outputs with CSV/CLTV are redeemed by LNWatcher. """ txs = {} # type: Dict[str, SweepInfo] our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True) - x = analyze_ctx(chan, ctx) + x = extract_ctx_secrets(chan, ctx) if not x: return ctn, their_pcp, is_revocation, per_commitment_secret = x - # to_local and to_remote addresses + # to_local our_revocation_pubkey = derive_blinded_pubkey(our_conf.revocation_basepoint.pubkey, their_pcp) their_delayed_pubkey = derive_pubkey(their_conf.delayed_basepoint.pubkey, their_pcp) witness_script = make_commitment_output_to_local_witness_script( @@ -377,6 +394,7 @@ def create_sweeptxs_for_their_ctx( if not found_to_local and not found_to_remote: return chan.logger.debug(f'(lnsweep) found their ctx: {to_local_address} {to_remote_address}') + # to_local is handled by lnwatcher if is_revocation: our_revocation_privkey = derive_blinded_privkey(our_conf.revocation_basepoint.privkey, per_commitment_secret) gen_tx = create_sweeptx_for_their_revoked_ctx(chan, ctx, per_commitment_secret, sweep_address) @@ -387,12 +405,42 @@ def create_sweeptxs_for_their_ctx( csv_delay=0, cltv_abs=0, gen_tx=gen_tx) - # prep + + # to_remote + if chan.has_anchors(): + csv_delay = 1 + sweep_to_remote = True + our_payment_privkey = ecc.ECPrivkey(our_conf.payment_basepoint.privkey) + else: + assert chan.is_static_remotekey_enabled() + csv_delay = 0 + sweep_to_remote = False + our_payment_privkey = None + + if sweep_to_remote: + assert our_payment_pubkey == our_payment_privkey.get_public_key_bytes(compressed=True) + output_idxs = ctx.get_output_idxs_from_address(to_remote_address) + if output_idxs: + output_idx = output_idxs.pop() + prevout = ctx.txid() + ':%d' % output_idx + sweep_tx = lambda: create_sweeptx_their_ctx_to_remote( + sweep_address=sweep_address, + ctx=ctx, + output_idx=output_idx, + our_payment_privkey=our_payment_privkey, + config=chan.lnworker.config, + has_anchors=chan.has_anchors() + ) + txs[prevout] = SweepInfo( + name='their_ctx_to_remote', + csv_delay=csv_delay, + cltv_abs=0, + gen_tx=sweep_tx) + + # HTLCs our_htlc_privkey = derive_privkey(secret=int.from_bytes(our_conf.htlc_basepoint.privkey, 'big'), per_commitment_point=their_pcp) our_htlc_privkey = ecc.ECPrivkey.from_secret_scalar(our_htlc_privkey) their_htlc_pubkey = derive_pubkey(their_conf.htlc_basepoint.pubkey, their_pcp) - # to_local is handled by lnwatcher - # HTLCs def create_sweeptx_for_htlc( *, htlc: 'UpdateAddHtlc', is_received_htlc: bool, @@ -408,6 +456,7 @@ def create_sweeptxs_for_their_ctx( has_anchors=chan.has_anchors()) cltv_abs = htlc.cltv_abs if is_received_htlc and not is_revocation else 0 + csv_delay = 1 if chan.has_anchors() else 0 prevout = ctx.txid() + ':%d'%ctx_output_idx sweep_tx = lambda: create_sweeptx_their_ctx_htlc( ctx=ctx, @@ -418,10 +467,12 @@ def create_sweeptxs_for_their_ctx( privkey=our_revocation_privkey if is_revocation else our_htlc_privkey.get_secret_bytes(), is_revocation=is_revocation, cltv_abs=cltv_abs, - config=chan.lnworker.config) + config=chan.lnworker.config, + has_anchors=chan.has_anchors(), + ) txs[prevout] = SweepInfo( - name=f'their_ctx_htlc_{ctx_output_idx}', - csv_delay=0, + name=f'their_ctx_htlc_{ctx_output_idx}{"_for_revoked_ctx" if is_revocation else ""}', + csv_delay=csv_delay, cltv_abs=cltv_abs, gen_tx=sweep_tx) # received HTLCs, in their ctx --> "timeout" @@ -488,7 +539,10 @@ def create_sweeptx_their_ctx_htlc( ctx: Transaction, witness_script: bytes, sweep_address: str, preimage: Optional[bytes], output_idx: int, privkey: bytes, is_revocation: bool, - cltv_abs: int, config: SimpleConfig) -> Optional[PartialTransaction]: + cltv_abs: int, + config: SimpleConfig, + has_anchors: bool, +) -> Optional[PartialTransaction]: assert type(cltv_abs) is int preimage = preimage or b'' # preimage is required iff (not is_revocation and htlc is offered) val = ctx.outputs()[output_idx].value @@ -497,6 +551,8 @@ def create_sweeptx_their_ctx_htlc( txin._trusted_value_sats = val txin.witness_script = witness_script txin.script_sig = b'' + if has_anchors: + txin.nsequence = 1 sweep_inputs = [txin] tx_size_bytes = 200 # TODO (depends on offered/received and is_revocation) fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True) @@ -518,7 +574,9 @@ def create_sweeptx_their_ctx_htlc( def create_sweeptx_their_ctx_to_remote( sweep_address: str, ctx: Transaction, output_idx: int, our_payment_privkey: ecc.ECPrivkey, - config: SimpleConfig) -> Optional[PartialTransaction]: + config: SimpleConfig, + has_anchors: bool, +) -> Optional[PartialTransaction]: our_payment_pubkey = our_payment_privkey.get_public_key_bytes(compressed=True) val = ctx.outputs()[output_idx].value prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx) @@ -526,15 +584,31 @@ def create_sweeptx_their_ctx_to_remote( txin._trusted_value_sats = val desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=our_payment_pubkey.hex(), script_type='p2wpkh') txin.script_descriptor = desc + txin.pubkeys = [bfh(our_payment_pubkey)] + txin.num_sig = 1 + if not has_anchors: + txin.script_type = 'p2wpkh' + tx_size_bytes = 110 # approx size of p2wpkh->p2wpkh + else: + txin.script_sig = b'' + txin.witness_script = make_commitment_output_to_remote_witness_script(bfh(our_payment_pubkey)) + txin.nsequence = 1 + tx_size_bytes = 196 # approx size of p2wsh->p2wpkh sweep_inputs = [txin] - tx_size_bytes = 110 # approx size of p2wpkh->p2wpkh fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True) outvalue = val - fee if outvalue <= dust_threshold(): return None sweep_outputs = [PartialTxOutput.from_address_and_value(sweep_address, outvalue)] sweep_tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs) - sweep_tx.set_rbf(True) - sweep_tx.sign({our_payment_pubkey: our_payment_privkey.get_secret_bytes()}) + + if not has_anchors: + sweep_tx.set_rbf(True) + sweep_tx.sign({our_payment_pubkey: our_payment_privkey.get_secret_bytes()}) + else: + sig = sweep_tx.sign_txin(0, our_payment_privkey.get_secret_bytes()) + witness = construct_witness([sig, sweep_tx.inputs()[0].witness_script]) + sweep_tx.inputs()[0].witness = bfh(witness) + if not sweep_tx.is_complete(): raise Exception('channel close sweep tx is not complete') return sweep_tx