tredeem htlc outputs of our local commitment transaction back to wallet - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 1f97a9753ec14e568b179874d874f46af70b85d4 DIR parent f70e679ababa033ccd99840d6149f9d72fef7f6f HTML Author: Janus <ysangkok@gmail.com> Date: Mon, 22 Oct 2018 18:57:51 +0200 redeem htlc outputs of our local commitment transaction back to wallet Diffstat: M electrum/lnbase.py | 6 +----- M electrum/lnchan.py | 134 +++++++++++++++++++++++-------- M electrum/lnutil.py | 18 +++++++++++------- M electrum/lnwatcher.py | 78 ++++++++++++++++++++----------- 4 files changed, 162 insertions(+), 74 deletions(-) --- DIR diff --git a/electrum/lnbase.py b/electrum/lnbase.py t@@ -939,6 +939,7 @@ class Peer(PrintError): # create onion packet final_cltv = self.network.get_local_height() + min_final_cltv_expiry hops_data, amount_msat, cltv = calc_hops_data_for_payment(route, amount_msat, final_cltv) + assert final_cltv <= cltv, (final_cltv, cltv) secret_key = os.urandom(32) onion = new_onion_packet([x.node_id for x in route], secret_key, hops_data, associated_data=payment_hash) chan.check_can_pay(amount_msat) t@@ -973,11 +974,6 @@ class Peer(PrintError): def on_commitment_signed(self, payload): self.print_error("commitment_signed", payload) channel_id = payload['channel_id'] - chan = self.channels[channel_id] - chan.config[LOCAL]=chan.config[LOCAL]._replace( - current_commitment_signature=payload['signature'], - current_htlc_signatures=payload['htlc_signature']) - self.lnworker.save_channel(chan) self.commitment_signed[channel_id].put_nowait(payload) @log_exceptions DIR diff --git a/electrum/lnchan.py b/electrum/lnchan.py t@@ -3,7 +3,7 @@ from collections import namedtuple, defaultdict import binascii import json from enum import Enum, auto -from typing import Optional, Mapping, List +from typing import Optional, Dict, List, Tuple from .util import bfh, PrintError, bh2u from .bitcoin import Hash, TYPE_SCRIPT, TYPE_ADDRESS t@@ -14,7 +14,7 @@ from .lnutil import Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeyp 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 +from .lnutil import sign_and_get_sig_string, privkey_to_pubkey, make_htlc_tx_witness 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_outputs t@@ -129,8 +129,8 @@ class Channel(PrintError): self.remote_commitment_to_be_revoked = Transaction(state["remote_commitment_to_be_revoked"]) template = lambda: { - 'adds': {}, # type: Mapping[HTLC_ID, UpdateAddHtlc] - 'settles': [], # type: List[HTLC_ID] + 'adds': {}, # Dict[HTLC_ID, UpdateAddHtlc] + 'settles': [], # List[HTLC_ID] } self.log = {LOCAL: template(), REMOTE: template()} for strname, subject in [('remote_log', REMOTE), ('local_log', LOCAL)]: t@@ -244,7 +244,7 @@ class Channel(PrintError): 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] - htlc_tx = make_htlc_tx_with_open_channel(self, *args) + _script, htlc_tx = make_htlc_tx_with_open_channel(self, *args) 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)) t@@ -286,22 +286,20 @@ class Channel(PrintError): if not ecc.verify_signature(self.config[REMOTE].multisig_key.pubkey, sig, pre_hash): raise Exception('failed verifying signature of our updated commitment transaction: ' + bh2u(sig) + ' preimage is ' + preimage_hex) - _, this_point, _ = self.points + htlc_sigs_string = b''.join(htlc_sigs) + htlc_sigs = htlc_sigs[:] # copy cause we will delete now for htlcs, we_receive in [(self.included_htlcs(LOCAL, REMOTE), True), (self.included_htlcs(LOCAL, LOCAL), False)]: for htlc in htlcs: - htlc_tx = make_htlc_tx_with_open_channel(self, this_point, True, we_receive, pending_local_commitment, htlc) - pre_hash = Hash(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): - if ecc.verify_signature(remote_htlc_pubkey, sig, pre_hash): - del htlc_sigs[idx] - break - else: - raise Exception(f'failed verifying HTLC signatures: {htlc}') + idx = self.verify_htlc(htlc, htlc_sigs, we_receive) + del htlc_sigs[idx] if len(htlc_sigs) != 0: # all sigs should have been popped above raise Exception('failed verifying HTLC signatures: invalid amount of correct signatures') + self.config[LOCAL]=self.config[LOCAL]._replace( + current_commitment_signature=sig, + current_htlc_signatures=htlc_sigs_string) + for pending_fee in self.fee_mgr: if not self.constraints.is_initiator: pending_fee[FUNDEE_SIGNED] = True t@@ -310,6 +308,16 @@ class Channel(PrintError): 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_local_commitment, htlc) + pre_hash = Hash(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): + if ecc.verify_signature(remote_htlc_pubkey, sig, pre_hash): + return idx + else: + raise Exception(f'failed verifying HTLC signatures: {htlc}') def revoke_current_commitment(self): """ t@@ -372,12 +380,14 @@ class Channel(PrintError): 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_sweeptx = maybe_create_sweeptx_for_our_ctx_to_local(self, ctx, our_cur_pcp, self.sweep_address) + 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_sweeptx = maybe_create_sweeptx_for_their_ctx_to_remote(self, ctx, their_cur_pcp, self.sweep_address) - if encumbered_sweeptx: - self.lnwatcher.add_sweep_tx(outpoint, ctx.txid(), encumbered_sweeptx.to_json()) + encumbered_sweeptxs = [(None, maybe_create_sweeptx_for_their_ctx_to_remote(self, ctx, their_cur_pcp, self.sweep_address))] + for prev_txid, encumbered_tx in encumbered_sweeptxs: + if prev_txid is None: + prev_txid = ctx.txid() + 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: t@@ -739,7 +749,7 @@ def maybe_create_sweeptx_for_their_ctx_to_remote(chan, ctx, their_pcp: bytes, ctx=ctx, output_idx=output_idx, our_payment_privkey=our_payment_privkey) - return EncumberedTransaction(sweep_tx, csv_delay=0) + 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, t@@ -766,11 +776,11 @@ def maybe_create_sweeptx_for_their_ctx_to_local(chan, ctx, per_commitment_secret witness_script=witness_script, privkey=revocation_privkey, is_revocation=True) - return EncumberedTransaction(sweep_tx, csv_delay=0) + return EncumberedTransaction('their_ctx_to_local', sweep_tx, csv_delay=0, cltv_expiry=0) -def maybe_create_sweeptx_for_our_ctx_to_local(chan, ctx, our_pcp: bytes, - sweep_address) -> Optional[EncumberedTransaction]: +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) t@@ -782,21 +792,77 @@ def maybe_create_sweeptx_for_our_ctx_to_local(chan, ctx, our_pcp: bytes, 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 - else: - return None - 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) - - return EncumberedTransaction(sweep_tx, csv_delay=to_self_delay) + # 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: DIR diff --git a/electrum/lnutil.py b/electrum/lnutil.py t@@ -173,8 +173,8 @@ def derive_pubkey(basepoint: bytes, per_commitment_point: bytes) -> bytes: def derive_privkey(secret: int, per_commitment_point: bytes) -> int: assert type(secret) is int - basepoint = secret_to_pubkey(secret) - basepoint = secret + ecc.string_to_number(sha256(per_commitment_point + basepoint)) + basepoint_bytes = secret_to_pubkey(secret) + basepoint = secret + ecc.string_to_number(sha256(per_commitment_point + basepoint_bytes)) basepoint %= CURVE_ORDER return basepoint t@@ -212,7 +212,7 @@ def make_htlc_tx_output(amount_msat, local_feerate, revocationpubkey, local_dela final_amount_sat = (amount_msat - fee) // 1000 assert final_amount_sat > 0, final_amount_sat output = TxOutput(bitcoin.TYPE_ADDRESS, p2wsh, final_amount_sat) - return output + return script, output def make_htlc_tx_witness(remotehtlcsig, localhtlcsig, payment_preimage, witness_script): assert type(remotehtlcsig) is bytes t@@ -296,7 +296,7 @@ def make_htlc_tx_with_open_channel(chan, pcp, for_us, we_receive, commit, htlc): # HTLC-success for the HTLC spending from a received HTLC output # if we do not receive, and the commitment tx is not for us, they receive, so it is also an HTLC-success is_htlc_success = for_us == we_receive - htlc_tx_output = make_htlc_tx_output( + 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, t@@ -317,7 +317,7 @@ def make_htlc_tx_with_open_channel(chan, pcp, for_us, we_receive, commit, htlc): if is_htlc_success: cltv_expiry = 0 htlc_tx = make_htlc_tx(cltv_expiry, inputs=htlc_tx_inputs, output=htlc_tx_output) - return htlc_tx + return script, htlc_tx def make_funding_input(local_funding_pubkey: bytes, remote_funding_pubkey: bytes, payment_basepoint: bytes, remote_payment_basepoint: bytes, t@@ -598,12 +598,16 @@ def generate_keypair(ln_keystore: BIP32_KeyStore, key_family: LnKeyFamily, index return Keypair(*ln_keystore.get_keypair([key_family, 0, index], None)) -class EncumberedTransaction(NamedTuple("EncumberedTransaction", [('tx', Transaction), - ('csv_delay', Optional[int])])): +class EncumberedTransaction(NamedTuple("EncumberedTransaction", [('name', str), + ('tx', Transaction), + ('csv_delay', int), + ('cltv_expiry', int),])): def to_json(self) -> dict: return { + 'name': self.name, 'tx': str(self.tx), 'csv_delay': self.csv_delay, + 'cltv_expiry': self.cltv_expiry, } @classmethod DIR diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py t@@ -35,15 +35,15 @@ class LNWatcher(PrintError): self.lock = threading.RLock() self.watched_addresses = set() self.channel_info = storage.get('channel_info', {}) # access with 'lock' - # TODO structure will need to change when we handle HTLCs...... - # [funding_outpoint_str][ctx_txid] -> set of EncumberedTransaction + # [funding_outpoint_str][prev_txid] -> set of EncumberedTransaction + # prev_txid is the txid of a tx that is watched for confirmations # access with 'lock' self.sweepstore = defaultdict(lambda: defaultdict(set)) for funding_outpoint, ctxs in storage.get('sweepstore', {}).items(): - for ctx_txid, set_of_txns in ctxs.items(): + for txid, set_of_txns in ctxs.items(): for e_tx in set_of_txns: e_tx2 = EncumberedTransaction.from_json(e_tx) - self.sweepstore[funding_outpoint][ctx_txid].add(e_tx2) + self.sweepstore[funding_outpoint][txid].add(e_tx2) self.network.register_callback(self.on_network_update, ['network_updated', 'blockchain_updated', 'verified', 'wallet_updated']) t@@ -85,8 +85,8 @@ class LNWatcher(PrintError): sweepstore = {} for funding_outpoint, ctxs in self.sweepstore.items(): sweepstore[funding_outpoint] = {} - for ctx_txid, set_of_txns in ctxs.items(): - sweepstore[funding_outpoint][ctx_txid] = [e_tx.to_json() for e_tx in set_of_txns] + for prev_txid, set_of_txns in ctxs.items(): + sweepstore[funding_outpoint][prev_txid] = [e_tx.to_json() for e_tx in set_of_txns] storage.put('sweepstore', sweepstore) storage.write() t@@ -134,7 +134,7 @@ class LNWatcher(PrintError): # only care about confirmed and verified ctxs. TODO is this necessary? if conf == 0: return - keep_watching_this = await self.inspect_ctx_candidate(funding_outpoint, ctx_candidate) + keep_watching_this = await self.inspect_tx_candidate(funding_outpoint, ctx_candidate) if not keep_watching_this: self.stop_and_delete(funding_outpoint) t@@ -142,55 +142,77 @@ class LNWatcher(PrintError): # TODO delete channel from watcher_db pass - async def inspect_ctx_candidate(self, funding_outpoint, ctx): + async def inspect_tx_candidate(self, funding_outpoint, prev_tx): """Returns True iff found any not-deeply-spent outputs that we could potentially sweep at some point.""" - # make sure we are subscribed to all outputs of ctx + # make sure we are subscribed to all outputs of tx not_yet_watching = False - for o in ctx.outputs(): + for o in prev_tx.outputs(): if o.address not in self.watched_addresses: self.watch_address(o.address) not_yet_watching = True if not_yet_watching: return True # get all possible responses we have - ctx_txid = ctx.txid() + prev_txid = prev_tx.txid() with self.lock: - encumbered_sweep_txns = self.sweepstore[funding_outpoint][ctx_txid] + encumbered_sweep_txns = self.sweepstore[funding_outpoint][prev_txid] if len(encumbered_sweep_txns) == 0: - # no useful response for this channel close.. - if self.get_tx_mined_status(ctx_txid) == TX_MINED_STATUS_DEEP: - self.print_error("channel close detected for {}. but can't sweep anything :(".format(funding_outpoint)) + if self.get_tx_mined_status(prev_txid) == TX_MINED_STATUS_DEEP: return False # check if any response applies keep_watching_this = False local_height = self.network.get_local_height() - for e_tx in encumbered_sweep_txns: + txs_to_add = [] + for e_tx in list(encumbered_sweep_txns): conflicts = self.addr_sync.get_conflicting_transactions(e_tx.tx.txid(), e_tx.tx, include_self=True) conflict_mined_status = self.get_deepest_tx_mined_status_for_txids(conflicts) if conflict_mined_status != TX_MINED_STATUS_DEEP: keep_watching_this = True if conflict_mined_status == TX_MINED_STATUS_FREE: - tx_height = self.addr_sync.get_tx_height(ctx_txid).height + tx_height = self.addr_sync.get_tx_height(prev_txid).height num_conf = local_height - tx_height + 1 - if num_conf >= e_tx.csv_delay: - try: - await self.network.broadcast_transaction(e_tx.tx) - except Exception as e: - self.print_error('broadcast: {}, {}'.format('failure', repr(e))) + broadcast = True + if e_tx.cltv_expiry: + if local_height > e_tx.cltv_expiry: + self.print_error('CLTV ({} > {}) fulfilled'.format(local_height, e_tx.cltv_expiry)) else: - self.print_error('broadcast: {}'.format('success')) - else: - self.print_error('waiting for CSV ({} < {}) for funding outpoint {} and ctx {}' - .format(num_conf, e_tx.csv_delay, funding_outpoint, ctx.txid())) + self.print_error('waiting for CLTV ({} > {}) for funding outpoint {} and tx {}' + .format(local_height, e_tx.cltv_expiry, funding_outpoint, prev_tx.txid())) + broadcast = False + if e_tx.csv_delay: + if num_conf < e_tx.csv_delay: + self.print_error('waiting for CSV ({} >= {}) for funding outpoint {} and tx {}' + .format(num_conf, e_tx.csv_delay, funding_outpoint, prev_tx.txid())) + broadcast = False + if broadcast: + await self.broadcast_or_log(e_tx) + else: + # not mined or in mempool + keep_watching_this |= await self.inspect_tx_candidate(funding_outpoint, e_tx.tx) + return keep_watching_this + async def broadcast_or_log(self, e_tx): + try: + txid = await self.network.broadcast_transaction(e_tx.tx) + except Exception as e: + self.print_error(f'broadcast: {e_tx.name}: failure: {repr(e)}') + else: + self.print_error(f'broadcast: {e_tx.name}: success. txid: {txid}') + return True + return False + @with_watchtower - def add_sweep_tx(self, funding_outpoint: str, ctx_txid: str, sweeptx): + def add_sweep_tx(self, funding_outpoint: str, prev_txid: str, sweeptx): encumbered_sweeptx = EncumberedTransaction.from_json(sweeptx) with self.lock: - self.sweepstore[funding_outpoint][ctx_txid].add(encumbered_sweeptx) + tx_set = self.sweepstore[funding_outpoint][prev_txid] + if encumbered_sweeptx in tx_set: + return False + tx_set.add(encumbered_sweeptx) self.write_to_disk() + return True def get_tx_mined_status(self, txid: str): if not txid: