electrum

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

commit 1763d02b05fca7c27ef7892271110a6853821e5e
parent b26dc66567fe40f5623d274a47b131893e368ee9
Author: Janus <ysangkok@gmail.com>
Date:   Thu, 11 Oct 2018 17:15:25 +0200

rename lnhtlc->lnchan, HTLCStateMachine->Channel

Diffstat:
Melectrum/gui/qt/channels_list.py | 6+++---
Melectrum/lnbase.py | 6+++---
Aelectrum/lnchan.py | 810+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Delectrum/lnhtlc.py | 810-------------------------------------------------------------------------------
Melectrum/lnworker.py | 6+++---
Aelectrum/tests/test_lnchan.py | 367+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Delectrum/tests/test_lnhtlc.py | 367-------------------------------------------------------------------------------
Melectrum/tests/test_lnutil.py | 4++--
8 files changed, 1188 insertions(+), 1188 deletions(-)

diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py @@ -5,7 +5,7 @@ from PyQt5.QtWidgets import * from electrum.util import inv_dict, bh2u, bfh from electrum.i18n import _ -from electrum.lnhtlc import HTLCStateMachine +from electrum.lnchan import Channel from electrum.lnutil import LOCAL, REMOTE, ConnStringFormatError from .util import MyTreeWidget, SortableTreeWidgetItem, WindowModalDialog, Buttons, OkButton, CancelButton @@ -13,7 +13,7 @@ from .amountedit import BTCAmountEdit class ChannelsList(MyTreeWidget): update_rows = QtCore.pyqtSignal() - update_single_row = QtCore.pyqtSignal(HTLCStateMachine) + update_single_row = QtCore.pyqtSignal(Channel) def __init__(self, parent): MyTreeWidget.__init__(self, parent, self.create_menu, [_('Node ID'), _('Balance'), _('Remote'), _('Status')], 0) @@ -43,7 +43,7 @@ class ChannelsList(MyTreeWidget): menu.addAction(_("Force-close channel"), close) menu.exec_(self.viewport().mapToGlobal(position)) - @QtCore.pyqtSlot(HTLCStateMachine) + @QtCore.pyqtSlot(Channel) def do_update_single_row(self, chan): for i in range(self.topLevelItemCount()): item = self.topLevelItem(i) diff --git a/electrum/lnbase.py b/electrum/lnbase.py @@ -27,7 +27,7 @@ from .util import PrintError, bh2u, print_error, bfh, log_exceptions from .transaction import Transaction, TxOutput from .lnonion import new_onion_packet, OnionHopsDataSingle, OnionPerHop, decode_onion_error, OnionFailureCode from .lnaddr import lndecode -from .lnhtlc import HTLCStateMachine, RevokeAndAck, htlcsum +from .lnchan import Channel, RevokeAndAck, htlcsum from .lnutil import (Outpoint, LocalConfig, ChannelConfig, RemoteConfig, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore, funding_output_script, get_ecdh, get_per_commitment_secret_from_seed, @@ -641,7 +641,7 @@ 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, } - m = HTLCStateMachine(chan) + m = Channel(chan) m.lnwatcher = self.lnwatcher m.sweep_address = self.lnworker.sweep_address sig_64, _ = m.sign_next_commitment() @@ -737,7 +737,7 @@ 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, } - m = HTLCStateMachine(chan) + m = Channel(chan) m.lnwatcher = self.lnwatcher m.sweep_address = self.lnworker.sweep_address remote_sig = funding_created['signature'] diff --git a/electrum/lnchan.py b/electrum/lnchan.py @@ -0,0 +1,810 @@ +# ported from lnd 42de4400bff5105352d0552155f73589166d162b +from collections import namedtuple, defaultdict +import binascii +import json +from enum import Enum, auto +from typing import Optional + +from .util import bfh, PrintError, bh2u +from .bitcoin import Hash, TYPE_SCRIPT, TYPE_ADDRESS +from .bitcoin import redeem_script_to_address +from .crypto import sha256 +from . import ecc +from .lnutil import Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore, EncumberedTransaction +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 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 +from .lnutil import ScriptHtlc, SENT, RECEIVED +from .transaction import Transaction, TxOutput, construct_witness +from .simple_config import SimpleConfig, FEERATE_FALLBACK_STATIC_FEE + + +RevokeAndAck = namedtuple("RevokeAndAck", ["per_commitment_secret", "next_per_commitment_point"]) + +class FeeUpdateProgress(Enum): + FUNDEE_SIGNED = auto() + FUNDEE_ACKED = auto() + FUNDER_SIGNED = auto() + +FUNDEE_SIGNED = FeeUpdateProgress.FUNDEE_SIGNED +FUNDEE_ACKED = FeeUpdateProgress.FUNDEE_ACKED +FUNDER_SIGNED = FeeUpdateProgress.FUNDER_SIGNED + +class FeeUpdate(defaultdict): + def __init__(self, chan, rate): + super().__init__(lambda: False) + self.rate = rate + self.chan = chan + + def pending_feerate(self, subject): + if self[FUNDEE_ACKED]: + return self.rate + if subject == REMOTE and self.chan.constraints.is_initiator: + return self.rate + if subject == LOCAL and not self.chan.constraints.is_initiator: + return self.rate + # implicit return None + +class UpdateAddHtlc(namedtuple('UpdateAddHtlc', ['amount_msat', 'payment_hash', 'cltv_expiry', 'locked_in', 'htlc_id'])): + __slots__ = () + def __new__(cls, *args, **kwargs): + if len(args) > 0: + args = list(args) + if type(args[1]) is str: + args[1] = bfh(args[1]) + args[3] = {HTLCOwner(int(x)): y for x,y in args[3].items()} + return super().__new__(cls, *args) + if type(kwargs['payment_hash']) is str: + kwargs['payment_hash'] = bfh(kwargs['payment_hash']) + if 'locked_in' not in kwargs: + kwargs['locked_in'] = {LOCAL: None, REMOTE: None} + else: + kwargs['locked_in'] = {HTLCOwner(int(x)): y for x,y in kwargs['locked_in'].items()} + return super().__new__(cls, **kwargs) + +def decodeAll(d, local): + for k, v in d.items(): + if k == 'revocation_store': + yield (k, RevocationStore.from_json_obj(v)) + elif k.endswith("_basepoint") or k.endswith("_key"): + if local: + yield (k, Keypair(**dict(decodeAll(v, local)))) + else: + yield (k, OnlyPubkeyKeypair(**dict(decodeAll(v, local)))) + elif k in ["node_id", "channel_id", "short_channel_id", "pubkey", "privkey", "current_per_commitment_point", "next_per_commitment_point", "per_commitment_secret_seed", "current_commitment_signature", "current_htlc_signatures"] and v is not None: + yield (k, binascii.unhexlify(v)) + else: + yield (k, v) + +def htlcsum(htlcs): + return sum([x.amount_msat for x in htlcs]) + +class Channel(PrintError): + def diagnostic_name(self): + return str(self.name) + + def __init__(self, state, name = None): + assert 'local_state' not in state + self.config = {} + self.config[LOCAL] = state["local_config"] + if type(self.config[LOCAL]) is not LocalConfig: + conf = dict(decodeAll(self.config[LOCAL], True)) + self.config[LOCAL] = LocalConfig(**conf) + assert type(self.config[LOCAL].htlc_basepoint.privkey) is bytes + + self.config[REMOTE] = state["remote_config"] + if type(self.config[REMOTE]) is not RemoteConfig: + conf = dict(decodeAll(self.config[REMOTE], False)) + self.config[REMOTE] = RemoteConfig(**conf) + assert type(self.config[REMOTE].htlc_basepoint.pubkey) is bytes + + self.channel_id = bfh(state["channel_id"]) if type(state["channel_id"]) not in (bytes, type(None)) else state["channel_id"] + self.constraints = ChannelConstraints(**state["constraints"]) if type(state["constraints"]) is not ChannelConstraints else state["constraints"] + self.funding_outpoint = Outpoint(**dict(decodeAll(state["funding_outpoint"], False))) if type(state["funding_outpoint"]) is not Outpoint else state["funding_outpoint"] + self.node_id = bfh(state["node_id"]) if type(state["node_id"]) not in (bytes, type(None)) else state["node_id"] + self.short_channel_id = bfh(state["short_channel_id"]) if type(state["short_channel_id"]) not in (bytes, type(None)) else state["short_channel_id"] + self.short_channel_id_predicted = self.short_channel_id + self.onion_keys = {int(k): bfh(v) for k,v in state['onion_keys'].items()} if 'onion_keys' in state else {} + + # FIXME this is a tx serialised in the custom electrum partial tx format. + # we should not persist txns in this format. we should persist htlcs, and be able to derive + # any past commitment transaction and use that instead; until then... + self.remote_commitment_to_be_revoked = Transaction(state["remote_commitment_to_be_revoked"]) + + template = lambda: {'adds': {}, 'settles': []} + self.log = {LOCAL: template(), REMOTE: template()} + for strname, subject in [('remote_log', REMOTE), ('local_log', LOCAL)]: + if strname not in state: continue + for y in state[strname]: + htlc = UpdateAddHtlc(**y) + self.log[subject]['adds'][htlc.htlc_id] = htlc + + self.name = name + + self.fee_mgr = [] + + self.local_commitment = self.pending_local_commitment + self.remote_commitment = self.pending_remote_commitment + + self._is_funding_txo_spent = None # "don't know" + self.set_state('DISCONNECTED') + + self.lnwatcher = None + + self.settled = {LOCAL: state.get('settled_local', []), REMOTE: state.get('settled_remote', [])} + + def set_state(self, state: str): + self._state = state + + def get_state(self): + return self._state + + def set_funding_txo_spentness(self, is_spent: bool): + assert isinstance(is_spent, bool) + self._is_funding_txo_spent = is_spent + + def should_try_to_reestablish_peer(self) -> bool: + return self._is_funding_txo_spent is False and self._state == 'DISCONNECTED' + + def get_funding_address(self): + script = funding_output_script(self.config[LOCAL], self.config[REMOTE]) + return redeem_script_to_address('p2wsh', script) + + def add_htlc(self, htlc): + """ + AddHTLC adds an HTLC to the state machine's local update log. This method + should be called when preparing to send an outgoing HTLC. + """ + assert type(htlc) is dict + htlc = UpdateAddHtlc(**htlc, htlc_id=self.config[LOCAL].next_htlc_id) + self.log[LOCAL]['adds'][htlc.htlc_id] = htlc + self.print_error("add_htlc") + self.config[LOCAL]=self.config[LOCAL]._replace(next_htlc_id=htlc.htlc_id + 1) + return htlc.htlc_id + + def receive_htlc(self, htlc): + """ + ReceiveHTLC adds an HTLC to the state machine's remote update log. This + method should be called in response to receiving a new HTLC from the remote + party. + """ + assert type(htlc) is dict + htlc = UpdateAddHtlc(**htlc, htlc_id = self.config[REMOTE].next_htlc_id) + self.log[REMOTE]['adds'][htlc.htlc_id] = htlc + self.print_error("receive_htlc") + self.config[REMOTE]=self.config[REMOTE]._replace(next_htlc_id=htlc.htlc_id + 1) + return htlc.htlc_id + + def sign_next_commitment(self): + """ + SignNextCommitment signs a new commitment which includes any previous + unsettled HTLCs, any new HTLCs, and any modifications to prior HTLCs + committed in previous commitment updates. Signing a new commitment + decrements the available revocation window by 1. After a successful method + call, the remote party's commitment chain is extended by a new commitment + which includes all updates to the HTLC log prior to this method invocation. + The first return parameter is the signature for the commitment transaction + itself, while the second parameter is a slice of all HTLC signatures (if + any). The HTLC signatures are sorted according to the BIP 69 order of the + HTLC's on the commitment transaction. + """ + for htlc in self.log[LOCAL]['adds'].values(): + if htlc.locked_in[LOCAL] is None: + htlc.locked_in[LOCAL] = self.config[LOCAL].ctn + self.print_error("sign_next_commitment") + + pending_remote_commitment = self.pending_remote_commitment + sig_64 = sign_and_get_sig_string(pending_remote_commitment, self.config[LOCAL], self.config[REMOTE]) + + their_remote_htlc_privkey_number = derive_privkey( + int.from_bytes(self.config[LOCAL].htlc_basepoint.privkey, 'big'), + self.config[REMOTE].next_per_commitment_point) + their_remote_htlc_privkey = their_remote_htlc_privkey_number.to_bytes(32, 'big') + + for_us = False + + 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] + 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)) + + for pending_fee in self.fee_mgr: + if not self.constraints.is_initiator: + pending_fee[FUNDEE_SIGNED] = True + if self.constraints.is_initiator and pending_fee[FUNDEE_ACKED]: + pending_fee[FUNDER_SIGNED] = True + + self.process_new_offchain_ctx(pending_remote_commitment, ours=False) + + htlcsigs.sort() + htlcsigs = [x[1] for x in htlcsigs] + + return sig_64, htlcsigs + + def receive_new_commitment(self, sig, htlc_sigs): + """ + ReceiveNewCommitment process a signature for a new commitment state sent by + the remote party. This method should be called in response to the + remote party initiating a new change, or when the remote party sends a + signature fully accepting a new state we've initiated. If we are able to + successfully validate the signature, then the generated commitment is added + to our local commitment chain. Once we send a revocation for our prior + state, then this newly added commitment becomes our current accepted channel + state. + """ + + self.print_error("receive_new_commitment") + for htlc in self.log[REMOTE]['adds'].values(): + if htlc.locked_in[REMOTE] is None: + htlc.locked_in[REMOTE] = self.config[REMOTE].ctn + assert len(htlc_sigs) == 0 or type(htlc_sigs[0]) is bytes + + pending_local_commitment = self.pending_local_commitment + preimage_hex = pending_local_commitment.serialize_preimage(0) + pre_hash = Hash(bfh(preimage_hex)) + 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 + + 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}') + if len(htlc_sigs) != 0: # all sigs should have been popped above + raise Exception('failed verifying HTLC signatures: invalid amount of correct signatures') + + for pending_fee in self.fee_mgr: + if not self.constraints.is_initiator: + pending_fee[FUNDEE_SIGNED] = True + if self.constraints.is_initiator and pending_fee[FUNDEE_ACKED]: + pending_fee[FUNDER_SIGNED] = True + + self.process_new_offchain_ctx(pending_local_commitment, ours=True) + + + def revoke_current_commitment(self): + """ + RevokeCurrentCommitment revokes the next lowest unrevoked commitment + transaction in the local commitment chain. As a result the edge of our + revocation window is extended by one, and the tail of our local commitment + chain is advanced by a single commitment. This now lowest unrevoked + commitment becomes our currently accepted state within the channel. This + method also returns the set of HTLC's currently active within the commitment + transaction. This return value allows callers to act once an HTLC has been + locked into our commitment transaction. + """ + self.print_error("revoke_current_commitment") + + last_secret, this_point, next_point = self.points + + new_feerate = self.constraints.feerate + + for pending_fee in self.fee_mgr[:]: + if not self.constraints.is_initiator and pending_fee[FUNDEE_SIGNED]: + new_feerate = pending_fee.rate + self.fee_mgr.remove(pending_fee) + print("FEERATE CHANGE COMPLETE (non-initiator)") + if self.constraints.is_initiator and pending_fee[FUNDER_SIGNED]: + new_feerate = pending_fee.rate + self.fee_mgr.remove(pending_fee) + print("FEERATE CHANGE COMPLETE (initiator)") + + self.config[LOCAL]=self.config[LOCAL]._replace( + ctn=self.config[LOCAL].ctn + 1, + ) + self.constraints=self.constraints._replace( + feerate=new_feerate + ) + + self.local_commitment = self.pending_local_commitment + + return RevokeAndAck(last_secret, next_point), "current htlcs" + + @property + def points(self): + last_small_num = self.config[LOCAL].ctn + this_small_num = last_small_num + 1 + next_small_num = last_small_num + 2 + last_secret = get_per_commitment_secret_from_seed(self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - last_small_num) + this_secret = get_per_commitment_secret_from_seed(self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - this_small_num) + this_point = secret_to_pubkey(int.from_bytes(this_secret, 'big')) + next_secret = get_per_commitment_secret_from_seed(self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - next_small_num) + 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): + 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_sweeptx = maybe_create_sweeptx_for_our_ctx_to_local(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) + self.lnwatcher.add_sweep_tx(outpoint, ctx.txid(), encumbered_sweeptx) + + 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) + self.lnwatcher.add_sweep_tx(outpoint, ctx.txid(), encumbered_sweeptx) + + def receive_revocation(self, revocation): + """ + ReceiveRevocation processes a revocation sent by the remote party for the + lowest unrevoked commitment within their commitment chain. We receive a + revocation either during the initial session negotiation wherein revocation + windows are extended, or in response to a state update that we initiate. If + successful, then the remote commitment chain is advanced by a single + commitment, and a log compaction is attempted. + + Returns the forwarding package corresponding to the remote commitment height + that was revoked. + """ + self.print_error("receive_revocation") + + cur_point = self.config[REMOTE].current_per_commitment_point + derived_point = ecc.ECPrivkey(revocation.per_commitment_secret).get_public_key_bytes(compressed=True) + if cur_point != derived_point: + raise Exception('revoked secret not for current point') + + # 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 + prev_remote_commitment = self.pending_remote_commitment + + self.config[REMOTE].revocation_store.add_next_entry(revocation.per_commitment_secret) + self.process_new_revocation_secret(revocation.per_commitment_secret) + + def mark_settled(subject): + """ + find pending settlements for subject (LOCAL or REMOTE) and mark them settled, return value of settled htlcs + """ + old_amount = htlcsum(self.htlcs(subject, False)) + + for htlc_id in self.log[-subject]['settles']: + adds = self.log[subject]['adds'] + htlc = adds.pop(htlc_id) + self.settled[subject].append(htlc.amount_msat) + self.log[-subject]['settles'].clear() + + return old_amount - htlcsum(self.htlcs(subject, False)) + + sent_this_batch = mark_settled(LOCAL) + received_this_batch = mark_settled(REMOTE) + + next_point = self.config[REMOTE].next_per_commitment_point + + print("RECEIVED", received_this_batch) + print("SENT", sent_this_batch) + self.config[REMOTE]=self.config[REMOTE]._replace( + ctn=self.config[REMOTE].ctn + 1, + current_per_commitment_point=next_point, + next_per_commitment_point=revocation.next_per_commitment_point, + amount_msat=self.config[REMOTE].amount_msat + (sent_this_batch - received_this_batch) + ) + self.config[LOCAL]=self.config[LOCAL]._replace( + amount_msat = self.config[LOCAL].amount_msat + (received_this_batch - sent_this_batch) + ) + + for pending_fee in self.fee_mgr: + if self.constraints.is_initiator: + pending_fee[FUNDEE_ACKED] = True + + self.local_commitment = self.pending_local_commitment + self.remote_commitment = self.pending_remote_commitment + self.remote_commitment_to_be_revoked = prev_remote_commitment + return received_this_batch, sent_this_batch + + def balance(self, subject): + initial = self.config[subject].initial_msat + + initial -= sum(self.settled[subject]) + initial += sum(self.settled[-subject]) + + assert initial == self.config[subject].amount_msat + return initial + + def amounts(self): + remote_settled= htlcsum(self.htlcs(REMOTE, False)) + local_settled= htlcsum(self.htlcs(LOCAL, False)) + unsettled_local = htlcsum(self.htlcs(LOCAL, True)) + unsettled_remote = htlcsum(self.htlcs(REMOTE, True)) + remote_msat = self.config[REMOTE].amount_msat -\ + unsettled_remote + local_settled - remote_settled + local_msat = self.config[LOCAL].amount_msat -\ + unsettled_local + remote_settled - local_settled + return remote_msat, local_msat + + def included_htlcs(self, subject, htlc_initiator): + """ + return filter of non-dust htlcs for subjects commitment transaction, initiated by given party + """ + feerate = self.pending_feerate(subject) + conf = self.config[subject] + weight = HTLC_SUCCESS_WEIGHT if subject != htlc_initiator else HTLC_TIMEOUT_WEIGHT + htlcs = self.htlcs(htlc_initiator, only_pending=True) + fee_for_htlc = lambda htlc: htlc.amount_msat // 1000 - (weight * feerate // 1000) + return filter(lambda htlc: fee_for_htlc(htlc) >= conf.dust_limit_sat, htlcs) + + @property + def pending_remote_commitment(self): + this_point = self.config[REMOTE].next_per_commitment_point + return self.make_commitment(REMOTE, this_point) + + def pending_feerate(self, subject): + candidate = self.constraints.feerate + for pending_fee in self.fee_mgr: + x = pending_fee.pending_feerate(subject) + if x is not None: + candidate = x + + return candidate + + @property + def pending_local_commitment(self): + _, this_point, _ = self.points + return self.make_commitment(LOCAL, this_point) + + def total_msat(self, sub): + return sum(self.settled[sub]) + + def htlcs(self, subject, only_pending): + """ + only_pending: require the htlc's settlement to be pending (needs additional signatures/acks) + """ + update_log = self.log[subject] + other_log = self.log[-subject] + res = [] + for htlc in update_log['adds'].values(): + locked_in = htlc.locked_in[subject] + + if locked_in is None or only_pending == (htlc.htlc_id in other_log['settles']): + continue + res.append(htlc) + return res + + def settle_htlc(self, preimage, htlc_id): + """ + SettleHTLC attempts to settle an existing outstanding received HTLC. + """ + self.print_error("settle_htlc") + htlc = self.log[REMOTE]['adds'][htlc_id] + assert htlc.payment_hash == sha256(preimage) + self.log[LOCAL]['settles'].append(htlc_id) + + def receive_htlc_settle(self, preimage, htlc_index): + self.print_error("receive_htlc_settle") + htlc = self.log[LOCAL]['adds'][htlc_index] + assert htlc.payment_hash == sha256(preimage) + self.log[REMOTE]['settles'].append(htlc_index) + + def receive_fail_htlc(self, htlc_id): + self.print_error("receive_fail_htlc") + self.log[LOCAL]['adds'].pop(htlc_id) + + @property + def current_height(self): + return {LOCAL: self.config[LOCAL].ctn, REMOTE: self.config[REMOTE].ctn} + + @property + def pending_local_fee(self): + return self.constraints.capacity - sum(x[2] for x in self.pending_local_commitment.outputs()) + + def update_fee(self, feerate): + if not self.constraints.is_initiator: + raise Exception("only initiator can update_fee, this counterparty is not initiator") + pending_fee = FeeUpdate(self, rate=feerate) + self.fee_mgr.append(pending_fee) + + def receive_update_fee(self, feerate): + if self.constraints.is_initiator: + raise Exception("only the non-initiator can receive_update_fee, this counterparty is initiator") + pending_fee = FeeUpdate(self, rate=feerate) + self.fee_mgr.append(pending_fee) + + def remove_uncommitted_htlcs_from_log(self, subject): + """ + returns + - the htlcs with uncommited (not locked in) htlcs removed + - a list of htlc_ids that were removed + """ + removed = [] + htlcs = [] + for i in self.log[subject]['adds'].values(): + locked_in = i.locked_in[LOCAL] is not None or i.locked_in[REMOTE] is not None + if locked_in: + htlcs.append(i._asdict()) + else: + removed.append(i.htlc_id) + return htlcs, removed + + def to_save(self): + # need to forget about uncommited htlcs + # since we must assume they don't know about it, + # if it was not acked + remote_filtered, remote_removed = self.remove_uncommitted_htlcs_from_log(REMOTE) + local_filtered, local_removed = self.remove_uncommitted_htlcs_from_log(LOCAL) + to_save = { + "local_config": self.config[LOCAL], + "remote_config": self.config[REMOTE], + "channel_id": self.channel_id, + "short_channel_id": self.short_channel_id, + "constraints": self.constraints, + "funding_outpoint": self.funding_outpoint, + "node_id": self.node_id, + "remote_commitment_to_be_revoked": str(self.remote_commitment_to_be_revoked), + "remote_log": remote_filtered, + "local_log": local_filtered, + "onion_keys": {str(k): bh2u(v) for k, v in self.onion_keys.items()}, + "settled_local": self.settled[LOCAL], + "settled_remote": self.settled[REMOTE], + } + + # htlcs number must be monotonically increasing, + # so we have to decrease the counter + if len(remote_removed) != 0: + assert min(remote_removed) < to_save['remote_config'].next_htlc_id + to_save['remote_config'] = to_save['remote_config']._replace(next_htlc_id = min(remote_removed)) + + if len(local_removed) != 0: + assert min(local_removed) < to_save['local_config'].next_htlc_id + to_save['local_config'] = to_save['local_config']._replace(next_htlc_id = min(local_removed)) + + return to_save + + def serialize(self): + namedtuples_to_dict = lambda v: {i: j._asdict() if isinstance(j, tuple) else j for i, j in v._asdict().items()} + serialized_channel = {k: namedtuples_to_dict(v) if isinstance(v, tuple) else v for k, v in self.to_save().items()} + class MyJsonEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, bytes): + return binascii.hexlify(o).decode("ascii") + if isinstance(o, RevocationStore): + return o.serialize() + return super(MyJsonEncoder, self) + dumped = MyJsonEncoder().encode(serialized_channel) + roundtripped = json.loads(dumped) + reconstructed = Channel(roundtripped) + if reconstructed.to_save() != self.to_save(): + from pprint import pformat + try: + from deepdiff import DeepDiff + except ImportError: + raise Exception("Channels did not roundtrip serialization without changes:\n" + pformat(reconstructed.to_save()) + "\n" + pformat(self.to_save())) + else: + raise Exception("Channels did not roundtrip serialization without changes:\n" + pformat(DeepDiff(reconstructed.to_save(), self.to_save()))) + return roundtripped + + def __str__(self): + return self.serialize() + + def make_commitment(self, subject, this_point) -> Transaction: + remote_msat, local_msat = self.amounts() + assert local_msat >= 0 + assert remote_msat >= 0 + this_config = self.config[subject] + other_config = self.config[-subject] + 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 = [] + 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)) + 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)) + if subject != LOCAL: + remote_msat, local_msat = local_msat, remote_msat + payment_pubkey = derive_pubkey(other_config.payment_basepoint.pubkey, this_point) + return make_commitment( + self.config[subject].ctn + 1, + this_config.multisig_key.pubkey, + other_config.multisig_key.pubkey, + payment_pubkey, + self.config[LOCAL].payment_basepoint.pubkey, + self.config[REMOTE].payment_basepoint.pubkey, + other_revocation_pubkey, + derive_pubkey(this_config.delayed_basepoint.pubkey, this_point), + other_config.to_self_delay, + *self.funding_outpoint, + self.constraints.capacity, + local_msat, + remote_msat, + this_config.dust_limit_sat, + self.pending_feerate(subject), + subject == LOCAL, + self.constraints.is_initiator, + htlcs=htlcs) + + def make_closing_tx(self, local_script: bytes, remote_script: bytes, fee_sat: Optional[int] = None) -> (bytes, int): + if fee_sat is None: + fee_sat = self.pending_local_fee + + _, outputs = make_outputs(fee_sat * 1000, True, + self.config[LOCAL].amount_msat, + self.config[REMOTE].amount_msat, + (TYPE_SCRIPT, bh2u(local_script)), + (TYPE_SCRIPT, bh2u(remote_script)), + [], self.config[LOCAL].dust_limit_sat) + + closing_tx = make_closing_tx(self.config[LOCAL].multisig_key.pubkey, + self.config[REMOTE].multisig_key.pubkey, + self.config[LOCAL].payment_basepoint.pubkey, + self.config[REMOTE].payment_basepoint.pubkey, + # TODO hardcoded we_are_initiator: + True, *self.funding_outpoint, self.constraints.capacity, + outputs) + + der_sig = bfh(closing_tx.sign_txin(0, self.config[LOCAL].multisig_key.privkey)) + sig = ecc.sig_string_from_der_sig(der_sig[:-1]) + return sig, fee_sat + +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(sweep_tx, csv_delay=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(sweep_tx, csv_delay=0) + + +def maybe_create_sweeptx_for_our_ctx_to_local(chan, ctx, our_pcp: bytes, + sweep_address) -> Optional[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) + 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=our_localdelayed_privkey.get_secret_bytes(), + is_revocation=False, + to_self_delay=to_self_delay) + + return EncumberedTransaction(sweep_tx, csv_delay=to_self_delay) + + +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/lnhtlc.py b/electrum/lnhtlc.py @@ -1,810 +0,0 @@ -# ported from lnd 42de4400bff5105352d0552155f73589166d162b -from collections import namedtuple, defaultdict -import binascii -import json -from enum import Enum, auto -from typing import Optional - -from .util import bfh, PrintError, bh2u -from .bitcoin import Hash, TYPE_SCRIPT, TYPE_ADDRESS -from .bitcoin import redeem_script_to_address -from .crypto import sha256 -from . import ecc -from .lnutil import Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore, EncumberedTransaction -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 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 -from .lnutil import ScriptHtlc, SENT, RECEIVED -from .transaction import Transaction, TxOutput, construct_witness -from .simple_config import SimpleConfig, FEERATE_FALLBACK_STATIC_FEE - - -RevokeAndAck = namedtuple("RevokeAndAck", ["per_commitment_secret", "next_per_commitment_point"]) - -class FeeUpdateProgress(Enum): - FUNDEE_SIGNED = auto() - FUNDEE_ACKED = auto() - FUNDER_SIGNED = auto() - -FUNDEE_SIGNED = FeeUpdateProgress.FUNDEE_SIGNED -FUNDEE_ACKED = FeeUpdateProgress.FUNDEE_ACKED -FUNDER_SIGNED = FeeUpdateProgress.FUNDER_SIGNED - -class FeeUpdate(defaultdict): - def __init__(self, chan, rate): - super().__init__(lambda: False) - self.rate = rate - self.chan = chan - - def pending_feerate(self, subject): - if self[FUNDEE_ACKED]: - return self.rate - if subject == REMOTE and self.chan.constraints.is_initiator: - return self.rate - if subject == LOCAL and not self.chan.constraints.is_initiator: - return self.rate - # implicit return None - -class UpdateAddHtlc(namedtuple('UpdateAddHtlc', ['amount_msat', 'payment_hash', 'cltv_expiry', 'locked_in', 'htlc_id'])): - __slots__ = () - def __new__(cls, *args, **kwargs): - if len(args) > 0: - args = list(args) - if type(args[1]) is str: - args[1] = bfh(args[1]) - args[3] = {HTLCOwner(int(x)): y for x,y in args[3].items()} - return super().__new__(cls, *args) - if type(kwargs['payment_hash']) is str: - kwargs['payment_hash'] = bfh(kwargs['payment_hash']) - if 'locked_in' not in kwargs: - kwargs['locked_in'] = {LOCAL: None, REMOTE: None} - else: - kwargs['locked_in'] = {HTLCOwner(int(x)): y for x,y in kwargs['locked_in'].items()} - return super().__new__(cls, **kwargs) - -def decodeAll(d, local): - for k, v in d.items(): - if k == 'revocation_store': - yield (k, RevocationStore.from_json_obj(v)) - elif k.endswith("_basepoint") or k.endswith("_key"): - if local: - yield (k, Keypair(**dict(decodeAll(v, local)))) - else: - yield (k, OnlyPubkeyKeypair(**dict(decodeAll(v, local)))) - elif k in ["node_id", "channel_id", "short_channel_id", "pubkey", "privkey", "current_per_commitment_point", "next_per_commitment_point", "per_commitment_secret_seed", "current_commitment_signature", "current_htlc_signatures"] and v is not None: - yield (k, binascii.unhexlify(v)) - else: - yield (k, v) - -def htlcsum(htlcs): - return sum([x.amount_msat for x in htlcs]) - -class HTLCStateMachine(PrintError): - def diagnostic_name(self): - return str(self.name) - - def __init__(self, state, name = None): - assert 'local_state' not in state - self.config = {} - self.config[LOCAL] = state["local_config"] - if type(self.config[LOCAL]) is not LocalConfig: - conf = dict(decodeAll(self.config[LOCAL], True)) - self.config[LOCAL] = LocalConfig(**conf) - assert type(self.config[LOCAL].htlc_basepoint.privkey) is bytes - - self.config[REMOTE] = state["remote_config"] - if type(self.config[REMOTE]) is not RemoteConfig: - conf = dict(decodeAll(self.config[REMOTE], False)) - self.config[REMOTE] = RemoteConfig(**conf) - assert type(self.config[REMOTE].htlc_basepoint.pubkey) is bytes - - self.channel_id = bfh(state["channel_id"]) if type(state["channel_id"]) not in (bytes, type(None)) else state["channel_id"] - self.constraints = ChannelConstraints(**state["constraints"]) if type(state["constraints"]) is not ChannelConstraints else state["constraints"] - self.funding_outpoint = Outpoint(**dict(decodeAll(state["funding_outpoint"], False))) if type(state["funding_outpoint"]) is not Outpoint else state["funding_outpoint"] - self.node_id = bfh(state["node_id"]) if type(state["node_id"]) not in (bytes, type(None)) else state["node_id"] - self.short_channel_id = bfh(state["short_channel_id"]) if type(state["short_channel_id"]) not in (bytes, type(None)) else state["short_channel_id"] - self.short_channel_id_predicted = self.short_channel_id - self.onion_keys = {int(k): bfh(v) for k,v in state['onion_keys'].items()} if 'onion_keys' in state else {} - - # FIXME this is a tx serialised in the custom electrum partial tx format. - # we should not persist txns in this format. we should persist htlcs, and be able to derive - # any past commitment transaction and use that instead; until then... - self.remote_commitment_to_be_revoked = Transaction(state["remote_commitment_to_be_revoked"]) - - template = lambda: {'adds': {}, 'settles': []} - self.log = {LOCAL: template(), REMOTE: template()} - for strname, subject in [('remote_log', REMOTE), ('local_log', LOCAL)]: - if strname not in state: continue - for y in state[strname]: - htlc = UpdateAddHtlc(**y) - self.log[subject]['adds'][htlc.htlc_id] = htlc - - self.name = name - - self.fee_mgr = [] - - self.local_commitment = self.pending_local_commitment - self.remote_commitment = self.pending_remote_commitment - - self._is_funding_txo_spent = None # "don't know" - self.set_state('DISCONNECTED') - - self.lnwatcher = None - - self.settled = {LOCAL: state.get('settled_local', []), REMOTE: state.get('settled_remote', [])} - - def set_state(self, state: str): - self._state = state - - def get_state(self): - return self._state - - def set_funding_txo_spentness(self, is_spent: bool): - assert isinstance(is_spent, bool) - self._is_funding_txo_spent = is_spent - - def should_try_to_reestablish_peer(self) -> bool: - return self._is_funding_txo_spent is False and self._state == 'DISCONNECTED' - - def get_funding_address(self): - script = funding_output_script(self.config[LOCAL], self.config[REMOTE]) - return redeem_script_to_address('p2wsh', script) - - def add_htlc(self, htlc): - """ - AddHTLC adds an HTLC to the state machine's local update log. This method - should be called when preparing to send an outgoing HTLC. - """ - assert type(htlc) is dict - htlc = UpdateAddHtlc(**htlc, htlc_id=self.config[LOCAL].next_htlc_id) - self.log[LOCAL]['adds'][htlc.htlc_id] = htlc - self.print_error("add_htlc") - self.config[LOCAL]=self.config[LOCAL]._replace(next_htlc_id=htlc.htlc_id + 1) - return htlc.htlc_id - - def receive_htlc(self, htlc): - """ - ReceiveHTLC adds an HTLC to the state machine's remote update log. This - method should be called in response to receiving a new HTLC from the remote - party. - """ - assert type(htlc) is dict - htlc = UpdateAddHtlc(**htlc, htlc_id = self.config[REMOTE].next_htlc_id) - self.log[REMOTE]['adds'][htlc.htlc_id] = htlc - self.print_error("receive_htlc") - self.config[REMOTE]=self.config[REMOTE]._replace(next_htlc_id=htlc.htlc_id + 1) - return htlc.htlc_id - - def sign_next_commitment(self): - """ - SignNextCommitment signs a new commitment which includes any previous - unsettled HTLCs, any new HTLCs, and any modifications to prior HTLCs - committed in previous commitment updates. Signing a new commitment - decrements the available revocation window by 1. After a successful method - call, the remote party's commitment chain is extended by a new commitment - which includes all updates to the HTLC log prior to this method invocation. - The first return parameter is the signature for the commitment transaction - itself, while the second parameter is a slice of all HTLC signatures (if - any). The HTLC signatures are sorted according to the BIP 69 order of the - HTLC's on the commitment transaction. - """ - for htlc in self.log[LOCAL]['adds'].values(): - if htlc.locked_in[LOCAL] is None: - htlc.locked_in[LOCAL] = self.config[LOCAL].ctn - self.print_error("sign_next_commitment") - - pending_remote_commitment = self.pending_remote_commitment - sig_64 = sign_and_get_sig_string(pending_remote_commitment, self.config[LOCAL], self.config[REMOTE]) - - their_remote_htlc_privkey_number = derive_privkey( - int.from_bytes(self.config[LOCAL].htlc_basepoint.privkey, 'big'), - self.config[REMOTE].next_per_commitment_point) - their_remote_htlc_privkey = their_remote_htlc_privkey_number.to_bytes(32, 'big') - - for_us = False - - 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] - 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)) - - for pending_fee in self.fee_mgr: - if not self.constraints.is_initiator: - pending_fee[FUNDEE_SIGNED] = True - if self.constraints.is_initiator and pending_fee[FUNDEE_ACKED]: - pending_fee[FUNDER_SIGNED] = True - - self.process_new_offchain_ctx(pending_remote_commitment, ours=False) - - htlcsigs.sort() - htlcsigs = [x[1] for x in htlcsigs] - - return sig_64, htlcsigs - - def receive_new_commitment(self, sig, htlc_sigs): - """ - ReceiveNewCommitment process a signature for a new commitment state sent by - the remote party. This method should be called in response to the - remote party initiating a new change, or when the remote party sends a - signature fully accepting a new state we've initiated. If we are able to - successfully validate the signature, then the generated commitment is added - to our local commitment chain. Once we send a revocation for our prior - state, then this newly added commitment becomes our current accepted channel - state. - """ - - self.print_error("receive_new_commitment") - for htlc in self.log[REMOTE]['adds'].values(): - if htlc.locked_in[REMOTE] is None: - htlc.locked_in[REMOTE] = self.config[REMOTE].ctn - assert len(htlc_sigs) == 0 or type(htlc_sigs[0]) is bytes - - pending_local_commitment = self.pending_local_commitment - preimage_hex = pending_local_commitment.serialize_preimage(0) - pre_hash = Hash(bfh(preimage_hex)) - 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 - - 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}') - if len(htlc_sigs) != 0: # all sigs should have been popped above - raise Exception('failed verifying HTLC signatures: invalid amount of correct signatures') - - for pending_fee in self.fee_mgr: - if not self.constraints.is_initiator: - pending_fee[FUNDEE_SIGNED] = True - if self.constraints.is_initiator and pending_fee[FUNDEE_ACKED]: - pending_fee[FUNDER_SIGNED] = True - - self.process_new_offchain_ctx(pending_local_commitment, ours=True) - - - def revoke_current_commitment(self): - """ - RevokeCurrentCommitment revokes the next lowest unrevoked commitment - transaction in the local commitment chain. As a result the edge of our - revocation window is extended by one, and the tail of our local commitment - chain is advanced by a single commitment. This now lowest unrevoked - commitment becomes our currently accepted state within the channel. This - method also returns the set of HTLC's currently active within the commitment - transaction. This return value allows callers to act once an HTLC has been - locked into our commitment transaction. - """ - self.print_error("revoke_current_commitment") - - last_secret, this_point, next_point = self.points - - new_feerate = self.constraints.feerate - - for pending_fee in self.fee_mgr[:]: - if not self.constraints.is_initiator and pending_fee[FUNDEE_SIGNED]: - new_feerate = pending_fee.rate - self.fee_mgr.remove(pending_fee) - print("FEERATE CHANGE COMPLETE (non-initiator)") - if self.constraints.is_initiator and pending_fee[FUNDER_SIGNED]: - new_feerate = pending_fee.rate - self.fee_mgr.remove(pending_fee) - print("FEERATE CHANGE COMPLETE (initiator)") - - self.config[LOCAL]=self.config[LOCAL]._replace( - ctn=self.config[LOCAL].ctn + 1, - ) - self.constraints=self.constraints._replace( - feerate=new_feerate - ) - - self.local_commitment = self.pending_local_commitment - - return RevokeAndAck(last_secret, next_point), "current htlcs" - - @property - def points(self): - last_small_num = self.config[LOCAL].ctn - this_small_num = last_small_num + 1 - next_small_num = last_small_num + 2 - last_secret = get_per_commitment_secret_from_seed(self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - last_small_num) - this_secret = get_per_commitment_secret_from_seed(self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - this_small_num) - this_point = secret_to_pubkey(int.from_bytes(this_secret, 'big')) - next_secret = get_per_commitment_secret_from_seed(self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - next_small_num) - 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): - 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_sweeptx = maybe_create_sweeptx_for_our_ctx_to_local(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) - self.lnwatcher.add_sweep_tx(outpoint, ctx.txid(), encumbered_sweeptx) - - 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) - self.lnwatcher.add_sweep_tx(outpoint, ctx.txid(), encumbered_sweeptx) - - def receive_revocation(self, revocation): - """ - ReceiveRevocation processes a revocation sent by the remote party for the - lowest unrevoked commitment within their commitment chain. We receive a - revocation either during the initial session negotiation wherein revocation - windows are extended, or in response to a state update that we initiate. If - successful, then the remote commitment chain is advanced by a single - commitment, and a log compaction is attempted. - - Returns the forwarding package corresponding to the remote commitment height - that was revoked. - """ - self.print_error("receive_revocation") - - cur_point = self.config[REMOTE].current_per_commitment_point - derived_point = ecc.ECPrivkey(revocation.per_commitment_secret).get_public_key_bytes(compressed=True) - if cur_point != derived_point: - raise Exception('revoked secret not for current point') - - # 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 - prev_remote_commitment = self.pending_remote_commitment - - self.config[REMOTE].revocation_store.add_next_entry(revocation.per_commitment_secret) - self.process_new_revocation_secret(revocation.per_commitment_secret) - - def mark_settled(subject): - """ - find pending settlements for subject (LOCAL or REMOTE) and mark them settled, return value of settled htlcs - """ - old_amount = htlcsum(self.htlcs(subject, False)) - - for htlc_id in self.log[-subject]['settles']: - adds = self.log[subject]['adds'] - htlc = adds.pop(htlc_id) - self.settled[subject].append(htlc.amount_msat) - self.log[-subject]['settles'].clear() - - return old_amount - htlcsum(self.htlcs(subject, False)) - - sent_this_batch = mark_settled(LOCAL) - received_this_batch = mark_settled(REMOTE) - - next_point = self.config[REMOTE].next_per_commitment_point - - print("RECEIVED", received_this_batch) - print("SENT", sent_this_batch) - self.config[REMOTE]=self.config[REMOTE]._replace( - ctn=self.config[REMOTE].ctn + 1, - current_per_commitment_point=next_point, - next_per_commitment_point=revocation.next_per_commitment_point, - amount_msat=self.config[REMOTE].amount_msat + (sent_this_batch - received_this_batch) - ) - self.config[LOCAL]=self.config[LOCAL]._replace( - amount_msat = self.config[LOCAL].amount_msat + (received_this_batch - sent_this_batch) - ) - - for pending_fee in self.fee_mgr: - if self.constraints.is_initiator: - pending_fee[FUNDEE_ACKED] = True - - self.local_commitment = self.pending_local_commitment - self.remote_commitment = self.pending_remote_commitment - self.remote_commitment_to_be_revoked = prev_remote_commitment - return received_this_batch, sent_this_batch - - def balance(self, subject): - initial = self.config[subject].initial_msat - - initial -= sum(self.settled[subject]) - initial += sum(self.settled[-subject]) - - assert initial == self.config[subject].amount_msat - return initial - - def amounts(self): - remote_settled= htlcsum(self.htlcs(REMOTE, False)) - local_settled= htlcsum(self.htlcs(LOCAL, False)) - unsettled_local = htlcsum(self.htlcs(LOCAL, True)) - unsettled_remote = htlcsum(self.htlcs(REMOTE, True)) - remote_msat = self.config[REMOTE].amount_msat -\ - unsettled_remote + local_settled - remote_settled - local_msat = self.config[LOCAL].amount_msat -\ - unsettled_local + remote_settled - local_settled - return remote_msat, local_msat - - def included_htlcs(self, subject, htlc_initiator): - """ - return filter of non-dust htlcs for subjects commitment transaction, initiated by given party - """ - feerate = self.pending_feerate(subject) - conf = self.config[subject] - weight = HTLC_SUCCESS_WEIGHT if subject != htlc_initiator else HTLC_TIMEOUT_WEIGHT - htlcs = self.htlcs(htlc_initiator, only_pending=True) - fee_for_htlc = lambda htlc: htlc.amount_msat // 1000 - (weight * feerate // 1000) - return filter(lambda htlc: fee_for_htlc(htlc) >= conf.dust_limit_sat, htlcs) - - @property - def pending_remote_commitment(self): - this_point = self.config[REMOTE].next_per_commitment_point - return self.make_commitment(REMOTE, this_point) - - def pending_feerate(self, subject): - candidate = self.constraints.feerate - for pending_fee in self.fee_mgr: - x = pending_fee.pending_feerate(subject) - if x is not None: - candidate = x - - return candidate - - @property - def pending_local_commitment(self): - _, this_point, _ = self.points - return self.make_commitment(LOCAL, this_point) - - def total_msat(self, sub): - return sum(self.settled[sub]) - - def htlcs(self, subject, only_pending): - """ - only_pending: require the htlc's settlement to be pending (needs additional signatures/acks) - """ - update_log = self.log[subject] - other_log = self.log[-subject] - res = [] - for htlc in update_log['adds'].values(): - locked_in = htlc.locked_in[subject] - - if locked_in is None or only_pending == (htlc.htlc_id in other_log['settles']): - continue - res.append(htlc) - return res - - def settle_htlc(self, preimage, htlc_id): - """ - SettleHTLC attempts to settle an existing outstanding received HTLC. - """ - self.print_error("settle_htlc") - htlc = self.log[REMOTE]['adds'][htlc_id] - assert htlc.payment_hash == sha256(preimage) - self.log[LOCAL]['settles'].append(htlc_id) - - def receive_htlc_settle(self, preimage, htlc_index): - self.print_error("receive_htlc_settle") - htlc = self.log[LOCAL]['adds'][htlc_index] - assert htlc.payment_hash == sha256(preimage) - self.log[REMOTE]['settles'].append(htlc_index) - - def receive_fail_htlc(self, htlc_id): - self.print_error("receive_fail_htlc") - self.log[LOCAL]['adds'].pop(htlc_id) - - @property - def current_height(self): - return {LOCAL: self.config[LOCAL].ctn, REMOTE: self.config[REMOTE].ctn} - - @property - def pending_local_fee(self): - return self.constraints.capacity - sum(x[2] for x in self.pending_local_commitment.outputs()) - - def update_fee(self, feerate): - if not self.constraints.is_initiator: - raise Exception("only initiator can update_fee, this counterparty is not initiator") - pending_fee = FeeUpdate(self, rate=feerate) - self.fee_mgr.append(pending_fee) - - def receive_update_fee(self, feerate): - if self.constraints.is_initiator: - raise Exception("only the non-initiator can receive_update_fee, this counterparty is initiator") - pending_fee = FeeUpdate(self, rate=feerate) - self.fee_mgr.append(pending_fee) - - def remove_uncommitted_htlcs_from_log(self, subject): - """ - returns - - the htlcs with uncommited (not locked in) htlcs removed - - a list of htlc_ids that were removed - """ - removed = [] - htlcs = [] - for i in self.log[subject]['adds'].values(): - locked_in = i.locked_in[LOCAL] is not None or i.locked_in[REMOTE] is not None - if locked_in: - htlcs.append(i._asdict()) - else: - removed.append(i.htlc_id) - return htlcs, removed - - def to_save(self): - # need to forget about uncommited htlcs - # since we must assume they don't know about it, - # if it was not acked - remote_filtered, remote_removed = self.remove_uncommitted_htlcs_from_log(REMOTE) - local_filtered, local_removed = self.remove_uncommitted_htlcs_from_log(LOCAL) - to_save = { - "local_config": self.config[LOCAL], - "remote_config": self.config[REMOTE], - "channel_id": self.channel_id, - "short_channel_id": self.short_channel_id, - "constraints": self.constraints, - "funding_outpoint": self.funding_outpoint, - "node_id": self.node_id, - "remote_commitment_to_be_revoked": str(self.remote_commitment_to_be_revoked), - "remote_log": remote_filtered, - "local_log": local_filtered, - "onion_keys": {str(k): bh2u(v) for k, v in self.onion_keys.items()}, - "settled_local": self.settled[LOCAL], - "settled_remote": self.settled[REMOTE], - } - - # htlcs number must be monotonically increasing, - # so we have to decrease the counter - if len(remote_removed) != 0: - assert min(remote_removed) < to_save['remote_config'].next_htlc_id - to_save['remote_config'] = to_save['remote_config']._replace(next_htlc_id = min(remote_removed)) - - if len(local_removed) != 0: - assert min(local_removed) < to_save['local_config'].next_htlc_id - to_save['local_config'] = to_save['local_config']._replace(next_htlc_id = min(local_removed)) - - return to_save - - def serialize(self): - namedtuples_to_dict = lambda v: {i: j._asdict() if isinstance(j, tuple) else j for i, j in v._asdict().items()} - serialized_channel = {k: namedtuples_to_dict(v) if isinstance(v, tuple) else v for k, v in self.to_save().items()} - class MyJsonEncoder(json.JSONEncoder): - def default(self, o): - if isinstance(o, bytes): - return binascii.hexlify(o).decode("ascii") - if isinstance(o, RevocationStore): - return o.serialize() - return super(MyJsonEncoder, self) - dumped = MyJsonEncoder().encode(serialized_channel) - roundtripped = json.loads(dumped) - reconstructed = HTLCStateMachine(roundtripped) - if reconstructed.to_save() != self.to_save(): - from pprint import pformat - try: - from deepdiff import DeepDiff - except ImportError: - raise Exception("Channels did not roundtrip serialization without changes:\n" + pformat(reconstructed.to_save()) + "\n" + pformat(self.to_save())) - else: - raise Exception("Channels did not roundtrip serialization without changes:\n" + pformat(DeepDiff(reconstructed.to_save(), self.to_save()))) - return roundtripped - - def __str__(self): - return self.serialize() - - def make_commitment(self, subject, this_point) -> Transaction: - remote_msat, local_msat = self.amounts() - assert local_msat >= 0 - assert remote_msat >= 0 - this_config = self.config[subject] - other_config = self.config[-subject] - 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 = [] - 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)) - 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)) - if subject != LOCAL: - remote_msat, local_msat = local_msat, remote_msat - payment_pubkey = derive_pubkey(other_config.payment_basepoint.pubkey, this_point) - return make_commitment( - self.config[subject].ctn + 1, - this_config.multisig_key.pubkey, - other_config.multisig_key.pubkey, - payment_pubkey, - self.config[LOCAL].payment_basepoint.pubkey, - self.config[REMOTE].payment_basepoint.pubkey, - other_revocation_pubkey, - derive_pubkey(this_config.delayed_basepoint.pubkey, this_point), - other_config.to_self_delay, - *self.funding_outpoint, - self.constraints.capacity, - local_msat, - remote_msat, - this_config.dust_limit_sat, - self.pending_feerate(subject), - subject == LOCAL, - self.constraints.is_initiator, - htlcs=htlcs) - - def make_closing_tx(self, local_script: bytes, remote_script: bytes, fee_sat: Optional[int] = None) -> (bytes, int): - if fee_sat is None: - fee_sat = self.pending_local_fee - - _, outputs = make_outputs(fee_sat * 1000, True, - self.config[LOCAL].amount_msat, - self.config[REMOTE].amount_msat, - (TYPE_SCRIPT, bh2u(local_script)), - (TYPE_SCRIPT, bh2u(remote_script)), - [], self.config[LOCAL].dust_limit_sat) - - closing_tx = make_closing_tx(self.config[LOCAL].multisig_key.pubkey, - self.config[REMOTE].multisig_key.pubkey, - self.config[LOCAL].payment_basepoint.pubkey, - self.config[REMOTE].payment_basepoint.pubkey, - # TODO hardcoded we_are_initiator: - True, *self.funding_outpoint, self.constraints.capacity, - outputs) - - der_sig = bfh(closing_tx.sign_txin(0, self.config[LOCAL].multisig_key.privkey)) - sig = ecc.sig_string_from_der_sig(der_sig[:-1]) - return sig, fee_sat - -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(sweep_tx, csv_delay=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(sweep_tx, csv_delay=0) - - -def maybe_create_sweeptx_for_our_ctx_to_local(chan, ctx, our_pcp: bytes, - sweep_address) -> Optional[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) - 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=our_localdelayed_privkey.get_secret_bytes(), - is_revocation=False, - to_self_delay=to_self_delay) - - return EncumberedTransaction(sweep_tx, csv_delay=to_self_delay) - - -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/lnworker.py b/electrum/lnworker.py @@ -19,7 +19,7 @@ from .util import bh2u, bfh, PrintError, InvoiceError, resolve_dns_srv, is_ip_ad from .lnbase import Peer, aiosafe from .lnaddr import lnencode, LnAddr, lndecode from .ecc import der_sig_from_sig_string -from .lnhtlc import HTLCStateMachine +from .lnchan import Channel from .lnutil import (Outpoint, calc_short_channel_id, LNPeerAddr, get_compressed_pubkey_from_bech32, extract_nodeid, PaymentFailure, split_host_port, ConnStringFormatError, @@ -50,7 +50,7 @@ class LNWorker(PrintError): self.node_keypair = generate_keypair(self.ln_keystore, LnKeyFamily.NODE_KEY, 0) self.config = network.config self.peers = {} # type: Dict[bytes, Peer] # pubkey -> Peer - self.channels = {x.channel_id: x for x in map(HTLCStateMachine, wallet.storage.get("channels", []))} # type: Dict[bytes, HTLCStateMachine] + self.channels = {x.channel_id: x for x in map(Channel, wallet.storage.get("channels", []))} # type: Dict[bytes, HTLCStateMachine] for c in self.channels.values(): c.lnwatcher = network.lnwatcher c.sweep_address = self.sweep_address @@ -115,7 +115,7 @@ class LNWorker(PrintError): return peer def save_channel(self, openchannel): - assert type(openchannel) is HTLCStateMachine + assert type(openchannel) is Channel if openchannel.config[REMOTE].next_per_commitment_point == openchannel.config[REMOTE].current_per_commitment_point: raise Exception("Tried to save channel with next_point == current_point, this should not happen") with self.lock: diff --git a/electrum/tests/test_lnchan.py b/electrum/tests/test_lnchan.py @@ -0,0 +1,367 @@ +# ported from lnd 42de4400bff5105352d0552155f73589166d162b + +import unittest +import electrum.bitcoin as bitcoin +import electrum.lnbase as lnbase +import electrum.lnchan as lnchan +import electrum.lnutil as lnutil +import electrum.util as util +import os +import binascii + +from electrum.lnutil import SENT, LOCAL, REMOTE, RECEIVED + +def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate, is_initiator, local_amount, remote_amount, privkeys, other_pubkeys, seed, cur, nex, other_node_id, l_dust, r_dust, l_csv, r_csv): + assert local_amount > 0 + assert remote_amount > 0 + channel_id, _ = lnbase.channel_id_from_funding_tx(funding_txid, funding_index) + their_revocation_store = lnbase.RevocationStore() + + return { + "channel_id":channel_id, + "short_channel_id":channel_id[:8], + "funding_outpoint":lnbase.Outpoint(funding_txid, funding_index), + "remote_config":lnbase.RemoteConfig( + payment_basepoint=other_pubkeys[0], + multisig_key=other_pubkeys[1], + htlc_basepoint=other_pubkeys[2], + delayed_basepoint=other_pubkeys[3], + revocation_basepoint=other_pubkeys[4], + to_self_delay=r_csv, + dust_limit_sat=r_dust, + max_htlc_value_in_flight_msat=500000 * 1000, + max_accepted_htlcs=5, + initial_msat=remote_amount, + ctn = 0, + next_htlc_id = 0, + amount_msat=remote_amount, + + next_per_commitment_point=nex, + current_per_commitment_point=cur, + revocation_store=their_revocation_store, + ), + "local_config":lnbase.LocalConfig( + payment_basepoint=privkeys[0], + multisig_key=privkeys[1], + htlc_basepoint=privkeys[2], + delayed_basepoint=privkeys[3], + revocation_basepoint=privkeys[4], + to_self_delay=l_csv, + dust_limit_sat=l_dust, + max_htlc_value_in_flight_msat=500000 * 1000, + max_accepted_htlcs=5, + initial_msat=local_amount, + ctn = 0, + next_htlc_id = 0, + amount_msat=local_amount, + + per_commitment_secret_seed=seed, + funding_locked_received=True, + was_announced=False, + current_commitment_signature=None, + current_htlc_signatures=None, + ), + "constraints":lnbase.ChannelConstraints( + capacity=funding_sat, + is_initiator=is_initiator, + funding_txn_minimum_depth=3, + feerate=local_feerate, + ), + "node_id":other_node_id, + "remote_commitment_to_be_revoked": None, + 'onion_keys': {}, + } + +def bip32(sequence): + xprv, xpub = bitcoin.bip32_root(b"9dk", 'standard') + xprv, xpub = bitcoin.bip32_private_derivation(xprv, "m/", sequence) + xtype, depth, fingerprint, child_number, c, k = bitcoin.deserialize_xprv(xprv) + assert len(k) == 32 + assert type(k) is bytes + return k + +def create_test_channels(feerate=6000, local=None, remote=None): + funding_txid = binascii.hexlify(os.urandom(32)).decode("ascii") + funding_index = 0 + funding_sat = ((local + remote) // 1000) if local is not None and remote is not None else (bitcoin.COIN * 10) + local_amount = local if local is not None else (funding_sat * 1000 // 2) + remote_amount = remote if remote is not None else (funding_sat * 1000 // 2) + alice_raw = [ bip32("m/" + str(i)) for i in range(5) ] + bob_raw = [ bip32("m/" + str(i)) for i in range(5,11) ] + alice_privkeys = [lnutil.Keypair(lnbase.privkey_to_pubkey(x), x) for x in alice_raw] + bob_privkeys = [lnutil.Keypair(lnbase.privkey_to_pubkey(x), x) for x in bob_raw] + alice_pubkeys = [lnutil.OnlyPubkeyKeypair(x.pubkey) for x in alice_privkeys] + bob_pubkeys = [lnutil.OnlyPubkeyKeypair(x.pubkey) for x in bob_privkeys] + + alice_seed = os.urandom(32) + bob_seed = os.urandom(32) + + alice_cur = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(alice_seed, lnutil.RevocationStore.START_INDEX), "big")) + alice_next = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(alice_seed, lnutil.RevocationStore.START_INDEX - 1), "big")) + bob_cur = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(bob_seed, lnutil.RevocationStore.START_INDEX), "big")) + bob_next = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(bob_seed, lnutil.RevocationStore.START_INDEX - 1), "big")) + + return \ + lnchan.Channel( + create_channel_state(funding_txid, funding_index, funding_sat, feerate, True, local_amount, remote_amount, alice_privkeys, bob_pubkeys, alice_seed, bob_cur, bob_next, b"\x02"*33, l_dust=200, r_dust=1300, l_csv=5, r_csv=4), "alice"), \ + lnchan.Channel( + create_channel_state(funding_txid, funding_index, funding_sat, feerate, False, remote_amount, local_amount, bob_privkeys, alice_pubkeys, bob_seed, alice_cur, alice_next, b"\x01"*33, l_dust=1300, r_dust=200, l_csv=4, r_csv=5), "bob") + +one_bitcoin_in_msat = bitcoin.COIN * 1000 + +class TestFee(unittest.TestCase): + """ + test + https://github.com/lightningnetwork/lightning-rfc/blob/e0c436bd7a3ed6a028e1cb472908224658a14eca/03-transactions.md#requirements-2 + """ + def test_SimpleAddSettleWorkflow(self): + alice_channel, bob_channel = create_test_channels(253, 10000000000, 5000000000) + self.assertIn(9999817, [x[2] for x in alice_channel.local_commitment.outputs()]) + +class TestChannel(unittest.TestCase): + def assertOutputExistsByValue(self, tx, amt_sat): + for typ, scr, val in tx.outputs(): + if val == amt_sat: + break + else: + self.assertFalse() + + def setUp(self): + # Create a test channel which will be used for the duration of this + # unittest. The channel will be funded evenly with Alice having 5 BTC, + # and Bob having 5 BTC. + self.alice_channel, self.bob_channel = create_test_channels() + + self.paymentPreimage = b"\x01" * 32 + paymentHash = bitcoin.sha256(self.paymentPreimage) + self.htlc = { + 'payment_hash' : paymentHash, + 'amount_msat' : one_bitcoin_in_msat, + 'cltv_expiry' : 5, + } + + # First Alice adds the outgoing HTLC to her local channel's state + # update log. Then Alice sends this wire message over to Bob who adds + # this htlc to his remote state update log. + self.aliceHtlcIndex = self.alice_channel.add_htlc(self.htlc) + + self.bobHtlcIndex = self.bob_channel.receive_htlc(self.htlc) + self.htlc = self.bob_channel.log[lnutil.REMOTE]['adds'][0] + + def test_SimpleAddSettleWorkflow(self): + alice_channel, bob_channel = self.alice_channel, self.bob_channel + htlc = self.htlc + + # Next alice commits this change by sending a signature message. Since + # we expect the messages to be ordered, Bob will receive the HTLC we + # just sent before he receives this signature, so the signature will + # cover the HTLC. + aliceSig, aliceHtlcSigs = alice_channel.sign_next_commitment() + + self.assertEqual(len(aliceHtlcSigs), 1, "alice should generate one htlc signature") + + # Bob receives this signature message, and checks that this covers the + # state he has in his remote log. This includes the HTLC just sent + # from Alice. + bob_channel.receive_new_commitment(aliceSig, aliceHtlcSigs) + + # 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() + + # Bob finally send a signature for Alice's commitment transaction. + # This signature will cover the HTLC, since Bob will first send the + # revocation just created. The revocation also acks every received + # HTLC up to the point where Alice sent here signature. + bobSig, bobHtlcSigs = bob_channel.sign_next_commitment() + + # Alice then processes this revocation, sending her own revocation for + # her prior commitment transaction. Alice shouldn't have any HTLCs to + # forward since she's sending an outgoing HTLC. + alice_channel.receive_revocation(bobRevocation) + + # Alice then processes bob's signature, and since she just received + # the revocation, she expect this signature to cover everything up to + # the point where she sent her signature, including the HTLC. + alice_channel.receive_new_commitment(bobSig, bobHtlcSigs) + + # Alice then generates a revocation for bob. + aliceRevocation, _ = alice_channel.revoke_current_commitment() + + # Finally Bob processes Alice's revocation, at this point the new HTLC + # is fully locked in within both commitment transactions. Bob should + # also be able to forward an HTLC now that the HTLC has been locked + # into both commitment transactions. + bob_channel.receive_revocation(aliceRevocation) + + # At this point, both sides should have the proper number of satoshis + # sent, and commitment height updated within their local channel + # state. + aliceSent = 0 + bobSent = 0 + + self.assertEqual(alice_channel.total_msat(SENT), aliceSent, "alice has incorrect milli-satoshis sent") + self.assertEqual(alice_channel.total_msat(RECEIVED), bobSent, "alice has incorrect milli-satoshis received") + self.assertEqual(bob_channel.total_msat(SENT), bobSent, "bob has incorrect milli-satoshis sent") + self.assertEqual(bob_channel.total_msat(RECEIVED), aliceSent, "bob has incorrect milli-satoshis received") + self.assertEqual(bob_channel.config[LOCAL].ctn, 1, "bob has incorrect commitment height") + self.assertEqual(alice_channel.config[LOCAL].ctn, 1, "alice has incorrect commitment height") + + # Both commitment transactions should have three outputs, and one of + # them should be exactly the amount of the HTLC. + self.assertEqual(len(alice_channel.local_commitment.outputs()), 3, "alice should have three commitment outputs, instead have %s"% len(alice_channel.local_commitment.outputs())) + self.assertEqual(len(bob_channel.local_commitment.outputs()), 3, "bob should have three commitment outputs, instead have %s"% len(bob_channel.local_commitment.outputs())) + self.assertOutputExistsByValue(alice_channel.local_commitment, htlc.amount_msat // 1000) + self.assertOutputExistsByValue(bob_channel.local_commitment, htlc.amount_msat // 1000) + + # Now we'll repeat a similar exchange, this time with Bob settling the + # HTLC once he learns of the preimage. + preimage = self.paymentPreimage + bob_channel.settle_htlc(preimage, self.bobHtlcIndex) + + alice_channel.receive_htlc_settle(preimage, self.aliceHtlcIndex) + + bobSig2, bobHtlcSigs2 = bob_channel.sign_next_commitment() + alice_channel.receive_new_commitment(bobSig2, bobHtlcSigs2) + + aliceRevocation2, _ = alice_channel.revoke_current_commitment() + aliceSig2, aliceHtlcSigs2 = alice_channel.sign_next_commitment() + self.assertEqual(aliceHtlcSigs2, [], "alice should generate no htlc signatures") + + received, sent = bob_channel.receive_revocation(aliceRevocation2) + self.assertEqual(received, one_bitcoin_in_msat) + + bob_channel.receive_new_commitment(aliceSig2, aliceHtlcSigs2) + + bobRevocation2, _ = bob_channel.revoke_current_commitment() + alice_channel.receive_revocation(bobRevocation2) + + # At this point, Bob should have 6 BTC settled, with Alice still having + # 4 BTC. Alice's channel should show 1 BTC sent and Bob's channel + # should show 1 BTC received. They should also be at commitment height + # two, with the revocation window extended by 1 (5). + mSatTransferred = one_bitcoin_in_msat + self.assertEqual(alice_channel.total_msat(SENT), mSatTransferred, "alice satoshis sent incorrect") + self.assertEqual(alice_channel.total_msat(RECEIVED), 0, "alice satoshis received incorrect") + self.assertEqual(bob_channel.total_msat(RECEIVED), mSatTransferred, "bob satoshis received incorrect") + self.assertEqual(bob_channel.total_msat(SENT), 0, "bob satoshis sent incorrect") + self.assertEqual(bob_channel.current_height[LOCAL], 2, "bob has incorrect commitment height") + self.assertEqual(alice_channel.current_height[LOCAL], 2, "alice has incorrect commitment height") + + # The logs of both sides should now be cleared since the entry adding + # the HTLC should have been removed once both sides receive the + # revocation. + #self.assertEqual(alice_channel.local_update_log, [], "alice's local not updated, should be empty, has %s entries instead"% len(alice_channel.local_update_log)) + #self.assertEqual(alice_channel.remote_update_log, [], "alice's remote not updated, should be empty, has %s entries instead"% len(alice_channel.remote_update_log)) + alice_channel.update_fee(100000) + alice_channel.serialize() + + def alice_to_bob_fee_update(self): + fee = 111 + self.alice_channel.update_fee(fee) + self.bob_channel.receive_update_fee(fee) + return fee + + def test_UpdateFeeSenderCommits(self): + old_feerate = self.alice_channel.pending_feerate(LOCAL) + fee = self.alice_to_bob_fee_update() + + alice_channel, bob_channel = self.alice_channel, self.bob_channel + + self.assertEqual(self.alice_channel.pending_feerate(LOCAL), old_feerate) + alice_sig, alice_htlc_sigs = alice_channel.sign_next_commitment() + self.assertEqual(self.alice_channel.pending_feerate(LOCAL), old_feerate) + + bob_channel.receive_new_commitment(alice_sig, alice_htlc_sigs) + + self.assertNotEqual(fee, bob_channel.constraints.feerate) + rev, _ = bob_channel.revoke_current_commitment() + self.assertEqual(fee, bob_channel.constraints.feerate) + + bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment() + alice_channel.receive_revocation(rev) + alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs) + + self.assertNotEqual(fee, alice_channel.constraints.feerate) + rev, _ = alice_channel.revoke_current_commitment() + self.assertEqual(fee, alice_channel.constraints.feerate) + + bob_channel.receive_revocation(rev) + self.assertEqual(fee, bob_channel.constraints.feerate) + + + def test_UpdateFeeReceiverCommits(self): + fee = self.alice_to_bob_fee_update() + + alice_channel, bob_channel = self.alice_channel, self.bob_channel + + bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment() + alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs) + + alice_revocation, _ = alice_channel.revoke_current_commitment() + bob_channel.receive_revocation(alice_revocation) + alice_sig, alice_htlc_sigs = alice_channel.sign_next_commitment() + bob_channel.receive_new_commitment(alice_sig, alice_htlc_sigs) + + self.assertNotEqual(fee, bob_channel.constraints.feerate) + bob_revocation, _ = bob_channel.revoke_current_commitment() + self.assertEqual(fee, bob_channel.constraints.feerate) + + bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment() + alice_channel.receive_revocation(bob_revocation) + alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs) + + self.assertNotEqual(fee, alice_channel.constraints.feerate) + alice_revocation, _ = alice_channel.revoke_current_commitment() + self.assertEqual(fee, alice_channel.constraints.feerate) + + bob_channel.receive_revocation(alice_revocation) + self.assertEqual(fee, bob_channel.constraints.feerate) + + + +class TestDust(unittest.TestCase): + def test_DustLimit(self): + alice_channel, bob_channel = create_test_channels() + + paymentPreimage = b"\x01" * 32 + paymentHash = bitcoin.sha256(paymentPreimage) + fee_per_kw = alice_channel.constraints.feerate + self.assertEqual(fee_per_kw, 6000) + htlcAmt = 500 + lnutil.HTLC_TIMEOUT_WEIGHT * (fee_per_kw // 1000) + self.assertEqual(htlcAmt, 4478) + htlc = { + 'payment_hash' : paymentHash, + 'amount_msat' : 1000 * htlcAmt, + 'cltv_expiry' : 5, # also in create_test_channels + } + + aliceHtlcIndex = alice_channel.add_htlc(htlc) + bobHtlcIndex = bob_channel.receive_htlc(htlc) + force_state_transition(alice_channel, bob_channel) + self.assertEqual(len(alice_channel.local_commitment.outputs()), 3) + self.assertEqual(len(bob_channel.local_commitment.outputs()), 2) + default_fee = calc_static_fee(0) + self.assertEqual(bob_channel.pending_local_fee, default_fee + htlcAmt) + bob_channel.settle_htlc(paymentPreimage, bobHtlcIndex) + alice_channel.receive_htlc_settle(paymentPreimage, aliceHtlcIndex) + force_state_transition(bob_channel, alice_channel) + self.assertEqual(len(alice_channel.local_commitment.outputs()), 2) + self.assertEqual(alice_channel.total_msat(SENT) // 1000, htlcAmt) + +def force_state_transition(chanA, chanB): + chanB.receive_new_commitment(*chanA.sign_next_commitment()) + rev, _ = chanB.revoke_current_commitment() + bob_sig, bob_htlc_sigs = chanB.sign_next_commitment() + chanA.receive_revocation(rev) + chanA.receive_new_commitment(bob_sig, bob_htlc_sigs) + chanB.receive_revocation(chanA.revoke_current_commitment()[0]) + +# calcStaticFee calculates appropriate fees for commitment transactions. This +# function provides a simple way to allow test balance assertions to take fee +# calculations into account. +def calc_static_fee(numHTLCs): + commitWeight = 724 + htlcWeight = 172 + feePerKw = 24//4 * 1000 + return feePerKw * (commitWeight + htlcWeight*numHTLCs) // 1000 diff --git a/electrum/tests/test_lnhtlc.py b/electrum/tests/test_lnhtlc.py @@ -1,367 +0,0 @@ -# ported from lnd 42de4400bff5105352d0552155f73589166d162b - -import unittest -import electrum.bitcoin as bitcoin -import electrum.lnbase as lnbase -import electrum.lnhtlc as lnhtlc -import electrum.lnutil as lnutil -import electrum.util as util -import os -import binascii - -from electrum.lnutil import SENT, LOCAL, REMOTE, RECEIVED - -def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate, is_initiator, local_amount, remote_amount, privkeys, other_pubkeys, seed, cur, nex, other_node_id, l_dust, r_dust, l_csv, r_csv): - assert local_amount > 0 - assert remote_amount > 0 - channel_id, _ = lnbase.channel_id_from_funding_tx(funding_txid, funding_index) - their_revocation_store = lnbase.RevocationStore() - - return { - "channel_id":channel_id, - "short_channel_id":channel_id[:8], - "funding_outpoint":lnbase.Outpoint(funding_txid, funding_index), - "remote_config":lnbase.RemoteConfig( - payment_basepoint=other_pubkeys[0], - multisig_key=other_pubkeys[1], - htlc_basepoint=other_pubkeys[2], - delayed_basepoint=other_pubkeys[3], - revocation_basepoint=other_pubkeys[4], - to_self_delay=r_csv, - dust_limit_sat=r_dust, - max_htlc_value_in_flight_msat=500000 * 1000, - max_accepted_htlcs=5, - initial_msat=remote_amount, - ctn = 0, - next_htlc_id = 0, - amount_msat=remote_amount, - - next_per_commitment_point=nex, - current_per_commitment_point=cur, - revocation_store=their_revocation_store, - ), - "local_config":lnbase.LocalConfig( - payment_basepoint=privkeys[0], - multisig_key=privkeys[1], - htlc_basepoint=privkeys[2], - delayed_basepoint=privkeys[3], - revocation_basepoint=privkeys[4], - to_self_delay=l_csv, - dust_limit_sat=l_dust, - max_htlc_value_in_flight_msat=500000 * 1000, - max_accepted_htlcs=5, - initial_msat=local_amount, - ctn = 0, - next_htlc_id = 0, - amount_msat=local_amount, - - per_commitment_secret_seed=seed, - funding_locked_received=True, - was_announced=False, - current_commitment_signature=None, - current_htlc_signatures=None, - ), - "constraints":lnbase.ChannelConstraints( - capacity=funding_sat, - is_initiator=is_initiator, - funding_txn_minimum_depth=3, - feerate=local_feerate, - ), - "node_id":other_node_id, - "remote_commitment_to_be_revoked": None, - 'onion_keys': {}, - } - -def bip32(sequence): - xprv, xpub = bitcoin.bip32_root(b"9dk", 'standard') - xprv, xpub = bitcoin.bip32_private_derivation(xprv, "m/", sequence) - xtype, depth, fingerprint, child_number, c, k = bitcoin.deserialize_xprv(xprv) - assert len(k) == 32 - assert type(k) is bytes - return k - -def create_test_channels(feerate=6000, local=None, remote=None): - funding_txid = binascii.hexlify(os.urandom(32)).decode("ascii") - funding_index = 0 - funding_sat = ((local + remote) // 1000) if local is not None and remote is not None else (bitcoin.COIN * 10) - local_amount = local if local is not None else (funding_sat * 1000 // 2) - remote_amount = remote if remote is not None else (funding_sat * 1000 // 2) - alice_raw = [ bip32("m/" + str(i)) for i in range(5) ] - bob_raw = [ bip32("m/" + str(i)) for i in range(5,11) ] - alice_privkeys = [lnutil.Keypair(lnbase.privkey_to_pubkey(x), x) for x in alice_raw] - bob_privkeys = [lnutil.Keypair(lnbase.privkey_to_pubkey(x), x) for x in bob_raw] - alice_pubkeys = [lnutil.OnlyPubkeyKeypair(x.pubkey) for x in alice_privkeys] - bob_pubkeys = [lnutil.OnlyPubkeyKeypair(x.pubkey) for x in bob_privkeys] - - alice_seed = os.urandom(32) - bob_seed = os.urandom(32) - - alice_cur = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(alice_seed, lnutil.RevocationStore.START_INDEX), "big")) - alice_next = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(alice_seed, lnutil.RevocationStore.START_INDEX - 1), "big")) - bob_cur = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(bob_seed, lnutil.RevocationStore.START_INDEX), "big")) - bob_next = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(bob_seed, lnutil.RevocationStore.START_INDEX - 1), "big")) - - return \ - lnhtlc.HTLCStateMachine( - create_channel_state(funding_txid, funding_index, funding_sat, feerate, True, local_amount, remote_amount, alice_privkeys, bob_pubkeys, alice_seed, bob_cur, bob_next, b"\x02"*33, l_dust=200, r_dust=1300, l_csv=5, r_csv=4), "alice"), \ - lnhtlc.HTLCStateMachine( - create_channel_state(funding_txid, funding_index, funding_sat, feerate, False, remote_amount, local_amount, bob_privkeys, alice_pubkeys, bob_seed, alice_cur, alice_next, b"\x01"*33, l_dust=1300, r_dust=200, l_csv=4, r_csv=5), "bob") - -one_bitcoin_in_msat = bitcoin.COIN * 1000 - -class TestFee(unittest.TestCase): - """ - test - https://github.com/lightningnetwork/lightning-rfc/blob/e0c436bd7a3ed6a028e1cb472908224658a14eca/03-transactions.md#requirements-2 - """ - def test_SimpleAddSettleWorkflow(self): - alice_channel, bob_channel = create_test_channels(253, 10000000000, 5000000000) - self.assertIn(9999817, [x[2] for x in alice_channel.local_commitment.outputs()]) - -class TestLNBaseHTLCStateMachine(unittest.TestCase): - def assertOutputExistsByValue(self, tx, amt_sat): - for typ, scr, val in tx.outputs(): - if val == amt_sat: - break - else: - self.assertFalse() - - def setUp(self): - # Create a test channel which will be used for the duration of this - # unittest. The channel will be funded evenly with Alice having 5 BTC, - # and Bob having 5 BTC. - self.alice_channel, self.bob_channel = create_test_channels() - - self.paymentPreimage = b"\x01" * 32 - paymentHash = bitcoin.sha256(self.paymentPreimage) - self.htlc = { - 'payment_hash' : paymentHash, - 'amount_msat' : one_bitcoin_in_msat, - 'cltv_expiry' : 5, - } - - # First Alice adds the outgoing HTLC to her local channel's state - # update log. Then Alice sends this wire message over to Bob who adds - # this htlc to his remote state update log. - self.aliceHtlcIndex = self.alice_channel.add_htlc(self.htlc) - - self.bobHtlcIndex = self.bob_channel.receive_htlc(self.htlc) - self.htlc = self.bob_channel.log[lnutil.REMOTE]['adds'][0] - - def test_SimpleAddSettleWorkflow(self): - alice_channel, bob_channel = self.alice_channel, self.bob_channel - htlc = self.htlc - - # Next alice commits this change by sending a signature message. Since - # we expect the messages to be ordered, Bob will receive the HTLC we - # just sent before he receives this signature, so the signature will - # cover the HTLC. - aliceSig, aliceHtlcSigs = alice_channel.sign_next_commitment() - - self.assertEqual(len(aliceHtlcSigs), 1, "alice should generate one htlc signature") - - # Bob receives this signature message, and checks that this covers the - # state he has in his remote log. This includes the HTLC just sent - # from Alice. - bob_channel.receive_new_commitment(aliceSig, aliceHtlcSigs) - - # 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() - - # Bob finally send a signature for Alice's commitment transaction. - # This signature will cover the HTLC, since Bob will first send the - # revocation just created. The revocation also acks every received - # HTLC up to the point where Alice sent here signature. - bobSig, bobHtlcSigs = bob_channel.sign_next_commitment() - - # Alice then processes this revocation, sending her own revocation for - # her prior commitment transaction. Alice shouldn't have any HTLCs to - # forward since she's sending an outgoing HTLC. - alice_channel.receive_revocation(bobRevocation) - - # Alice then processes bob's signature, and since she just received - # the revocation, she expect this signature to cover everything up to - # the point where she sent her signature, including the HTLC. - alice_channel.receive_new_commitment(bobSig, bobHtlcSigs) - - # Alice then generates a revocation for bob. - aliceRevocation, _ = alice_channel.revoke_current_commitment() - - # Finally Bob processes Alice's revocation, at this point the new HTLC - # is fully locked in within both commitment transactions. Bob should - # also be able to forward an HTLC now that the HTLC has been locked - # into both commitment transactions. - bob_channel.receive_revocation(aliceRevocation) - - # At this point, both sides should have the proper number of satoshis - # sent, and commitment height updated within their local channel - # state. - aliceSent = 0 - bobSent = 0 - - self.assertEqual(alice_channel.total_msat(SENT), aliceSent, "alice has incorrect milli-satoshis sent") - self.assertEqual(alice_channel.total_msat(RECEIVED), bobSent, "alice has incorrect milli-satoshis received") - self.assertEqual(bob_channel.total_msat(SENT), bobSent, "bob has incorrect milli-satoshis sent") - self.assertEqual(bob_channel.total_msat(RECEIVED), aliceSent, "bob has incorrect milli-satoshis received") - self.assertEqual(bob_channel.config[LOCAL].ctn, 1, "bob has incorrect commitment height") - self.assertEqual(alice_channel.config[LOCAL].ctn, 1, "alice has incorrect commitment height") - - # Both commitment transactions should have three outputs, and one of - # them should be exactly the amount of the HTLC. - self.assertEqual(len(alice_channel.local_commitment.outputs()), 3, "alice should have three commitment outputs, instead have %s"% len(alice_channel.local_commitment.outputs())) - self.assertEqual(len(bob_channel.local_commitment.outputs()), 3, "bob should have three commitment outputs, instead have %s"% len(bob_channel.local_commitment.outputs())) - self.assertOutputExistsByValue(alice_channel.local_commitment, htlc.amount_msat // 1000) - self.assertOutputExistsByValue(bob_channel.local_commitment, htlc.amount_msat // 1000) - - # Now we'll repeat a similar exchange, this time with Bob settling the - # HTLC once he learns of the preimage. - preimage = self.paymentPreimage - bob_channel.settle_htlc(preimage, self.bobHtlcIndex) - - alice_channel.receive_htlc_settle(preimage, self.aliceHtlcIndex) - - bobSig2, bobHtlcSigs2 = bob_channel.sign_next_commitment() - alice_channel.receive_new_commitment(bobSig2, bobHtlcSigs2) - - aliceRevocation2, _ = alice_channel.revoke_current_commitment() - aliceSig2, aliceHtlcSigs2 = alice_channel.sign_next_commitment() - self.assertEqual(aliceHtlcSigs2, [], "alice should generate no htlc signatures") - - received, sent = bob_channel.receive_revocation(aliceRevocation2) - self.assertEqual(received, one_bitcoin_in_msat) - - bob_channel.receive_new_commitment(aliceSig2, aliceHtlcSigs2) - - bobRevocation2, _ = bob_channel.revoke_current_commitment() - alice_channel.receive_revocation(bobRevocation2) - - # At this point, Bob should have 6 BTC settled, with Alice still having - # 4 BTC. Alice's channel should show 1 BTC sent and Bob's channel - # should show 1 BTC received. They should also be at commitment height - # two, with the revocation window extended by 1 (5). - mSatTransferred = one_bitcoin_in_msat - self.assertEqual(alice_channel.total_msat(SENT), mSatTransferred, "alice satoshis sent incorrect") - self.assertEqual(alice_channel.total_msat(RECEIVED), 0, "alice satoshis received incorrect") - self.assertEqual(bob_channel.total_msat(RECEIVED), mSatTransferred, "bob satoshis received incorrect") - self.assertEqual(bob_channel.total_msat(SENT), 0, "bob satoshis sent incorrect") - self.assertEqual(bob_channel.current_height[LOCAL], 2, "bob has incorrect commitment height") - self.assertEqual(alice_channel.current_height[LOCAL], 2, "alice has incorrect commitment height") - - # The logs of both sides should now be cleared since the entry adding - # the HTLC should have been removed once both sides receive the - # revocation. - #self.assertEqual(alice_channel.local_update_log, [], "alice's local not updated, should be empty, has %s entries instead"% len(alice_channel.local_update_log)) - #self.assertEqual(alice_channel.remote_update_log, [], "alice's remote not updated, should be empty, has %s entries instead"% len(alice_channel.remote_update_log)) - alice_channel.update_fee(100000) - alice_channel.serialize() - - def alice_to_bob_fee_update(self): - fee = 111 - self.alice_channel.update_fee(fee) - self.bob_channel.receive_update_fee(fee) - return fee - - def test_UpdateFeeSenderCommits(self): - old_feerate = self.alice_channel.pending_feerate(LOCAL) - fee = self.alice_to_bob_fee_update() - - alice_channel, bob_channel = self.alice_channel, self.bob_channel - - self.assertEqual(self.alice_channel.pending_feerate(LOCAL), old_feerate) - alice_sig, alice_htlc_sigs = alice_channel.sign_next_commitment() - self.assertEqual(self.alice_channel.pending_feerate(LOCAL), old_feerate) - - bob_channel.receive_new_commitment(alice_sig, alice_htlc_sigs) - - self.assertNotEqual(fee, bob_channel.constraints.feerate) - rev, _ = bob_channel.revoke_current_commitment() - self.assertEqual(fee, bob_channel.constraints.feerate) - - bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment() - alice_channel.receive_revocation(rev) - alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs) - - self.assertNotEqual(fee, alice_channel.constraints.feerate) - rev, _ = alice_channel.revoke_current_commitment() - self.assertEqual(fee, alice_channel.constraints.feerate) - - bob_channel.receive_revocation(rev) - self.assertEqual(fee, bob_channel.constraints.feerate) - - - def test_UpdateFeeReceiverCommits(self): - fee = self.alice_to_bob_fee_update() - - alice_channel, bob_channel = self.alice_channel, self.bob_channel - - bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment() - alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs) - - alice_revocation, _ = alice_channel.revoke_current_commitment() - bob_channel.receive_revocation(alice_revocation) - alice_sig, alice_htlc_sigs = alice_channel.sign_next_commitment() - bob_channel.receive_new_commitment(alice_sig, alice_htlc_sigs) - - self.assertNotEqual(fee, bob_channel.constraints.feerate) - bob_revocation, _ = bob_channel.revoke_current_commitment() - self.assertEqual(fee, bob_channel.constraints.feerate) - - bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment() - alice_channel.receive_revocation(bob_revocation) - alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs) - - self.assertNotEqual(fee, alice_channel.constraints.feerate) - alice_revocation, _ = alice_channel.revoke_current_commitment() - self.assertEqual(fee, alice_channel.constraints.feerate) - - bob_channel.receive_revocation(alice_revocation) - self.assertEqual(fee, bob_channel.constraints.feerate) - - - -class TestLNHTLCDust(unittest.TestCase): - def test_HTLCDustLimit(self): - alice_channel, bob_channel = create_test_channels() - - paymentPreimage = b"\x01" * 32 - paymentHash = bitcoin.sha256(paymentPreimage) - fee_per_kw = alice_channel.constraints.feerate - self.assertEqual(fee_per_kw, 6000) - htlcAmt = 500 + lnutil.HTLC_TIMEOUT_WEIGHT * (fee_per_kw // 1000) - self.assertEqual(htlcAmt, 4478) - htlc = { - 'payment_hash' : paymentHash, - 'amount_msat' : 1000 * htlcAmt, - 'cltv_expiry' : 5, # also in create_test_channels - } - - aliceHtlcIndex = alice_channel.add_htlc(htlc) - bobHtlcIndex = bob_channel.receive_htlc(htlc) - force_state_transition(alice_channel, bob_channel) - self.assertEqual(len(alice_channel.local_commitment.outputs()), 3) - self.assertEqual(len(bob_channel.local_commitment.outputs()), 2) - default_fee = calc_static_fee(0) - self.assertEqual(bob_channel.pending_local_fee, default_fee + htlcAmt) - bob_channel.settle_htlc(paymentPreimage, bobHtlcIndex) - alice_channel.receive_htlc_settle(paymentPreimage, aliceHtlcIndex) - force_state_transition(bob_channel, alice_channel) - self.assertEqual(len(alice_channel.local_commitment.outputs()), 2) - self.assertEqual(alice_channel.total_msat(SENT) // 1000, htlcAmt) - -def force_state_transition(chanA, chanB): - chanB.receive_new_commitment(*chanA.sign_next_commitment()) - rev, _ = chanB.revoke_current_commitment() - bob_sig, bob_htlc_sigs = chanB.sign_next_commitment() - chanA.receive_revocation(rev) - chanA.receive_new_commitment(bob_sig, bob_htlc_sigs) - chanB.receive_revocation(chanA.revoke_current_commitment()[0]) - -# calcStaticFee calculates appropriate fees for commitment transactions. This -# function provides a simple way to allow test balance assertions to take fee -# calculations into account. -def calc_static_fee(numHTLCs): - commitWeight = 724 - htlcWeight = 172 - feePerKw = 24//4 * 1000 - return feePerKw * (commitWeight + htlcWeight*numHTLCs) // 1000 diff --git a/electrum/tests/test_lnutil.py b/electrum/tests/test_lnutil.py @@ -7,7 +7,7 @@ from electrum.lnutil import (RevocationStore, get_per_commitment_secret_from_see derive_pubkey, make_htlc_tx, extract_ctn_from_tx, UnableToDeriveSecret, get_compressed_pubkey_from_bech32, split_host_port, ConnStringFormatError, ScriptHtlc, extract_nodeid) -from electrum import lnhtlc +from electrum import lnchan from electrum.util import bh2u, bfh from electrum.transaction import Transaction @@ -496,7 +496,7 @@ class TestLNUtil(unittest.TestCase): (1, 2000 * 1000), (3, 3000 * 1000), (4, 4000 * 1000)]: - htlc_obj[num] = lnhtlc.UpdateAddHtlc(amount_msat=msat, payment_hash=bitcoin.sha256(htlc_payment_preimage[num]), cltv_expiry=None, htlc_id=None) + htlc_obj[num] = lnchan.UpdateAddHtlc(amount_msat=msat, payment_hash=bitcoin.sha256(htlc_payment_preimage[num]), cltv_expiry=None, htlc_id=None) htlcs = [ScriptHtlc(htlc[x], htlc_obj[x]) for x in range(5)] our_commit_tx = make_commitment(