URI: 
       tlnhtlc: save logs and feeupdates - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit d5d9270d0c3d09972ec94fa90c016449a93077ea
   DIR parent eca5545004bbe07ba7ead674bedff290b4664f33
  HTML Author: Janus <ysangkok@gmail.com>
       Date:   Tue, 18 Sep 2018 18:38:57 +0200
       
       lnhtlc: save logs and feeupdates
       
       Diffstat:
         M electrum/gui/qt/channels_list.py    |       5 +++--
         M electrum/lnbase.py                  |      25 ++++++++++++++-----------
         M electrum/lnhtlc.py                  |     138 +++++++++++++++++++++----------
         M electrum/lnutil.py                  |       2 +-
         M electrum/tests/test_lnhtlc.py       |      31 ++++++++++++++++++-------------
       
       5 files changed, 131 insertions(+), 70 deletions(-)
       ---
   DIR diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py
       t@@ -6,6 +6,7 @@ from electrum.util import inv_dict, bh2u, bfh
        from electrum.i18n import _
        from electrum.lnhtlc import HTLCStateMachine
        from electrum.lnaddr import lndecode
       +from electrum.lnutil import LOCAL, REMOTE
        
        from .util import MyTreeWidget, SortableTreeWidgetItem, WindowModalDialog, Buttons, OkButton, CancelButton
        from .amountedit import BTCAmountEdit
       t@@ -24,8 +25,8 @@ class ChannelsList(MyTreeWidget):
            def format_fields(self, chan):
                return [
                    bh2u(chan.node_id),
       -            self.parent.format_amount(chan.local_state.amount_msat//1000),
       -            self.parent.format_amount(chan.remote_state.amount_msat//1000),
       +            self.parent.format_amount(chan.balance(LOCAL)//1000),
       +            self.parent.format_amount(chan.balance(REMOTE)//1000),
                    chan.get_state()
                ]
        
   DIR diff --git a/electrum/lnbase.py b/electrum/lnbase.py
       t@@ -8,6 +8,7 @@ from collections import namedtuple, defaultdict, OrderedDict, defaultdict
        from .lnutil import Outpoint, ChannelConfig, LocalState, RemoteState, Keypair, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore
        from .lnutil import sign_and_get_sig_string, funding_output_script, get_ecdh, get_per_commitment_secret_from_seed
        from .lnutil import secret_to_pubkey, LNPeerAddr, PaymentFailure
       +from .lnutil import LOCAL, REMOTE
        from .bitcoin import COIN
        
        from ecdsa.util import sigdecode_der, sigencode_string_canonize, sigdecode_string
       t@@ -33,7 +34,7 @@ from .util import PrintError, bh2u, print_error, bfh, aiosafe
        from .transaction import opcodes, Transaction, TxOutput
        from .lnonion import new_onion_packet, OnionHopsDataSingle, OnionPerHop, decode_onion_error, ONION_FAILURE_CODE_MAP
        from .lnaddr import lndecode
       -from .lnhtlc import UpdateAddHtlc, HTLCStateMachine, RevokeAndAck, SettleHtlc
       +from .lnhtlc import HTLCStateMachine, RevokeAndAck
        
        def channel_id_from_funding_tx(funding_txid, funding_index):
            funding_txid_bytes = bytes.fromhex(funding_txid)[::-1]
       t@@ -496,7 +497,8 @@ class Peer(PrintError):
                    to_self_delay=143,
                    dust_limit_sat=546,
                    max_htlc_value_in_flight_msat=0xffffffffffffffff,
       -            max_accepted_htlcs=5
       +            max_accepted_htlcs=5,
       +            initial_msat=funding_sat * 1000 - push_msat,
                )
                # TODO derive this?
                per_commitment_secret_seed = 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100.to_bytes(32, 'big')
       t@@ -536,7 +538,8 @@ class Peer(PrintError):
                    to_self_delay=int.from_bytes(payload['to_self_delay'], byteorder='big'),
                    dust_limit_sat=int.from_bytes(payload['dust_limit_satoshis'], byteorder='big'),
                    max_htlc_value_in_flight_msat=int.from_bytes(payload['max_htlc_value_in_flight_msat'], 'big'),
       -            max_accepted_htlcs=int.from_bytes(payload["max_accepted_htlcs"], 'big')
       +            max_accepted_htlcs=int.from_bytes(payload["max_accepted_htlcs"], 'big'),
       +            initial_msat=push_msat
                )
                funding_txn_minimum_depth = int.from_bytes(payload['minimum_depth'], 'big')
                assert remote_config.dust_limit_sat < 600
       t@@ -844,9 +847,9 @@ class Peer(PrintError):
                onion = new_onion_packet([x.node_id for x in route], self.secret_key, hops_data, associated_data)
                amount_msat += total_fee
                # FIXME this below will probably break with multiple HTLCs
       -        msat_local = chan.local_state.amount_msat - amount_msat
       -        msat_remote = chan.remote_state.amount_msat + amount_msat
       -        htlc = UpdateAddHtlc(amount_msat, payment_hash, final_cltv_expiry_with_deltas)
       +        msat_local = chan.balance(LOCAL) - amount_msat
       +        msat_remote = chan.balance(REMOTE) + amount_msat
       +        htlc = {'amount_msat':amount_msat, 'payment_hash':payment_hash, 'cltv_expiry':final_cltv_expiry_with_deltas}
        
                # FIXME if we raise here, this channel will not get blacklisted, and the payment can never succeed,
                # as we will just keep retrying this same path. using the current blacklisting is not a solution as
       t@@ -861,10 +864,10 @@ class Peer(PrintError):
                    # FIXME what about channel_reserve_satoshis? will the remote fail the channel if we go below? test.
                    raise PaymentFailure('not enough local balance')
        
       -        self.send_message(gen_msg("update_add_htlc", channel_id=chan.channel_id, id=chan.local_state.next_htlc_id, cltv_expiry=final_cltv_expiry_with_deltas, amount_msat=amount_msat, payment_hash=payment_hash, onion_routing_packet=onion.to_bytes()))
       +        htlc_id = chan.add_htlc(htlc)
       +        self.send_message(gen_msg("update_add_htlc", channel_id=chan.channel_id, id=htlc_id, cltv_expiry=final_cltv_expiry_with_deltas, amount_msat=amount_msat, payment_hash=payment_hash, onion_routing_packet=onion.to_bytes()))
        
       -        chan.add_htlc(htlc)
       -        self.attempted_route[(chan.channel_id, htlc.htlc_id)] = route
       +        self.attempted_route[(chan.channel_id, htlc_id)] = route
        
                sig_64, htlc_sigs = chan.sign_next_commitment()
                self.send_message(gen_msg("commitment_signed", channel_id=chan.channel_id, signature=sig_64, num_htlcs=len(htlc_sigs), htlc_signature=b"".join(htlc_sigs)))
       t@@ -947,7 +950,7 @@ class Peer(PrintError):
                assert amount_msat == expected_received_msat
                payment_hash = htlc["payment_hash"]
        
       -        htlc = UpdateAddHtlc(amount_msat, payment_hash, cltv_expiry)
       +        htlc = {'amount_msat': amount_msat, 'payment_hash':payment_hash, 'cltv_expiry':cltv_expiry}
        
                chan.receive_htlc(htlc)
        
       t@@ -967,7 +970,7 @@ class Peer(PrintError):
                # remote commitment transaction without htlcs
                # FIXME why is this not using the HTLC state machine?
                bare_ctx = chan.make_commitment(chan.remote_state.ctn + 1, False, chan.remote_state.next_per_commitment_point,
       -            chan.remote_state.amount_msat - expected_received_msat, chan.local_state.amount_msat + expected_received_msat)
       +            chan.balance(REMOTE) - expected_received_msat, chan.balance(LOCAL) + expected_received_msat)
                self.lnwatcher.process_new_offchain_ctx(chan, bare_ctx, ours=False)
                sig_64 = sign_and_get_sig_string(bare_ctx, chan.local_config, chan.remote_config)
                self.send_message(gen_msg("commitment_signed", channel_id=channel_id, signature=sig_64, num_htlcs=0))
   DIR diff --git a/electrum/lnhtlc.py b/electrum/lnhtlc.py
       t@@ -16,7 +16,7 @@ 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, extract_ctn_from_tx_and_chan
       -from .lnutil import LOCAL, REMOTE, SENT, RECEIVED
       +from .lnutil import LOCAL, REMOTE, SENT, RECEIVED, HTLCOwner
        from .transaction import Transaction
        
        
       t@@ -34,12 +34,22 @@ FUNDEE_ACKED = FeeUpdateProgress.FUNDEE_ACKED
        FUNDER_SIGNED = FeeUpdateProgress.FUNDER_SIGNED
        COMMITTED = FeeUpdateProgress.COMMITTED
        
       -class FeeUpdate:
       +from collections import namedtuple
        
       -    def __init__(self, chan, feerate):
       -        self.rate = feerate
       -        self.proposed = chan.remote_state.ctn if not chan.constraints.is_initiator else chan.local_state.ctn
       -        self.progress = {FUNDEE_SIGNED: None, FUNDEE_ACKED: None, FUNDER_SIGNED: None, COMMITTED: None}
       +class FeeUpdate:
       +    def __init__(self, chan, **kwargs):
       +        if 'rate' in kwargs:
       +            self.rate = kwargs['rate']
       +        else:
       +            assert False
       +        if 'proposed' not in kwargs:
       +            self.proposed = chan.remote_state.ctn if not chan.constraints.is_initiator else chan.local_state.ctn
       +        else:
       +            self.proposed = kwargs['proposed']
       +        if 'progress' not in kwargs:
       +            self.progress = {FUNDEE_SIGNED: None, FUNDEE_ACKED: None, FUNDER_SIGNED: None, COMMITTED: None}
       +        else:
       +            self.progress = {FeeUpdateProgress[x.partition('.')[2]]: y for x,y in kwargs['progress'].items()}
                self.chan = chan
        
            @property
       t@@ -65,30 +75,30 @@ class FeeUpdate:
                if subject == LOCAL and not self.chan.constraints.is_initiator:
                    return self.rate
        
       -class UpdateAddHtlc:
       -    def __init__(self, amount_msat, payment_hash, cltv_expiry):
       -        self.amount_msat = amount_msat
       -        self.payment_hash = payment_hash
       -        self.cltv_expiry = cltv_expiry
       -
       -        # the height the htlc was locked in at, or None
       -        self.locked_in = {LOCAL: None, REMOTE: None}
       -
       -        self.settled = {LOCAL: None, REMOTE: None}
       -
       -        self.htlc_id = None
       -
       -    def as_tuple(self):
       -        return (self.htlc_id, self.amount_msat, self.payment_hash, self.cltv_expiry, self.locked_in[REMOTE], self.locked_in[LOCAL], self.settled)
       -
       -    def __hash__(self):
       -        return hash(self.as_tuple())
       -
       -    def __eq__(self, o):
       -        return type(o) is UpdateAddHtlc and self.as_tuple() == o.as_tuple()
       -
       -    def __repr__(self):
       -        return "UpdateAddHtlc" + str(self.as_tuple())
       +    def to_save(self):
       +        return {'rate': self.rate, 'proposed': self.proposed, 'progress': self.progress}
       +
       +class UpdateAddHtlc(namedtuple('UpdateAddHtlc', ['amount_msat', 'payment_hash', 'cltv_expiry', 'settled', '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()}
       +            args[4] = {HTLCOwner(int(x)): y for x,y in args[4].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']}
       +        if 'settled' not in kwargs:
       +            kwargs['settled'] = {LOCAL: None, REMOTE: None}
       +        else:
       +            kwargs['settled'] = {HTLCOwner(int(x)): y for x,y in kwargs['settled']}
       +        return super().__new__(cls, **kwargs)
        
        is_key = lambda k: k.endswith("_basepoint") or k.endswith("_key")
        
       t@@ -155,10 +165,22 @@ class HTLCStateMachine(PrintError):
                self.remote_commitment_to_be_revoked = Transaction(state["remote_commitment_to_be_revoked"])
        
                self.log = {LOCAL: [], REMOTE: []}
       +        for strname, subject in [('remote_log', REMOTE), ('local_log', LOCAL)]:
       +            if strname not in state: continue
       +            for typ,y in state[strname]:
       +                if typ == "UpdateAddHtlc":
       +                    self.log[subject].append(UpdateAddHtlc(*decodeAll(y)))
       +                elif typ == "SettleHtlc":
       +                    self.log[subject].append(SettleHtlc(*decodeAll(y)))
       +                else:
       +                    assert False
        
                self.name = name
        
                self.fee_mgr = []
       +        if 'fee_updates' in state:
       +            for y in state['fee_updates']:
       +                self.fee_mgr.append(FeeUpdate(self, **y))
        
                self.local_commitment = self.pending_local_commitment
                self.remote_commitment = self.pending_remote_commitment
       t@@ -190,13 +212,12 @@ class HTLCStateMachine(PrintError):
                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 UpdateAddHtlc
       +        assert type(htlc) is dict
       +        htlc = UpdateAddHtlc(**htlc, htlc_id=self.local_state.next_htlc_id)
                self.log[LOCAL].append(htlc)
                self.print_error("add_htlc")
       -        htlc_id = self.local_state.next_htlc_id
       -        self.local_state=self.local_state._replace(next_htlc_id=htlc_id + 1)
       -        htlc.htlc_id = htlc_id
       -        return htlc_id
       +        self.local_state=self.local_state._replace(next_htlc_id=htlc.htlc_id + 1)
       +        return htlc.htlc_id
        
            def receive_htlc(self, htlc):
                """
       t@@ -204,13 +225,12 @@ class HTLCStateMachine(PrintError):
                method should be called in response to receiving a new HTLC from the remote
                party.
                """
       -        self.print_error("receive_htlc")
       -        assert type(htlc) is UpdateAddHtlc
       +        assert type(htlc) is dict
       +        htlc = UpdateAddHtlc(**htlc, htlc_id = self.remote_state.next_htlc_id)
                self.log[REMOTE].append(htlc)
       -        htlc_id = self.remote_state.next_htlc_id
       -        self.remote_state=self.remote_state._replace(next_htlc_id=htlc_id + 1)
       -        htlc.htlc_id = htlc_id
       -        return htlc_id
       +        self.print_error("receive_htlc")
       +        self.remote_state=self.remote_state._replace(next_htlc_id=htlc.htlc_id + 1)
       +        return htlc.htlc_id
        
            def sign_next_commitment(self):
                """
       t@@ -431,6 +451,9 @@ class HTLCStateMachine(PrintError):
                    amount_msat = self.local_state.amount_msat + (received_this_batch - sent_this_batch)
                )
        
       +        self.balance(LOCAL)
       +        self.balance(REMOTE)
       +
                for pending_fee in self.fee_mgr:
                    if pending_fee.is_proposed():
                        if self.constraints.is_initiator:
       t@@ -441,6 +464,26 @@ class HTLCStateMachine(PrintError):
                self.remote_commitment_to_be_revoked = prev_remote_commitment
                return received_this_batch, sent_this_batch
        
       +    def balance(self, subject):
       +        initial = self.local_config.initial_msat if subject == LOCAL else self.remote_config.initial_msat
       +
       +        for x in self.log[-subject]:
       +            if type(x) is not SettleHtlc: continue
       +            htlc = self.lookup_htlc(self.log[subject], x.htlc_id)
       +            htlc_height = htlc.settled[subject]
       +            if htlc_height is not None and htlc_height <= self.current_height[subject]:
       +                initial -= htlc.amount_msat
       +
       +        for x in self.log[subject]:
       +            if type(x) is not SettleHtlc: continue
       +            htlc = self.lookup_htlc(self.log[-subject], x.htlc_id)
       +            htlc_height = htlc.settled[-subject]
       +            if htlc_height is not None and htlc_height <= self.current_height[-subject]:
       +                initial += htlc.amount_msat
       +
       +        assert initial == (self.local_state.amount_msat if subject == LOCAL else self.remote_state.amount_msat)
       +        return initial
       +
            @staticmethod
            def htlcsum(htlcs):
                amount_unsettled = 0
       t@@ -611,13 +654,13 @@ class HTLCStateMachine(PrintError):
            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, feerate)
       +        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, feerate)
       +        pending_fee = FeeUpdate(self, rate=feerate)
                self.fee_mgr.append(pending_fee)
        
            def to_save(self):
       t@@ -632,6 +675,9 @@ class HTLCStateMachine(PrintError):
                        "funding_outpoint": self.funding_outpoint,
                        "node_id": self.node_id,
                        "remote_commitment_to_be_revoked": str(self.remote_commitment_to_be_revoked),
       +                "remote_log": [(type(x).__name__, x) for x in self.log[REMOTE]],
       +                "local_log": [(type(x).__name__, x) for x in self.log[LOCAL]],
       +                "fee_updates": [x.to_save() for x in self.fee_mgr],
                }
        
            def serialize(self):
       t@@ -643,7 +689,13 @@ class HTLCStateMachine(PrintError):
                            return binascii.hexlify(o).decode("ascii")
                        if isinstance(o, RevocationStore):
                            return o.serialize()
       +                if isinstance(o, SettleHtlc):
       +                    return json.dumps(('SettleHtlc', namedtuples_to_dict(o)))
       +                if isinstance(o, UpdateAddHtlc):
       +                    return json.dumps(('UpdateAddHtlc', namedtuples_to_dict(o)))
                        return super(MyJsonEncoder, self)
       +        for fee_upd in serialized_channel['fee_updates']:
       +            fee_upd['progress'] = {str(k): v for k,v in fee_upd['progress'].items()}
                dumped = MyJsonEncoder().encode(serialized_channel)
                roundtripped = json.loads(dumped)
                reconstructed = HTLCStateMachine(roundtripped)
   DIR diff --git a/electrum/lnutil.py b/electrum/lnutil.py
       t@@ -18,7 +18,7 @@ HTLC_SUCCESS_WEIGHT = 703
        Keypair = namedtuple("Keypair", ["pubkey", "privkey"])
        ChannelConfig = namedtuple("ChannelConfig", [
            "payment_basepoint", "multisig_key", "htlc_basepoint", "delayed_basepoint", "revocation_basepoint",
       -    "to_self_delay", "dust_limit_sat", "max_htlc_value_in_flight_msat", "max_accepted_htlcs"])
       +    "to_self_delay", "dust_limit_sat", "max_htlc_value_in_flight_msat", "max_accepted_htlcs", "initial_msat"])
        OnlyPubkeyKeypair = namedtuple("OnlyPubkeyKeypair", ["pubkey"])
        RemoteState = namedtuple("RemoteState", ["ctn", "next_per_commitment_point", "amount_msat", "revocation_store", "current_per_commitment_point", "next_htlc_id", "feerate"])
        LocalState = namedtuple("LocalState", ["ctn", "per_commitment_secret_seed", "amount_msat", "next_htlc_id", "funding_locked_received", "was_announced", "current_commitment_signature", "current_htlc_signatures", "feerate"])
   DIR diff --git a/electrum/tests/test_lnhtlc.py b/electrum/tests/test_lnhtlc.py
       t@@ -25,7 +25,8 @@ def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate
                to_self_delay=l_csv,
                dust_limit_sat=l_dust,
                max_htlc_value_in_flight_msat=500000 * 1000,
       -        max_accepted_htlcs=5
       +        max_accepted_htlcs=5,
       +        initial_msat=local_amount,
            )
            remote_config=lnbase.ChannelConfig(
                payment_basepoint=other_pubkeys[0],
       t@@ -36,7 +37,8 @@ def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate
                to_self_delay=r_csv,
                dust_limit_sat=r_dust,
                max_htlc_value_in_flight_msat=500000 * 1000,
       -        max_accepted_htlcs=5
       +        max_accepted_htlcs=5,
       +        initial_msat=remote_amount,
            )
        
            return {
       t@@ -132,11 +134,11 @@ class TestLNBaseHTLCStateMachine(unittest.TestCase):
        
                self.paymentPreimage = b"\x01" * 32
                paymentHash = bitcoin.sha256(self.paymentPreimage)
       -        self.htlc = lnhtlc.UpdateAddHtlc(
       -            payment_hash = paymentHash,
       -            amount_msat =  one_bitcoin_in_msat,
       -            cltv_expiry =  5,
       -        )
       +        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
       t@@ -144,6 +146,7 @@ class TestLNBaseHTLCStateMachine(unittest.TestCase):
                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][0]
        
            def test_SimpleAddSettleWorkflow(self):
                alice_channel, bob_channel = self.alice_channel, self.bob_channel
       t@@ -250,6 +253,8 @@ class TestLNBaseHTLCStateMachine(unittest.TestCase):
                # 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
       t@@ -325,11 +330,11 @@ class TestLNHTLCDust(unittest.TestCase):
                self.assertEqual(fee_per_kw, 6000)
                htlcAmt = 500 + lnutil.HTLC_TIMEOUT_WEIGHT * (fee_per_kw // 1000)
                self.assertEqual(htlcAmt, 4478)
       -        htlc = lnhtlc.UpdateAddHtlc(
       -            payment_hash = paymentHash,
       -            amount_msat =  1000 * htlcAmt,
       -            cltv_expiry =  5, # also in create_test_channels
       -        )
       +        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)
       t@@ -338,7 +343,7 @@ class TestLNHTLCDust(unittest.TestCase):
                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, htlc.htlc_id)
       +        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)