electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 595cfcbb65ec7303656d3d5a5b2e1182a7188fcb
parent bc729664429aecf692b6f7a4fb02ece560358b34
Author: SomberNight <somber.night@protonmail.com>
Date:   Tue, 23 Oct 2018 16:44:39 +0200

move sweeping methods from lnchan.py to new file

also sweep "received" htlcs from "our" ctx
also sweep htlcs from their ctx (non-breach)
extract ctn; included_htlcs_in_their_latest_ctxs

Diffstat:
Melectrum/lnbase.py | 2++
Melectrum/lnchan.py | 312+++++++++++++++----------------------------------------------------------------
Aelectrum/lnsweep.py | 493+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Melectrum/lnutil.py | 86+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Melectrum/lnwatcher.py | 6++----
Melectrum/lnworker.py | 2++
Melectrum/tests/test_lnchan.py | 21+++++++++++++--------
Melectrum/tests/test_lnutil.py | 2--
Melectrum/transaction.py | 22++++++++++++++++++++++
9 files changed, 645 insertions(+), 301 deletions(-)

diff --git a/electrum/lnbase.py b/electrum/lnbase.py @@ -500,6 +500,7 @@ class Peer(PrintError): chan = Channel(chan_dict, 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", temporary_channel_id=temp_channel_id, @@ -590,6 +591,7 @@ class Peer(PrintError): chan = Channel(chan_dict, 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, []) sig_64, _ = chan.sign_next_commitment() diff --git a/electrum/lnchan.py b/electrum/lnchan.py @@ -26,7 +26,7 @@ from collections import namedtuple, defaultdict import binascii import json from enum import Enum, auto -from typing import Optional, Dict, List, Tuple, NamedTuple, Set, Callable, Iterable +from typing import Optional, Dict, List, Tuple, NamedTuple, Set, Callable, Iterable, Sequence from copy import deepcopy from .util import bfh, PrintError, bh2u @@ -34,17 +34,18 @@ from .bitcoin import TYPE_SCRIPT, TYPE_ADDRESS from .bitcoin import redeem_script_to_address from .crypto import sha256, sha256d from . import ecc -from .lnutil import Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore, EncumberedTransaction +from .lnutil import Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore from .lnutil import get_per_commitment_secret_from_seed -from .lnutil import make_commitment_output_to_remote_address, make_commitment_output_to_local_witness_script -from .lnutil import secret_to_pubkey, derive_privkey, derive_pubkey, derive_blinded_pubkey, derive_blinded_privkey -from .lnutil import sign_and_get_sig_string, privkey_to_pubkey, make_htlc_tx_witness +from .lnutil import secret_to_pubkey, derive_privkey, derive_pubkey, derive_blinded_pubkey +from .lnutil import sign_and_get_sig_string from .lnutil import make_htlc_tx_with_open_channel, make_commitment, make_received_htlc, make_offered_htlc from .lnutil import HTLC_TIMEOUT_WEIGHT, HTLC_SUCCESS_WEIGHT from .lnutil import funding_output_script, LOCAL, REMOTE, HTLCOwner, make_closing_tx, make_commitment_outputs -from .lnutil import ScriptHtlc, SENT, RECEIVED, PaymentFailure, calc_onchain_fees, RemoteMisbehaving -from .transaction import Transaction, TxOutput, construct_witness -from .simple_config import SimpleConfig, FEERATE_FALLBACK_STATIC_FEE +from .lnutil import ScriptHtlc, PaymentFailure, calc_onchain_fees, RemoteMisbehaving, make_htlc_output_witness_script +from .transaction import Transaction +from .lnsweep import (create_sweeptxs_for_our_latest_ctx, create_sweeptxs_for_their_latest_ctx, + create_sweeptxs_for_their_just_revoked_ctx) + class ChannelJsonEncoder(json.JSONEncoder): def default(self, o): @@ -309,13 +310,16 @@ class Channel(PrintError): htlcsigs = [] for we_receive, htlcs in zip([True, False], [self.included_htlcs(REMOTE, REMOTE), self.included_htlcs(REMOTE, LOCAL)]): for htlc in htlcs: - args = [self.config[REMOTE].next_per_commitment_point, for_us, we_receive, pending_remote_commitment, htlc] - _script, htlc_tx = make_htlc_tx_with_open_channel(self, *args) + _script, htlc_tx = make_htlc_tx_with_open_channel(chan=self, + pcp=self.config[REMOTE].next_per_commitment_point, + for_us=for_us, + we_receive=we_receive, + commit=pending_remote_commitment, + htlc=htlc) sig = bfh(htlc_tx.sign_txin(0, their_remote_htlc_privkey)) htlc_sig = ecc.sig_string_from_der_sig(sig[:-1]) - htlcsigs.append((pending_remote_commitment.htlc_output_indices[htlc.payment_hash], htlc_sig)) - - self.process_new_offchain_ctx(pending_remote_commitment, ours=False) + htlc_output_idx = htlc_tx.inputs()[0]['prevout_n'] + htlcsigs.append((htlc_output_idx, htlc_sig)) htlcsigs.sort() htlcsigs = [x[1] for x in htlcsigs] @@ -383,11 +387,14 @@ class Channel(PrintError): if self.constraints.is_initiator and self.pending_fee[FUNDEE_ACKED]: self.pending_fee[FUNDER_SIGNED] = True - self.process_new_offchain_ctx(pending_local_commitment, ours=True) - - def verify_htlc(self, htlc, htlc_sigs, we_receive): - _, this_point, _ = self.points() - _script, htlc_tx = make_htlc_tx_with_open_channel(self, this_point, True, we_receive, self.pending_commitment(LOCAL), htlc) + def verify_htlc(self, htlc: UpdateAddHtlc, htlc_sigs: Sequence[bytes], we_receive: bool) -> int: + _, this_point, _ = self.points + _script, htlc_tx = make_htlc_tx_with_open_channel(chan=self, + pcp=this_point, + for_us=True, + we_receive=we_receive, + commit=self.pending_commitment(LOCAL), + htlc=htlc) pre_hash = sha256d(bfh(htlc_tx.serialize_preimage(0))) remote_htlc_pubkey = derive_pubkey(self.config[REMOTE].htlc_basepoint.pubkey, this_point) for idx, sig in enumerate(htlc_sigs): @@ -396,6 +403,13 @@ class Channel(PrintError): else: raise Exception(f'failed verifying HTLC signatures: {htlc}') + def get_remote_htlc_sig_for_htlc(self, htlc: UpdateAddHtlc, we_receive: bool) -> bytes: + data = self.config[LOCAL].current_htlc_signatures + htlc_sigs = [data[i:i + 64] for i in range(0, len(data), 64)] + idx = self.verify_htlc(htlc, htlc_sigs, we_receive=we_receive) + remote_htlc_sig = ecc.der_sig_from_sig_string(htlc_sigs[idx]) + b'\x01' + return remote_htlc_sig + def revoke_current_commitment(self): self.print_error("revoke_current_commitment") @@ -435,36 +449,18 @@ class Channel(PrintError): next_point = secret_to_pubkey(int.from_bytes(next_secret, 'big')) return last_secret, this_point, next_point - # TODO batch sweeps - # TODO sweep HTLC outputs - def process_new_offchain_ctx(self, ctx, ours: bool): + def process_new_revocation_secret(self, per_commitment_secret: bytes): if not self.lnwatcher: return outpoint = self.funding_outpoint.to_str() - if ours: - ctn = self.config[LOCAL].ctn + 1 - our_per_commitment_secret = get_per_commitment_secret_from_seed( - self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - ctn) - our_cur_pcp = ecc.ECPrivkey(our_per_commitment_secret).get_public_key_bytes(compressed=True) - encumbered_sweeptxs = create_sweeptxs_for_our_ctx(self, ctx, our_cur_pcp, self.sweep_address) - else: - their_cur_pcp = self.config[REMOTE].next_per_commitment_point - encumbered_sweeptxs = [(None, maybe_create_sweeptx_for_their_ctx_to_remote(self, ctx, their_cur_pcp, self.sweep_address))] + ctx = self.remote_commitment_to_be_revoked # FIXME can't we just reconstruct it? + encumbered_sweeptxs = create_sweeptxs_for_their_just_revoked_ctx(self, ctx, per_commitment_secret, self.sweep_address) for prev_txid, encumbered_tx in encumbered_sweeptxs: if prev_txid is None: prev_txid = ctx.txid() if encumbered_tx is not None: self.lnwatcher.add_sweep_tx(outpoint, prev_txid, encumbered_tx.to_json()) - def process_new_revocation_secret(self, per_commitment_secret: bytes): - if not self.lnwatcher: - return - outpoint = self.funding_outpoint.to_str() - ctx = self.remote_commitment_to_be_revoked - encumbered_sweeptx = maybe_create_sweeptx_for_their_ctx_to_local(self, ctx, per_commitment_secret, self.sweep_address) - if encumbered_sweeptx: - self.lnwatcher.add_sweep_tx(outpoint, ctx.txid(), encumbered_sweeptx.to_json()) - def receive_revocation(self, revocation) -> Tuple[int, int]: self.print_error("receive_revocation") @@ -476,12 +472,6 @@ class Channel(PrintError): self.log = old_logs raise Exception('revoked secret not for current point') - if self.pending_fee is not None: - if not self.constraints.is_initiator: - self.pending_fee[FUNDEE_SIGNED] = True - if self.constraints.is_initiator and self.pending_fee[FUNDEE_ACKED]: - self.pending_fee[FUNDER_SIGNED] = True - # FIXME not sure this is correct... but it seems to work # if there are update_add_htlc msgs between commitment_signed and rev_ack, # this might break @@ -490,6 +480,14 @@ class Channel(PrintError): self.config[REMOTE].revocation_store.add_next_entry(revocation.per_commitment_secret) self.process_new_revocation_secret(revocation.per_commitment_secret) + ##### start applying fee/htlc changes + + if self.pending_fee is not None: + if not self.constraints.is_initiator: + self.pending_fee[FUNDEE_SIGNED] = True + if self.constraints.is_initiator and self.pending_fee[FUNDEE_ACKED]: + self.pending_fee[FUNDER_SIGNED] = True + def mark_settled(subject): """ find pending settlements for subject (LOCAL or REMOTE) and mark them settled, return value of settled htlcs @@ -768,21 +766,19 @@ class Channel(PrintError): other_htlc_pubkey = derive_pubkey(other_config.htlc_basepoint.pubkey, this_point) this_htlc_pubkey = derive_pubkey(this_config.htlc_basepoint.pubkey, this_point) other_revocation_pubkey = derive_blinded_pubkey(other_config.revocation_basepoint.pubkey, this_point) - htlcs = [] + htlcs = [] # type: List[ScriptHtlc] + def append_htlc(htlc: UpdateAddHtlc, is_received_htlc: bool): + htlcs.append(ScriptHtlc(make_htlc_output_witness_script( + is_received_htlc=is_received_htlc, + remote_revocation_pubkey=other_revocation_pubkey, + remote_htlc_pubkey=other_htlc_pubkey, + local_htlc_pubkey=this_htlc_pubkey, + payment_hash=htlc.payment_hash, + cltv_expiry=htlc.cltv_expiry), htlc)) for htlc in self.included_htlcs(subject, -subject): - htlcs.append( ScriptHtlc( make_received_htlc( - other_revocation_pubkey, - other_htlc_pubkey, - this_htlc_pubkey, - htlc.payment_hash, - htlc.cltv_expiry), htlc)) + append_htlc(htlc, is_received_htlc=True) for htlc in self.included_htlcs(subject, subject): - htlcs.append( - ScriptHtlc( make_offered_htlc( - other_revocation_pubkey, - other_htlc_pubkey, - this_htlc_pubkey, - htlc.payment_hash), htlc)) + append_htlc(htlc, is_received_htlc=False) if subject != LOCAL: remote_msat, local_msat = local_msat, remote_msat payment_pubkey = derive_pubkey(other_config.payment_basepoint.pubkey, this_point) @@ -851,209 +847,15 @@ class Channel(PrintError): assert tx.is_complete() return tx - def included_htlcs_in_latest_ctxs(self): + def included_htlcs_in_their_latest_ctxs(self, htlc_initiator) -> Dict[int, List[UpdateAddHtlc]]: """ A map from commitment number to list of HTLCs in their latest two commitment transactions. The oldest might have been revoked. """ - old_htlcs = list(self.included_htlcs(REMOTE, REMOTE, only_pending=False)) \ - + list(self.included_htlcs(REMOTE, LOCAL, only_pending=False)) + old_htlcs = list(self.included_htlcs(REMOTE, htlc_initiator, only_pending=False)) old_logs = dict(self.lock_in_htlc_changes(LOCAL)) - new_htlcs = list(self.included_htlcs(REMOTE, REMOTE)) \ - + list(self.included_htlcs(REMOTE, LOCAL)) + new_htlcs = list(self.included_htlcs(REMOTE, htlc_initiator)) self.log = old_logs return {self.config[REMOTE].ctn: old_htlcs, self.config[REMOTE].ctn+1: new_htlcs, } - -def maybe_create_sweeptx_for_their_ctx_to_remote(chan, ctx, their_pcp: bytes, - sweep_address) -> Optional[EncumberedTransaction]: - assert isinstance(their_pcp, bytes) - payment_bp_privkey = ecc.ECPrivkey(chan.config[LOCAL].payment_basepoint.privkey) - our_payment_privkey = derive_privkey(payment_bp_privkey.secret_scalar, their_pcp) - our_payment_privkey = ecc.ECPrivkey.from_secret_scalar(our_payment_privkey) - our_payment_pubkey = our_payment_privkey.get_public_key_bytes(compressed=True) - to_remote_address = make_commitment_output_to_remote_address(our_payment_pubkey) - for output_idx, (type_, addr, val) in enumerate(ctx.outputs()): - if type_ == TYPE_ADDRESS and addr == to_remote_address: - break - else: - return None - sweep_tx = create_sweeptx_their_ctx_to_remote(address=sweep_address, - ctx=ctx, - output_idx=output_idx, - our_payment_privkey=our_payment_privkey) - return EncumberedTransaction('their_ctx_to_remote', sweep_tx, csv_delay=0, cltv_expiry=0) - - -def maybe_create_sweeptx_for_their_ctx_to_local(chan, ctx, per_commitment_secret: bytes, - sweep_address) -> Optional[EncumberedTransaction]: - assert isinstance(per_commitment_secret, bytes) - per_commitment_point = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True) - revocation_privkey = derive_blinded_privkey(chan.config[LOCAL].revocation_basepoint.privkey, - per_commitment_secret) - revocation_pubkey = ecc.ECPrivkey(revocation_privkey).get_public_key_bytes(compressed=True) - to_self_delay = chan.config[LOCAL].to_self_delay - delayed_pubkey = derive_pubkey(chan.config[REMOTE].delayed_basepoint.pubkey, - per_commitment_point) - witness_script = bh2u(make_commitment_output_to_local_witness_script( - revocation_pubkey, to_self_delay, delayed_pubkey)) - to_local_address = redeem_script_to_address('p2wsh', witness_script) - for output_idx, o in enumerate(ctx.outputs()): - if o.type == TYPE_ADDRESS and o.address == to_local_address: - break - else: - return None - sweep_tx = create_sweeptx_ctx_to_local(address=sweep_address, - ctx=ctx, - output_idx=output_idx, - witness_script=witness_script, - privkey=revocation_privkey, - is_revocation=True) - return EncumberedTransaction('their_ctx_to_local', sweep_tx, csv_delay=0, cltv_expiry=0) - - -def create_sweeptxs_for_our_ctx(chan, ctx, our_pcp: bytes, sweep_address) \ - -> List[Tuple[Optional[str],EncumberedTransaction]]: - assert isinstance(our_pcp, bytes) - delayed_bp_privkey = ecc.ECPrivkey(chan.config[LOCAL].delayed_basepoint.privkey) - our_localdelayed_privkey = derive_privkey(delayed_bp_privkey.secret_scalar, our_pcp) - our_localdelayed_privkey = ecc.ECPrivkey.from_secret_scalar(our_localdelayed_privkey) - our_localdelayed_pubkey = our_localdelayed_privkey.get_public_key_bytes(compressed=True) - revocation_pubkey = derive_blinded_pubkey(chan.config[REMOTE].revocation_basepoint.pubkey, - our_pcp) - to_self_delay = chan.config[REMOTE].to_self_delay - witness_script = bh2u(make_commitment_output_to_local_witness_script( - revocation_pubkey, to_self_delay, our_localdelayed_pubkey)) - to_local_address = redeem_script_to_address('p2wsh', witness_script) - txs = [] - for output_idx, o in enumerate(ctx.outputs()): - if o.type == TYPE_ADDRESS and o.address == to_local_address: - sweep_tx = create_sweeptx_ctx_to_local(address=sweep_address, - ctx=ctx, - output_idx=output_idx, - witness_script=witness_script, - privkey=our_localdelayed_privkey.get_secret_bytes(), - is_revocation=False, - to_self_delay=to_self_delay) - - txs.append((None, EncumberedTransaction('our_ctx_to_local', sweep_tx, csv_delay=to_self_delay, cltv_expiry=0))) - break - - # TODO htlc successes - htlcs = list(chan.included_htlcs(LOCAL, LOCAL)) # timeouts - for htlc in htlcs: - witness_script, htlc_tx = make_htlc_tx_with_open_channel( - chan, - our_pcp, - True, # for_us - False, # we_receive - ctx, htlc) - - data = chan.config[LOCAL].current_htlc_signatures - htlc_sigs = [data[i:i+64] for i in range(0, len(data), 64)] - idx = chan.verify_htlc(htlc, htlc_sigs, False) - remote_htlc_sig = ecc.der_sig_from_sig_string(htlc_sigs[idx]) + b'\x01' - - remote_revocation_pubkey = derive_blinded_pubkey(chan.config[REMOTE].revocation_basepoint.pubkey, our_pcp) - remote_htlc_pubkey = derive_pubkey(chan.config[REMOTE].htlc_basepoint.pubkey, our_pcp) - local_htlc_key = derive_privkey( - int.from_bytes(chan.config[LOCAL].htlc_basepoint.privkey, 'big'), - our_pcp).to_bytes(32, 'big') - program = make_offered_htlc(remote_revocation_pubkey, remote_htlc_pubkey, privkey_to_pubkey(local_htlc_key), htlc.payment_hash) - local_htlc_sig = bfh(htlc_tx.sign_txin(0, local_htlc_key)) - - htlc_tx.inputs()[0]['witness'] = bh2u(make_htlc_tx_witness(remote_htlc_sig, local_htlc_sig, b'', program)) - - tx_size_bytes = 999 # TODO - fee_per_kb = FEERATE_FALLBACK_STATIC_FEE - fee = SimpleConfig.estimate_fee_for_feerate(fee_per_kb, tx_size_bytes) - second_stage_outputs = [TxOutput(TYPE_ADDRESS, chan.sweep_address, htlc.amount_msat // 1000 - fee)] - assert to_self_delay is not None - second_stage_inputs = [{ - 'scriptSig': '', - 'type': 'p2wsh', - 'signatures': [], - 'num_sig': 0, - 'prevout_n': 0, - 'prevout_hash': htlc_tx.txid(), - 'value': htlc_tx.outputs()[0].value, - 'coinbase': False, - 'preimage_script': bh2u(witness_script), - 'sequence': to_self_delay, - }] - tx = Transaction.from_io(second_stage_inputs, second_stage_outputs, version=2) - - local_delaykey = derive_privkey( - int.from_bytes(chan.config[LOCAL].delayed_basepoint.privkey, 'big'), - our_pcp).to_bytes(32, 'big') - assert local_delaykey == our_localdelayed_privkey.get_secret_bytes() - - witness = construct_witness([bfh(tx.sign_txin(0, local_delaykey)), 0, witness_script]) - tx.inputs()[0]['witness'] = witness - assert tx.is_complete() - - txs.append((htlc_tx.txid(), EncumberedTransaction(f'second_stage_to_wallet_{bh2u(htlc.payment_hash)}', tx, csv_delay=to_self_delay, cltv_expiry=0))) - txs.append((ctx.txid(), EncumberedTransaction(f'our_ctx_htlc_tx_{bh2u(htlc.payment_hash)}', htlc_tx, csv_delay=0, cltv_expiry=htlc.cltv_expiry))) - - return txs - -def create_sweeptx_their_ctx_to_remote(address, ctx, output_idx: int, our_payment_privkey: ecc.ECPrivkey, - fee_per_kb: int=None) -> Transaction: - our_payment_pubkey = our_payment_privkey.get_public_key_hex(compressed=True) - val = ctx.outputs()[output_idx].value - sweep_inputs = [{ - 'type': 'p2wpkh', - 'x_pubkeys': [our_payment_pubkey], - 'num_sig': 1, - 'prevout_n': output_idx, - 'prevout_hash': ctx.txid(), - 'value': val, - 'coinbase': False, - 'signatures': [None], - }] - tx_size_bytes = 110 # approx size of p2wpkh->p2wpkh - if fee_per_kb is None: fee_per_kb = FEERATE_FALLBACK_STATIC_FEE - fee = SimpleConfig.estimate_fee_for_feerate(fee_per_kb, tx_size_bytes) - sweep_outputs = [TxOutput(TYPE_ADDRESS, address, val-fee)] - sweep_tx = Transaction.from_io(sweep_inputs, sweep_outputs) - sweep_tx.set_rbf(True) - sweep_tx.sign({our_payment_pubkey: (our_payment_privkey.get_secret_bytes(), True)}) - if not sweep_tx.is_complete(): - raise Exception('channel close sweep tx is not complete') - return sweep_tx - - -def create_sweeptx_ctx_to_local(address, ctx, output_idx: int, witness_script: str, - privkey: bytes, is_revocation: bool, - to_self_delay: int=None, - fee_per_kb: int=None) -> Transaction: - """Create a txn that sweeps the 'to_local' output of a commitment - transaction into our wallet. - - privkey: either revocation_privkey or localdelayed_privkey - is_revocation: tells us which ^ - """ - val = ctx.outputs()[output_idx].value - sweep_inputs = [{ - 'scriptSig': '', - 'type': 'p2wsh', - 'signatures': [], - 'num_sig': 0, - 'prevout_n': output_idx, - 'prevout_hash': ctx.txid(), - 'value': val, - 'coinbase': False, - 'preimage_script': witness_script, - }] - if to_self_delay is not None: - sweep_inputs[0]['sequence'] = to_self_delay - tx_size_bytes = 121 # approx size of to_local -> p2wpkh - if fee_per_kb is None: fee_per_kb = FEERATE_FALLBACK_STATIC_FEE - fee = SimpleConfig.estimate_fee_for_feerate(fee_per_kb, tx_size_bytes) - sweep_outputs = [TxOutput(TYPE_ADDRESS, address, val - fee)] - sweep_tx = Transaction.from_io(sweep_inputs, sweep_outputs, version=2) - sig = sweep_tx.sign_txin(0, privkey) - witness = construct_witness([sig, int(is_revocation), witness_script]) - sweep_tx.inputs()[0]['witness'] = witness - return sweep_tx diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py @@ -0,0 +1,493 @@ +# Copyright (C) 2018 The Electrum developers +# Distributed under the MIT software license, see the accompanying +# file LICENCE or http://www.opensource.org/licenses/mit-license.php + +from typing import Optional, Dict, List, Tuple, TYPE_CHECKING + +from .util import bfh, bh2u, print_error +from .bitcoin import TYPE_ADDRESS, redeem_script_to_address, dust_threshold +from . import ecc +from .lnutil import (EncumberedTransaction, + make_commitment_output_to_remote_address, make_commitment_output_to_local_witness_script, + derive_privkey, derive_pubkey, derive_blinded_pubkey, derive_blinded_privkey, + make_htlc_tx_witness, make_htlc_tx_with_open_channel, + LOCAL, REMOTE, make_htlc_output_witness_script, UnknownPaymentHash, + get_ordered_channel_configs, privkey_to_pubkey, get_per_commitment_secret_from_seed, + RevocationStore, extract_ctn_from_tx_and_chan, UnableToDeriveSecret) +from .transaction import Transaction, TxOutput, construct_witness +from .simple_config import SimpleConfig, FEERATE_FALLBACK_STATIC_FEE + +if TYPE_CHECKING: + from .lnchan import Channel, UpdateAddHtlc + + +def maybe_create_sweeptx_for_their_ctx_to_remote(ctx: Transaction, sweep_address: str, + our_payment_privkey: ecc.ECPrivkey) -> Optional[Transaction]: + our_payment_pubkey = our_payment_privkey.get_public_key_bytes(compressed=True) + to_remote_address = make_commitment_output_to_remote_address(our_payment_pubkey) + output_idx = ctx.get_output_idx_from_address(to_remote_address) + if output_idx is None: return None + sweep_tx = create_sweeptx_their_ctx_to_remote(sweep_address=sweep_address, + ctx=ctx, + output_idx=output_idx, + our_payment_privkey=our_payment_privkey) + return sweep_tx + + +def maybe_create_sweeptx_for_their_ctx_to_local(ctx: Transaction, revocation_privkey: bytes, + to_self_delay: int, delayed_pubkey: bytes, + sweep_address: str) -> Optional[EncumberedTransaction]: + revocation_pubkey = ecc.ECPrivkey(revocation_privkey).get_public_key_bytes(compressed=True) + witness_script = bh2u(make_commitment_output_to_local_witness_script( + revocation_pubkey, to_self_delay, delayed_pubkey)) + to_local_address = redeem_script_to_address('p2wsh', witness_script) + output_idx = ctx.get_output_idx_from_address(to_local_address) + if output_idx is None: return None + sweep_tx = create_sweeptx_ctx_to_local(sweep_address=sweep_address, + ctx=ctx, + output_idx=output_idx, + witness_script=witness_script, + privkey=revocation_privkey, + is_revocation=True) + if sweep_tx is None: return None + return EncumberedTransaction('their_ctx_to_local', sweep_tx, csv_delay=0, cltv_expiry=0) + + +def create_sweeptxs_for_their_just_revoked_ctx(chan: 'Channel', ctx: Transaction, per_commitment_secret: bytes, + sweep_address: str) -> List[Tuple[Optional[str],EncumberedTransaction]]: + """Presign sweeping transactions using the just received revoked pcs. + These will only be utilised if the remote breaches. + Sweep 'lo_local', and all the HTLCs (two cases: directly from ctx, or from HTLC tx). + """ + # prep + pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True) + this_conf, other_conf = get_ordered_channel_configs(chan=chan, for_us=False) + other_revocation_privkey = derive_blinded_privkey(other_conf.revocation_basepoint.privkey, + per_commitment_secret) + to_self_delay = other_conf.to_self_delay + this_delayed_pubkey = derive_pubkey(this_conf.delayed_basepoint.pubkey, pcp) + txs = [] + # to_local + sweep_tx = maybe_create_sweeptx_for_their_ctx_to_local(ctx=ctx, + revocation_privkey=other_revocation_privkey, + to_self_delay=to_self_delay, + delayed_pubkey=this_delayed_pubkey, + sweep_address=sweep_address) + if sweep_tx: + txs.append((None, EncumberedTransaction('their_ctx_to_local', sweep_tx, csv_delay=0, cltv_expiry=0))) + # HTLCs + def create_sweeptx_for_htlc(htlc: UpdateAddHtlc, is_received_htlc: bool) -> Tuple[Optional[Transaction], + Optional[Transaction], + Transaction]: + htlc_tx_witness_script, htlc_tx = make_htlc_tx_with_open_channel(chan=chan, + pcp=pcp, + for_us=False, + we_receive=not is_received_htlc, + commit=ctx, + htlc=htlc) + htlc_tx_txin = htlc_tx.inputs()[0] + htlc_output_witness_script = bfh(Transaction.get_preimage_script(htlc_tx_txin)) + # sweep directly from ctx + direct_sweep_tx = maybe_create_sweeptx_for_their_ctx_htlc( + ctx=ctx, + sweep_address=sweep_address, + htlc_output_witness_script=htlc_output_witness_script, + privkey=other_revocation_privkey, + preimage=None, + is_revocation=True) + # sweep from htlc tx + secondstage_sweep_tx = create_sweeptx_that_spends_htlctx_that_spends_htlc_in_ctx( + htlc_tx=htlc_tx, + htlctx_witness_script=htlc_tx_witness_script, + sweep_address=sweep_address, + privkey=other_revocation_privkey, + is_revocation=True) + return direct_sweep_tx, secondstage_sweep_tx, htlc_tx + # received HTLCs, in their ctx + # TODO consider carefully if "included_htlcs" is what we need here + received_htlcs = list(chan.included_htlcs(REMOTE, LOCAL)) # type: List[UpdateAddHtlc] + for htlc in received_htlcs: + direct_sweep_tx, secondstage_sweep_tx, htlc_tx = create_sweeptx_for_htlc(htlc, is_received_htlc=True) + if direct_sweep_tx: + txs.append((ctx.txid(), EncumberedTransaction(f'their_ctx_sweep_htlc_{bh2u(htlc.payment_hash)}', direct_sweep_tx, csv_delay=0, cltv_expiry=0))) + if secondstage_sweep_tx: + txs.append((htlc_tx.txid(), EncumberedTransaction(f'their_htlctx_{bh2u(htlc.payment_hash)}', secondstage_sweep_tx, csv_delay=0, cltv_expiry=0))) + # offered HTLCs, in their ctx + # TODO consider carefully if "included_htlcs" is what we need here + offered_htlcs = list(chan.included_htlcs(REMOTE, REMOTE)) # type: List[UpdateAddHtlc] + for htlc in offered_htlcs: + direct_sweep_tx, secondstage_sweep_tx, htlc_tx = create_sweeptx_for_htlc(htlc, is_received_htlc=False) + if direct_sweep_tx: + txs.append((ctx.txid(), EncumberedTransaction(f'their_ctx_sweep_htlc_{bh2u(htlc.payment_hash)}', direct_sweep_tx, csv_delay=0, cltv_expiry=0))) + if secondstage_sweep_tx: + txs.append((htlc_tx.txid(), EncumberedTransaction(f'their_htlctx_{bh2u(htlc.payment_hash)}', secondstage_sweep_tx, csv_delay=0, cltv_expiry=0))) + return txs + + +def create_sweeptxs_for_our_latest_ctx(chan: 'Channel', ctx: Transaction, + sweep_address: str) -> List[Tuple[Optional[str],EncumberedTransaction]]: + """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). + 'to_local' can be swept even if this is a breach (by us), + but HTLCs cannot (old HTLCs are no longer stored). + """ + this_conf, other_conf = get_ordered_channel_configs(chan=chan, for_us=True) + ctn = extract_ctn_from_tx_and_chan(ctx, chan) + our_per_commitment_secret = get_per_commitment_secret_from_seed( + this_conf.per_commitment_secret_seed, RevocationStore.START_INDEX - ctn) + our_pcp = ecc.ECPrivkey(our_per_commitment_secret).get_public_key_bytes(compressed=True) + # prep + this_delayed_bp_privkey = ecc.ECPrivkey(this_conf.delayed_basepoint.privkey) + this_localdelayed_privkey = derive_privkey(this_delayed_bp_privkey.secret_scalar, our_pcp) + this_localdelayed_privkey = ecc.ECPrivkey.from_secret_scalar(this_localdelayed_privkey) + other_revocation_pubkey = derive_blinded_pubkey(other_conf.revocation_basepoint.pubkey, our_pcp) + 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 = [] + # to_local + sweep_tx = maybe_create_sweeptx_that_spends_to_local_in_our_ctx(ctx=ctx, + sweep_address=sweep_address, + our_localdelayed_privkey=this_localdelayed_privkey, + remote_revocation_pubkey=other_revocation_pubkey, + to_self_delay=to_self_delay) + if sweep_tx: + txs.append((None, 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: + try: + preimage, invoice = chan.get_preimage_and_invoice(htlc.payment_hash) + except UnknownPaymentHash as e: + print_error(f'trying to sweep htlc from our latest ctx but getting {repr(e)}') + return None, None + else: + preimage = None + htlctx_witness_script, htlc_tx = create_htlctx_that_spends_from_our_ctx( + chan=chan, + our_pcp=our_pcp, + ctx=ctx, + htlc=htlc, + local_htlc_privkey=this_htlc_privkey, + preimage=preimage, + is_received_htlc=is_received_htlc) + to_wallet_tx = create_sweeptx_that_spends_htlctx_that_spends_htlc_in_ctx( + to_self_delay=to_self_delay, + htlc_tx=htlc_tx, + htlctx_witness_script=htlctx_witness_script, + sweep_address=sweep_address, + privkey=this_localdelayed_privkey.get_secret_bytes(), + is_revocation=False) + return htlc_tx, to_wallet_tx + # offered HTLCs, in our ctx --> "timeout" + # TODO consider carefully if "included_htlcs" is what we need here + offered_htlcs = list(chan.included_htlcs(LOCAL, LOCAL)) # type: List[UpdateAddHtlc] + 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((htlc_tx.txid(), EncumberedTransaction(f'second_stage_to_wallet_{bh2u(htlc.payment_hash)}', to_wallet_tx, csv_delay=to_self_delay, cltv_expiry=0))) + txs.append((ctx.txid(), 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((htlc_tx.txid(), EncumberedTransaction(f'second_stage_to_wallet_{bh2u(htlc.payment_hash)}', to_wallet_tx, csv_delay=to_self_delay, cltv_expiry=0))) + txs.append((ctx.txid(), EncumberedTransaction(f'our_ctx_htlc_tx_{bh2u(htlc.payment_hash)}', htlc_tx, csv_delay=0, cltv_expiry=0))) + return txs + + +def create_sweeptxs_for_their_latest_ctx(chan: 'Channel', ctx: Transaction, + sweep_address: str) -> List[Tuple[Optional[str],EncumberedTransaction]]: + """Handle the case when the remote force-closes with their ctx. + Regardless of it is a breach or not, construct sweep tx for 'to_remote'. + If it is a breach, also construct sweep tx for 'to_local'. + Sweep txns for HTLCs are only constructed if it is NOT a breach, as + lnchan does not store old HTLCs. + """ + this_conf, other_conf = get_ordered_channel_configs(chan=chan, for_us=False) + ctn = extract_ctn_from_tx_and_chan(ctx, chan) + # note: the remote sometimes has two valid non-revoked commitment transactions, + # either of which could be broadcast (this_conf.ctn, this_conf.ctn+1) + per_commitment_secret = None + if ctn == this_conf.ctn: + their_pcp = this_conf.current_per_commitment_point + elif ctn == this_conf.ctn + 1: + their_pcp = this_conf.next_per_commitment_point + elif ctn < this_conf.ctn: # breach + try: + per_commitment_secret = this_conf.revocation_store.retrieve_secret(RevocationStore.START_INDEX - ctn) + except UnableToDeriveSecret: + return [] + their_pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True) + else: + return [] + # prep + other_revocation_pubkey = derive_blinded_pubkey(other_conf.revocation_basepoint.pubkey, their_pcp) + other_htlc_privkey = derive_privkey(secret=int.from_bytes(other_conf.htlc_basepoint.privkey, 'big'), + per_commitment_point=their_pcp) + other_htlc_privkey = ecc.ECPrivkey.from_secret_scalar(other_htlc_privkey) + this_htlc_pubkey = derive_pubkey(this_conf.htlc_basepoint.pubkey, their_pcp) + other_payment_bp_privkey = ecc.ECPrivkey(other_conf.payment_basepoint.privkey) + 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 = [] + if per_commitment_secret: # breach + # to_local + other_revocation_privkey = derive_blinded_privkey(other_conf.revocation_basepoint.privkey, + per_commitment_secret) + this_delayed_pubkey = derive_pubkey(this_conf.delayed_basepoint.pubkey, their_pcp) + sweep_tx = maybe_create_sweeptx_for_their_ctx_to_local(ctx=ctx, + revocation_privkey=other_revocation_privkey, + to_self_delay=other_conf.to_self_delay, + delayed_pubkey=this_delayed_pubkey, + sweep_address=sweep_address) + if sweep_tx: + txs.append((None, EncumberedTransaction('their_ctx_to_local', sweep_tx, csv_delay=0, cltv_expiry=0))) + # to_remote + sweep_tx = maybe_create_sweeptx_for_their_ctx_to_remote(ctx=ctx, + sweep_address=sweep_address, + our_payment_privkey=other_payment_privkey) + if sweep_tx: + txs.append((None, 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 + # to handle the breach already; out of scope here) + if ctn not in (this_conf.ctn, this_conf.ctn + 1): + return txs + def create_sweeptx_for_htlc(htlc: UpdateAddHtlc, is_received_htlc: bool) -> Optional[Transaction]: + if not is_received_htlc: + try: + preimage, invoice = chan.get_preimage_and_invoice(htlc.payment_hash) + except UnknownPaymentHash as e: + print_error(f'trying to sweep htlc from their latest ctx but getting {repr(e)}') + return None + else: + preimage = None + htlc_output_witness_script = make_htlc_output_witness_script( + is_received_htlc=is_received_htlc, + remote_revocation_pubkey=other_revocation_pubkey, + remote_htlc_pubkey=other_htlc_privkey.get_public_key_bytes(compressed=True), + local_htlc_pubkey=this_htlc_pubkey, + payment_hash=htlc.payment_hash, + cltv_expiry=htlc.cltv_expiry) + sweep_tx = maybe_create_sweeptx_for_their_ctx_htlc( + ctx=ctx, + sweep_address=sweep_address, + htlc_output_witness_script=htlc_output_witness_script, + privkey=other_htlc_privkey.get_secret_bytes(), + preimage=preimage, + is_revocation=False) + return sweep_tx + # received HTLCs, in their ctx --> "timeout" + received_htlcs = chan.included_htlcs_in_their_latest_ctxs(LOCAL)[ctn] # type: List[UpdateAddHtlc] + for htlc in received_htlcs: + sweep_tx = create_sweeptx_for_htlc(htlc, is_received_htlc=True) + if sweep_tx: + txs.append((ctx.txid(), 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((ctx.txid(), EncumberedTransaction(f'their_ctx_sweep_htlc_{bh2u(htlc.payment_hash)}', sweep_tx, csv_delay=0, cltv_expiry=0))) + return txs + + +def maybe_create_sweeptx_that_spends_to_local_in_our_ctx( + ctx: Transaction, sweep_address: str, our_localdelayed_privkey: ecc.ECPrivkey, + remote_revocation_pubkey: bytes, to_self_delay: int) -> Optional[Transaction]: + our_localdelayed_pubkey = our_localdelayed_privkey.get_public_key_bytes(compressed=True) + to_local_witness_script = bh2u(make_commitment_output_to_local_witness_script( + remote_revocation_pubkey, to_self_delay, our_localdelayed_pubkey)) + to_local_address = redeem_script_to_address('p2wsh', to_local_witness_script) + output_idx = ctx.get_output_idx_from_address(to_local_address) + if output_idx is None: return None + sweep_tx = create_sweeptx_ctx_to_local(sweep_address=sweep_address, + ctx=ctx, + output_idx=output_idx, + witness_script=to_local_witness_script, + privkey=our_localdelayed_privkey.get_secret_bytes(), + is_revocation=False, + to_self_delay=to_self_delay) + if sweep_tx is None: return None + return sweep_tx + + +def create_htlctx_that_spends_from_our_ctx(chan: 'Channel', our_pcp: bytes, + ctx: Transaction, htlc: 'UpdateAddHtlc', + local_htlc_privkey: bytes, preimage: Optional[bytes], + is_received_htlc: bool) -> Tuple[bytes, Transaction]: + assert is_received_htlc == bool(preimage), 'preimage is required iff htlc is received' + preimage = preimage or b'' + witness_script, htlc_tx = make_htlc_tx_with_open_channel(chan=chan, + pcp=our_pcp, + for_us=True, + we_receive=is_received_htlc, + commit=ctx, + htlc=htlc) + remote_htlc_sig = chan.get_remote_htlc_sig_for_htlc(htlc, we_receive=is_received_htlc) + local_htlc_sig = bfh(htlc_tx.sign_txin(0, local_htlc_privkey)) + txin = htlc_tx.inputs()[0] + witness_program = bfh(Transaction.get_preimage_script(txin)) + txin['witness'] = bh2u(make_htlc_tx_witness(remote_htlc_sig, local_htlc_sig, preimage, witness_program)) + return witness_script, htlc_tx + + +def maybe_create_sweeptx_for_their_ctx_htlc(ctx: Transaction, sweep_address: str, + htlc_output_witness_script: bytes, + privkey: bytes, is_revocation: bool, + preimage: Optional[bytes]) -> Optional[Transaction]: + htlc_address = redeem_script_to_address('p2wsh', bh2u(htlc_output_witness_script)) + # FIXME handle htlc_address collision + # also: https://github.com/lightningnetwork/lightning-rfc/issues/448 + output_idx = ctx.get_output_idx_from_address(htlc_address) + if output_idx is None: return None + sweep_tx = create_sweeptx_their_ctx_htlc(ctx=ctx, + witness_script=htlc_output_witness_script, + sweep_address=sweep_address, + preimage=preimage, + output_idx=output_idx, + privkey=privkey, + is_revocation=is_revocation) + return sweep_tx + + +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, + fee_per_kb: int=None) -> Optional[Transaction]: + preimage = preimage or b'' # preimage is required iff (not is_revocation and htlc is offered) + val = ctx.outputs()[output_idx].value + sweep_inputs = [{ + 'scriptSig': '', + 'type': 'p2wsh', + 'signatures': [], + 'num_sig': 0, + 'prevout_n': output_idx, + 'prevout_hash': ctx.txid(), + 'value': val, + 'coinbase': False, + 'preimage_script': bh2u(witness_script), + }] + tx_size_bytes = 200 # TODO (depends on offered/received and is_revocation) + if fee_per_kb is None: fee_per_kb = FEERATE_FALLBACK_STATIC_FEE + fee = SimpleConfig.estimate_fee_for_feerate(fee_per_kb, tx_size_bytes) + outvalue = val - fee + if outvalue <= dust_threshold(): return None + sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)] + tx = Transaction.from_io(sweep_inputs, sweep_outputs, version=2) + + sig = bfh(tx.sign_txin(0, privkey)) + if not is_revocation: + witness = construct_witness([sig, preimage, witness_script]) + else: + revocation_pubkey = privkey_to_pubkey(privkey) + witness = construct_witness([sig, revocation_pubkey, witness_script]) + tx.inputs()[0]['witness'] = witness + assert tx.is_complete() + return tx + + +def create_sweeptx_their_ctx_to_remote(sweep_address: str, ctx: Transaction, output_idx: int, + our_payment_privkey: ecc.ECPrivkey, + fee_per_kb: int=None) -> Optional[Transaction]: + our_payment_pubkey = our_payment_privkey.get_public_key_hex(compressed=True) + val = ctx.outputs()[output_idx].value + sweep_inputs = [{ + 'type': 'p2wpkh', + 'x_pubkeys': [our_payment_pubkey], + 'num_sig': 1, + 'prevout_n': output_idx, + 'prevout_hash': ctx.txid(), + 'value': val, + 'coinbase': False, + 'signatures': [None], + }] + tx_size_bytes = 110 # approx size of p2wpkh->p2wpkh + if fee_per_kb is None: fee_per_kb = FEERATE_FALLBACK_STATIC_FEE + fee = SimpleConfig.estimate_fee_for_feerate(fee_per_kb, tx_size_bytes) + outvalue = val - fee + if outvalue <= dust_threshold(): return None + sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)] + sweep_tx = Transaction.from_io(sweep_inputs, sweep_outputs) + sweep_tx.set_rbf(True) + sweep_tx.sign({our_payment_pubkey: (our_payment_privkey.get_secret_bytes(), True)}) + if not sweep_tx.is_complete(): + raise Exception('channel close sweep tx is not complete') + return sweep_tx + + +def create_sweeptx_ctx_to_local(sweep_address: str, ctx: Transaction, output_idx: int, witness_script: str, + privkey: bytes, is_revocation: bool, + to_self_delay: int=None, + fee_per_kb: int=None) -> Optional[Transaction]: + """Create a txn that sweeps the 'to_local' output of a commitment + transaction into our wallet. + + privkey: either revocation_privkey or localdelayed_privkey + is_revocation: tells us which ^ + """ + val = ctx.outputs()[output_idx].value + sweep_inputs = [{ + 'scriptSig': '', + 'type': 'p2wsh', + 'signatures': [], + 'num_sig': 0, + 'prevout_n': output_idx, + 'prevout_hash': ctx.txid(), + 'value': val, + 'coinbase': False, + 'preimage_script': witness_script, + }] + if not is_revocation: + assert isinstance(to_self_delay, int) + sweep_inputs[0]['sequence'] = to_self_delay + tx_size_bytes = 121 # approx size of to_local -> p2wpkh + if fee_per_kb is None: fee_per_kb = FEERATE_FALLBACK_STATIC_FEE + fee = SimpleConfig.estimate_fee_for_feerate(fee_per_kb, tx_size_bytes) + outvalue = val - fee + if outvalue <= dust_threshold(): return None + sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)] + sweep_tx = Transaction.from_io(sweep_inputs, sweep_outputs, version=2) + sig = sweep_tx.sign_txin(0, privkey) + witness = construct_witness([sig, int(is_revocation), witness_script]) + sweep_tx.inputs()[0]['witness'] = witness + return sweep_tx + + +def create_sweeptx_that_spends_htlctx_that_spends_htlc_in_ctx( + htlc_tx: Transaction, htlctx_witness_script: bytes, sweep_address: str, + privkey: bytes, is_revocation: bool, to_self_delay: int=None, + fee_per_kb: int=None) -> Optional[Transaction]: + val = htlc_tx.outputs()[0].value + sweep_inputs = [{ + 'scriptSig': '', + 'type': 'p2wsh', + 'signatures': [], + 'num_sig': 0, + 'prevout_n': 0, + 'prevout_hash': htlc_tx.txid(), + 'value': val, + 'coinbase': False, + 'preimage_script': bh2u(htlctx_witness_script), + }] + if not is_revocation: + assert isinstance(to_self_delay, int) + sweep_inputs[0]['sequence'] = to_self_delay + tx_size_bytes = 200 # TODO + if fee_per_kb is None: fee_per_kb = FEERATE_FALLBACK_STATIC_FEE + fee = SimpleConfig.estimate_fee_for_feerate(fee_per_kb, tx_size_bytes) + outvalue = val - fee + if outvalue <= dust_threshold(): return None + sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)] + tx = Transaction.from_io(sweep_inputs, sweep_outputs, version=2) + + sig = bfh(tx.sign_txin(0, privkey)) + witness = construct_witness([sig, int(is_revocation), htlctx_witness_script]) + tx.inputs()[0]['witness'] = witness + assert tx.is_complete() + return tx diff --git a/electrum/lnutil.py b/electrum/lnutil.py @@ -5,7 +5,7 @@ from enum import IntFlag, IntEnum import json from collections import namedtuple -from typing import NamedTuple, List, Tuple, Mapping, Optional +from typing import NamedTuple, List, Tuple, Mapping, Optional, TYPE_CHECKING, Union import re from .util import bfh, bh2u, inv_dict @@ -13,13 +13,16 @@ from .crypto import sha256 from .transaction import Transaction from .ecc import CURVE_ORDER, sig_string_from_der_sig, ECPubkey, string_to_number from . import ecc, bitcoin, crypto, transaction -from .transaction import opcodes, TxOutput -from .bitcoin import push_script +from .transaction import opcodes, TxOutput, Transaction +from .bitcoin import push_script, redeem_script_to_address, TYPE_ADDRESS from . import segwit_addr from .i18n import _ from .lnaddr import lndecode from .keystore import BIP32_KeyStore +if TYPE_CHECKING: + from .lnchan import Channel, UpdateAddHtlc + HTLC_TIMEOUT_WEIGHT = 663 HTLC_SUCCESS_WEIGHT = 703 @@ -238,18 +241,18 @@ def make_htlc_tx_output(amount_msat, local_feerate, revocationpubkey, local_dela output = TxOutput(bitcoin.TYPE_ADDRESS, p2wsh, final_amount_sat) return script, output -def make_htlc_tx_witness(remotehtlcsig, localhtlcsig, payment_preimage, witness_script): +def make_htlc_tx_witness(remotehtlcsig: bytes, localhtlcsig: bytes, + payment_preimage: bytes, witness_script: bytes) -> bytes: assert type(remotehtlcsig) is bytes assert type(localhtlcsig) is bytes assert type(payment_preimage) is bytes assert type(witness_script) is bytes return bfh(transaction.construct_witness([0, remotehtlcsig, localhtlcsig, payment_preimage, witness_script])) -def make_htlc_tx_inputs(htlc_output_txid, htlc_output_index, revocationpubkey, local_delayedpubkey, amount_msat, witness_script): +def make_htlc_tx_inputs(htlc_output_txid: str, htlc_output_index: int, + amount_msat: int, witness_script: str) -> List[dict]: assert type(htlc_output_txid) is str assert type(htlc_output_index) is int - assert type(revocationpubkey) is bytes - assert type(local_delayedpubkey) is bytes assert type(amount_msat) is int assert type(witness_script) is str c_inputs = [{ @@ -272,7 +275,8 @@ def make_htlc_tx(cltv_timeout, inputs, output): tx = Transaction.from_io(inputs, c_outputs, locktime=cltv_timeout, version=2) return tx -def make_offered_htlc(revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, payment_hash): +def make_offered_htlc(revocation_pubkey: bytes, remote_htlcpubkey: bytes, + local_htlcpubkey: bytes, payment_hash: bytes) -> bytes: assert type(revocation_pubkey) is bytes assert type(remote_htlcpubkey) is bytes assert type(local_htlcpubkey) is bytes @@ -285,7 +289,8 @@ def make_offered_htlc(revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, pa + bytes([opcodes.OP_CHECKMULTISIG, opcodes.OP_ELSE, opcodes.OP_HASH160])\ + bfh(push_script(bh2u(crypto.ripemd(payment_hash)))) + bytes([opcodes.OP_EQUALVERIFY, opcodes.OP_CHECKSIG, opcodes.OP_ENDIF, opcodes.OP_ENDIF]) -def make_received_htlc(revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, payment_hash, cltv_expiry): +def make_received_htlc(revocation_pubkey: bytes, remote_htlcpubkey: bytes, + local_htlcpubkey: bytes, payment_hash: bytes, cltv_expiry: int) -> bytes: for i in [revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, payment_hash]: assert type(i) is bytes assert type(cltv_expiry) is int @@ -307,12 +312,34 @@ def make_received_htlc(revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, p + bitcoin.add_number_to_script(cltv_expiry) \ + bytes([opcodes.OP_CLTV, opcodes.OP_DROP, opcodes.OP_CHECKSIG, opcodes.OP_ENDIF, opcodes.OP_ENDIF]) -def make_htlc_tx_with_open_channel(chan, pcp, for_us, we_receive, commit, htlc): - amount_msat, cltv_expiry, payment_hash = htlc.amount_msat, htlc.cltv_expiry, htlc.payment_hash +def make_htlc_output_witness_script(is_received_htlc: bool, remote_revocation_pubkey: bytes, remote_htlc_pubkey: bytes, + local_htlc_pubkey: bytes, payment_hash: bytes, cltv_expiry: Optional[int]) -> bytes: + if is_received_htlc: + return make_received_htlc(revocation_pubkey=remote_revocation_pubkey, + remote_htlcpubkey=remote_htlc_pubkey, + local_htlcpubkey=local_htlc_pubkey, + payment_hash=payment_hash, + cltv_expiry=cltv_expiry) + else: + return make_offered_htlc(revocation_pubkey=remote_revocation_pubkey, + remote_htlcpubkey=remote_htlc_pubkey, + local_htlcpubkey=local_htlc_pubkey, + payment_hash=payment_hash) + + +def get_ordered_channel_configs(chan: 'Channel', for_us: bool) -> Tuple[Union[LocalConfig, RemoteConfig], + Union[LocalConfig, RemoteConfig]]: conf = chan.config[LOCAL] if for_us else chan.config[REMOTE] other_conf = chan.config[LOCAL] if not for_us else chan.config[REMOTE] + return conf, other_conf + + +def make_htlc_tx_with_open_channel(chan: 'Channel', pcp: bytes, for_us: bool, + we_receive: bool, commit: Transaction, + htlc: 'UpdateAddHtlc') -> Tuple[bytes, Transaction]: + amount_msat, cltv_expiry, payment_hash = htlc.amount_msat, htlc.cltv_expiry, htlc.payment_hash + conf, other_conf = get_ordered_channel_configs(chan=chan, for_us=for_us) - revocation_pubkey = derive_blinded_pubkey(other_conf.revocation_basepoint.pubkey, pcp) delayedpubkey = derive_pubkey(conf.delayed_basepoint.pubkey, pcp) other_revocation_pubkey = derive_blinded_pubkey(other_conf.revocation_basepoint.pubkey, pcp) other_htlc_pubkey = derive_pubkey(other_conf.htlc_basepoint.pubkey, pcp) @@ -323,19 +350,23 @@ def make_htlc_tx_with_open_channel(chan, pcp, for_us, we_receive, commit, htlc): script, htlc_tx_output = make_htlc_tx_output( amount_msat = amount_msat, local_feerate = chan.pending_feerate(LOCAL if for_us else REMOTE), - revocationpubkey=revocation_pubkey, + revocationpubkey=other_revocation_pubkey, local_delayedpubkey=delayedpubkey, success = is_htlc_success, to_self_delay = other_conf.to_self_delay) - if is_htlc_success: - preimage_script = make_received_htlc(other_revocation_pubkey, other_htlc_pubkey, htlc_pubkey, payment_hash, cltv_expiry) - else: - preimage_script = make_offered_htlc(other_revocation_pubkey, other_htlc_pubkey, htlc_pubkey, payment_hash) - output_idx = commit.htlc_output_indices[htlc.payment_hash] + preimage_script = make_htlc_output_witness_script(is_received_htlc=is_htlc_success, + remote_revocation_pubkey=other_revocation_pubkey, + remote_htlc_pubkey=other_htlc_pubkey, + local_htlc_pubkey=htlc_pubkey, + payment_hash=payment_hash, + cltv_expiry=cltv_expiry) + htlc_address = redeem_script_to_address('p2wsh', bh2u(preimage_script)) + # FIXME handle htlc_address collision + # also: https://github.com/lightningnetwork/lightning-rfc/issues/448 + prevout_idx = commit.get_output_idx_from_address(htlc_address) + assert prevout_idx is not None htlc_tx_inputs = make_htlc_tx_inputs( - commit.txid(), output_idx, - revocationpubkey=revocation_pubkey, - local_delayedpubkey=delayedpubkey, + commit.txid(), prevout_idx, amount_msat=amount_msat, witness_script=bh2u(preimage_script)) if is_htlc_success: @@ -401,7 +432,7 @@ def make_commitment(ctn, local_funding_pubkey, remote_funding_pubkey, delayed_pubkey, to_self_delay, funding_txid, funding_pos, funding_sat, local_amount, remote_amount, dust_limit_sat, fees_per_participant, - htlcs): + htlcs: List[ScriptHtlc]) -> Transaction: c_input = make_funding_input(local_funding_pubkey, remote_funding_pubkey, funding_pos, funding_txid, funding_sat) obs = get_obscured_ctn(ctn, funder_payment_basepoint, fundee_payment_basepoint) @@ -423,15 +454,6 @@ def make_commitment(ctn, local_funding_pubkey, remote_funding_pubkey, # create commitment tx tx = Transaction.from_io(c_inputs, c_outputs_filtered, locktime=locktime, version=2) - - tx.htlc_output_indices = {} - assert len(htlcs) == len(htlc_outputs) - for script_htlc, output in zip(htlcs, htlc_outputs): - if output in tx.outputs(): - # minus the first two outputs (to_local, to_remote) - assert script_htlc.htlc.payment_hash not in tx.htlc_output_indices - tx.htlc_output_indices[script_htlc.htlc.payment_hash] = tx.outputs().index(output) - return tx def make_commitment_output_to_local_witness_script( @@ -487,7 +509,7 @@ def extract_ctn_from_tx(tx, txin_index: int, funder_payment_basepoint: bytes, obs = ((sequence & 0xffffff) << 24) + (locktime & 0xffffff) return get_obscured_ctn(obs, funder_payment_basepoint, fundee_payment_basepoint) -def extract_ctn_from_tx_and_chan(tx, chan) -> int: +def extract_ctn_from_tx_and_chan(tx: Transaction, chan: 'Channel') -> int: funder_conf = chan.config[LOCAL] if chan.constraints.is_initiator else chan.config[REMOTE] fundee_conf = chan.config[LOCAL] if not chan.constraints.is_initiator else chan.config[REMOTE] return extract_ctn_from_tx(tx, txin_index=0, diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py @@ -36,9 +36,6 @@ class TxMinedDepth(IntEnum): class LNWatcher(AddressSynchronizer): - # TODO if verifier gets an incorrect merkle proof, that tx will never verify!! - # similarly, what if server ignores request for merkle proof? - # maybe we should disconnect from server in these cases verbosity_filter = 'W' def __init__(self, network: 'Network'): @@ -181,6 +178,7 @@ class LNWatcher(AddressSynchronizer): if self.get_tx_mined_depth(prev_txid) == TxMinedDepth.DEEP: self.print_error('have no follow-up transactions and prevtx', prev_txid, 'mined deep, returning') return False + return True # check if any response applies keep_watching_this = False local_height = self.network.get_local_height() @@ -241,7 +239,7 @@ class LNWatcher(AddressSynchronizer): def get_tx_mined_depth(self, txid: str): if not txid: - return TxMinedStatus.FREE + return TxMinedDepth.FREE tx_mined_depth = self.get_tx_height(txid) height, conf = tx_mined_depth.height, tx_mined_depth.conf if conf > 100: diff --git a/electrum/lnworker.py b/electrum/lnworker.py @@ -531,6 +531,8 @@ class LNWorker(PrintError): return routing_hints def delete_invoice(self, payment_hash_hex: str): + # FIXME we will now LOSE the preimage!! is this feature a good idea? + # maybe instead of deleting, we could have a feature to "hide" invoices (e.g. for GUI) try: del self.invoices[payment_hash_hex] except KeyError: diff --git a/electrum/tests/test_lnchan.py b/electrum/tests/test_lnchan.py @@ -199,10 +199,10 @@ class TestChannel(unittest.TestCase): alice_channel, bob_channel = self.alice_channel, self.bob_channel htlc = self.htlc - ctn_to_htlcs = alice_channel.included_htlcs_in_latest_ctxs() - self.assertEqual(list(ctn_to_htlcs.keys()), [0,1]) - self.assertEqual(ctn_to_htlcs[0], []) - self.assertEqual(ctn_to_htlcs[1], [htlc]) + self.assertEqual({0: [], 1: [htlc]}, alice_channel.included_htlcs_in_their_latest_ctxs(LOCAL)) + self.assertEqual({0: [], 1: []}, bob_channel.included_htlcs_in_their_latest_ctxs(REMOTE)) + self.assertEqual({0: [], 1: []}, alice_channel.included_htlcs_in_their_latest_ctxs(REMOTE)) + self.assertEqual({0: [], 1: []}, bob_channel.included_htlcs_in_their_latest_ctxs(LOCAL)) # Next alice commits this change by sending a signature message. Since # we expect the messages to be ordered, Bob will receive the HTLC we @@ -217,6 +217,11 @@ class TestChannel(unittest.TestCase): # from Alice. bob_channel.receive_new_commitment(aliceSig, aliceHtlcSigs) + self.assertEqual({0: [], 1: [htlc]}, alice_channel.included_htlcs_in_their_latest_ctxs(LOCAL)) + self.assertEqual({0: [], 1: [htlc]}, bob_channel.included_htlcs_in_their_latest_ctxs(REMOTE)) + self.assertEqual({0: [], 1: []}, alice_channel.included_htlcs_in_their_latest_ctxs(REMOTE)) + self.assertEqual({0: [], 1: []}, bob_channel.included_htlcs_in_their_latest_ctxs(LOCAL)) + # Bob revokes his prior commitment given to him by Alice, since he now # has a valid signature for a newer commitment. bobRevocation, _ = bob_channel.revoke_current_commitment() @@ -279,10 +284,10 @@ class TestChannel(unittest.TestCase): bobSig2, bobHtlcSigs2 = bob_channel.sign_next_commitment() - ctn_to_htlcs = bob_channel.included_htlcs_in_latest_ctxs() - self.assertEqual(list(ctn_to_htlcs.keys()), [1,2]) - self.assertEqual(len(ctn_to_htlcs[1]), 1) - self.assertEqual(len(ctn_to_htlcs[2]), 0) + self.assertEqual({1: [htlc], 2: []}, alice_channel.included_htlcs_in_their_latest_ctxs(LOCAL)) + self.assertEqual({1: [htlc], 2: []}, bob_channel.included_htlcs_in_their_latest_ctxs(REMOTE)) + self.assertEqual({1: [], 2: []}, alice_channel.included_htlcs_in_their_latest_ctxs(REMOTE)) + self.assertEqual({1: [], 2: []}, bob_channel.included_htlcs_in_their_latest_ctxs(LOCAL)) alice_channel.receive_new_commitment(bobSig2, bobHtlcSigs2) diff --git a/electrum/tests/test_lnutil.py b/electrum/tests/test_lnutil.py @@ -552,8 +552,6 @@ class TestLNUtil(unittest.TestCase): our_htlc_tx_inputs = make_htlc_tx_inputs( htlc_output_txid=our_commit_tx.txid(), htlc_output_index=htlc_output_index, - revocationpubkey=local_revocation_pubkey, - local_delayedpubkey=local_delayedpubkey, amount_msat=amount_msat, witness_script=bh2u(htlc)) our_htlc_tx = make_htlc_tx(cltv_timeout, diff --git a/electrum/transaction.py b/electrum/transaction.py @@ -1190,6 +1190,28 @@ class Transaction: return (addr in (o.address for o in self.outputs())) \ or (addr in (txin.get("address") for txin in self.inputs())) + def get_output_idx_from_scriptpubkey(self, script: str) -> Optional[int]: + """Returns the index of an output with given script. + If there are no such outputs, returns None; + if there are multiple, returns one of them. + """ + assert isinstance(script, str) # hex + # build cache if there isn't one yet + # note: can become stale and return incorrect data + # if the tx is modified later; that's out of scope. + if not hasattr(self, '_script_to_output_idx'): + d = {} + for output_idx, o in enumerate(self.outputs()): + o_script = self.pay_script(o.type, o.address) + assert isinstance(o_script, str) + d[o_script] = output_idx + self._script_to_output_idx = d + return self._script_to_output_idx.get(script) + + def get_output_idx_from_address(self, addr: str) -> Optional: + script = bitcoin.address_to_script(addr) + return self.get_output_idx_from_scriptpubkey(script) + def as_dict(self): if self.raw is None: self.raw = self.serialize()