URI: 
       tln: integrate lnhtlc in lnbase, fix multiple lnhtlc bugs - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 4d25933898bbc23b615ae4148e04929e0c110ef7
   DIR parent 5ed6f79a330b09175cf5b3abc8d5d034af83326e
  HTML Author: Janus <ysangkok@gmail.com>
       Date:   Mon, 11 Jun 2018 00:14:38 +0200
       
       ln: integrate lnhtlc in lnbase, fix multiple lnhtlc bugs
       
       Diffstat:
         M lib/lnbase.py                       |      24 +++++++-----------------
         M lib/lnhtlc.py                       |     119 +++++++++++++++++++------------
         M lib/lnworker.py                     |       2 +-
         M lib/tests/test_lnhtlc.py            |      20 +++++++++++---------
       
       4 files changed, 93 insertions(+), 72 deletions(-)
       ---
   DIR diff --git a/lib/lnbase.py b/lib/lnbase.py
       t@@ -37,6 +37,7 @@ from .util import PrintError, bh2u, print_error, bfh, profiler, xor_bytes
        from .transaction import opcodes, Transaction
        from .lnrouter import new_onion_packet, OnionHopsDataSingle, OnionPerHop
        from .lightning_payencode.lnaddr import lndecode
       +from .lnhtlc import UpdateAddHtlc, HTLCStateMachine
        
        from collections import namedtuple, defaultdict
        
       t@@ -1035,27 +1036,16 @@ class Peer(PrintError):
                onion = new_onion_packet(self.node_keys, self.secret_key, hops_data, associated_data)
                msat_local = chan.local_state.amount_msat - (amount_msat + total_fee)
                msat_remote = chan.remote_state.amount_msat + (amount_msat + total_fee)
       +        htlc = UpdateAddHtlc(amount_msat, payment_hash, final_cltv_expiry_with_deltas, total_fee)
                amount_msat += total_fee
       +
                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()))
        
       -        their_local_htlc_pubkey = derive_pubkey(chan.remote_config.htlc_basepoint.pubkey, chan.remote_state.next_per_commitment_point)
       -        their_remote_htlc_pubkey = derive_pubkey(chan.local_config.htlc_basepoint.pubkey, chan.remote_state.next_per_commitment_point)
       -        their_remote_htlc_privkey_number = derive_privkey(
       -            int.from_bytes(chan.local_config.htlc_basepoint.privkey, 'big'),
       -            chan.remote_state.next_per_commitment_point)
       -        their_remote_htlc_privkey = their_remote_htlc_privkey_number.to_bytes(32, 'big')
       -        # TODO check payment_hash
       -        revocation_pubkey = derive_blinded_pubkey(chan.local_config.revocation_basepoint.pubkey, chan.remote_state.next_per_commitment_point)
       -        htlcs_in_remote = [(make_received_htlc(revocation_pubkey, their_remote_htlc_pubkey, their_local_htlc_pubkey, payment_hash, final_cltv_expiry_with_deltas), amount_msat)]
       -        remote_ctx = make_commitment_using_open_channel(chan, chan.remote_state.ctn + 1, False, chan.remote_state.next_per_commitment_point,
       -            chan.remote_state.amount_msat, msat_local, htlcs_in_remote)
       -        sig_64 = sign_and_get_sig_string(remote_ctx, chan.local_config, chan.remote_config)
       +        m = HTLCStateMachine(chan)
       +        m.add_htlc(htlc)
        
       -        htlc_tx = make_htlc_tx_with_open_channel(chan, chan.remote_state.next_per_commitment_point, False, False, amount_msat, final_cltv_expiry_with_deltas, payment_hash, remote_ctx, 0)
       -        # htlc_sig signs the HTLC transaction that spends from THEIR commitment transaction's received_htlc output
       -        sig = bfh(htlc_tx.sign_txin(0, their_remote_htlc_privkey))
       -        r, s = sigdecode_der(sig[:-1], SECP256k1.generator.order())
       -        htlc_sig = sigencode_string_canonize(r, s, SECP256k1.generator.order())
       +        sig_64, htlc_sigs = m.sign_next_commitment()
       +        htlc_sig = htlc_sigs[0][1]
        
                self.send_message(gen_msg("commitment_signed", channel_id=chan.channel_id, signature=sig_64, num_htlcs=1, htlc_signature=htlc_sig))
        
   DIR diff --git a/lib/lnhtlc.py b/lib/lnhtlc.py
       t@@ -1,25 +1,28 @@
        # ported from lnd 42de4400bff5105352d0552155f73589166d162b
       -from .lnbase import *
       +from ecdsa.util import sigencode_string_canonize, sigdecode_der
       +from .util import bfh, PrintError
       +from collections import namedtuple
       +from ecdsa.curves import SECP256k1
       +from .crypto import sha256
        
        SettleHtlc = namedtuple("SettleHtlc", ["htlc_id"])
        RevokeAndAck = namedtuple("RevokeAndAck", ["height", "per_commitment_secret", "next_per_commitment_point"])
        
        class UpdateAddHtlc:
       -    def __init__(self, amount_msat, payment_hash, cltv_expiry, final_cltv_expiry_with_deltas):
       +    def __init__(self, amount_msat, payment_hash, cltv_expiry, total_fee):
                self.amount_msat = amount_msat
                self.payment_hash = payment_hash
                self.cltv_expiry = cltv_expiry
       +        self.total_fee = total_fee
        
                # the height the htlc was locked in at, or None
       -        self.locked_in = None
       -
       -        # this field is not in update_add_htlc but we need to to make the right htlcs
       -        self.final_cltv_expiry_with_deltas = final_cltv_expiry_with_deltas
       +        self.r_locked_in = None
       +        self.l_locked_in = 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, self.final_cltv_expiry_with_deltas)
       +        return (self.htlc_id, self.amount_msat, self.payment_hash, self.cltv_expiry, self.r_locked_in, self.l_locked_in, self.total_fee)
        
            def __hash__(self):
                return hash(self.as_tuple())
       t@@ -43,14 +46,15 @@ class HTLCStateMachine(PrintError):
            def diagnostic_name(self):
                return str(self.name)
        
       -    def __init__(self, state: OpenChannel, name = None):
       +    def __init__(self, state, name = None):
                self.state = state
                self.local_update_log = []
                self.remote_update_log = []
        
                self.name = name
        
       -        self.current_height = 0
       +        self.l_current_height = 0
       +        self.r_current_height = 0
                self.total_msat_sent = 0
                self.total_msat_received = 0
        
       t@@ -92,9 +96,14 @@ class HTLCStateMachine(PrintError):
                any). The HTLC signatures are sorted according to the BIP 69 order of the
                HTLC's on the commitment transaction.
                """
       +        from .lnbase import sign_and_get_sig_string, derive_privkey, make_htlc_tx_with_open_channel
       +        self.l_current_height += 1
       +        for htlc in self.local_update_log:
       +            if not type(htlc) is UpdateAddHtlc: continue
       +            if htlc.l_locked_in is None: htlc.l_locked_in = self.l_current_height
                for htlc in self.remote_update_log:
                    if not type(htlc) is UpdateAddHtlc: continue
       -            if htlc.locked_in is None: htlc.locked_in = self.current_height
       +            if htlc.r_locked_in is None: htlc.r_locked_in = self.r_current_height
                self.print_error("sign_next_commitment")
        
                sig_64 = sign_and_get_sig_string(self.remote_commitment, self.state.local_config, self.state.remote_config)
       t@@ -106,17 +115,18 @@ class HTLCStateMachine(PrintError):
        
                for_us = False
        
       -        htlcs = self.htlcs_in_remote # TODO also htlcs_in_local
       -        assert len(htlcs) <= 1
                htlcsigs = []
       -        for htlc in htlcs:
       -            original_htlc_output_index = 0
       -            we_receive = True # when we do htlcs_in_local, we need to flip this flag
       -            htlc_tx = make_htlc_tx_with_open_channel(self.state, self.state.remote_state.next_per_commitment_point, for_us, we_receive, htlc.amount_msat, htlc.final_cltv_expiry_with_deltas, htlc.payment_hash, self.remote_commitment, original_htlc_output_index)
       -            sig = bfh(htlc_tx.sign_txin(0, their_remote_htlc_privkey))
       -            r, s = sigdecode_der(sig[:-1], SECP256k1.generator.order())
       -            htlc_sig = sigencode_string_canonize(r, s, SECP256k1.generator.order())
       -            htlcsigs.append(htlc_sig)
       +        for we_receive, htlcs in zip([True, False], [self.htlcs_in_remote, self.htlcs_in_local]):
       +            assert len(htlcs) <= 1
       +            for htlc in htlcs:
       +                original_htlc_output_index = 0
       +                args = [self.state.remote_state.next_per_commitment_point, for_us, we_receive, htlc.amount_msat + htlc.total_fee, htlc.cltv_expiry, htlc.payment_hash, self.remote_commitment, original_htlc_output_index]
       +                print("args", args)
       +                htlc_tx = make_htlc_tx_with_open_channel(self.state, *args)
       +                sig = bfh(htlc_tx.sign_txin(0, their_remote_htlc_privkey))
       +                r, s = sigdecode_der(sig[:-1], SECP256k1.generator.order())
       +                htlc_sig = sigencode_string_canonize(r, s, SECP256k1.generator.order())
       +                htlcsigs.append((htlc_tx, htlc_sig))
        
                return sig_64, htlcsigs
        
       t@@ -145,7 +155,8 @@ class HTLCStateMachine(PrintError):
                transaction. This return value allows callers to act once an HTLC has been
                locked into our commitment transaction.
                """
       -        self.current_height += 1
       +        from .lnbase import get_per_commitment_secret_from_seed, secret_to_pubkey
       +        self.r_current_height += 1
                self.print_error("revoke_current_commitment")
        
                chan = self.state
       t@@ -163,7 +174,7 @@ class HTLCStateMachine(PrintError):
                    )
                )
        
       -        return RevokeAndAck(self.current_height, last_secret, next_point), "current htlcs"
       +        return RevokeAndAck(self.r_current_height, last_secret, next_point), "current htlcs"
        
            def receive_revocation(self, revocation):
                """
       t@@ -186,7 +197,7 @@ class HTLCStateMachine(PrintError):
                    settle_fails2.append(x)
        
                if revocation.height is not None:
       -            adds2 = list(x for x in self.htlcs_in_remote if x.locked_in == revocation.height)
       +            adds2 = list(x for x in self.htlcs_in_remote if x.r_locked_in == revocation.height)
        
                    class FwdPkg:
                        adds = adds2
       t@@ -196,10 +207,11 @@ class HTLCStateMachine(PrintError):
                    self.total_msat_sent += self.lookup_htlc(self.local_update_log, x.htlc_id).amount_msat
        
                # increase received_msat counter for htlc's that have been settled
       -        adds2 = self.gen_htlc_indices(self.remote_update_log)
       -        for x in adds2:
       -            if SettleHtlc(x) in self.local_update_log:
       -                self.total_msat_received += self.lookup_htlc(self.remote_update_log, x).amount_msat
       +        adds2 = self.gen_htlc_indices("remote")
       +        for htlc in adds2:
       +            htlc_id = htlc.htlc_id
       +            if SettleHtlc(htlc_id) in self.local_update_log:
       +                self.total_msat_received += self.lookup_htlc(self.remote_update_log, htlc_id).amount_msat
        
                # log compaction (remove entries relating to htlc's that have been settled)
        
       t@@ -241,14 +253,17 @@ class HTLCStateMachine(PrintError):
        
            @staticmethod
            def htlcsum(htlcs):
       -        return sum(x.amount_msat for x in htlcs)
       +        return sum(x.amount_msat for x in htlcs), sum(x.total_fee for x in htlcs)
        
            @property
            def remote_commitment(self):
       +        from .lnbase import make_commitment_using_open_channel, make_received_htlc, make_offered_htlc, derive_pubkey, derive_blinded_pubkey
       +        htlc_value_local, total_fee_local = self.htlcsum(self.htlcs_in_local)
       +        htlc_value_remote, total_fee_remote = self.htlcsum(self.htlcs_in_remote)
                local_msat = self.state.local_state.amount_msat -\
       -          self.htlcsum(self.htlcs_in_local)
       +          htlc_value_local
                remote_msat = self.state.remote_state.amount_msat -\
       -          self.htlcsum(self.htlcs_in_remote)
       +          htlc_value_remote
                assert local_msat > 0
                assert remote_msat > 0
        
       t@@ -262,25 +277,30 @@ class HTLCStateMachine(PrintError):
                htlcs_in_local = []
                for htlc in self.htlcs_in_local:
                    htlcs_in_local.append(
       -                ( make_received_htlc(local_revocation_pubkey, local_htlc_pubkey, remote_htlc_pubkey, htlc.payment_hash, htlc.cltv_expiry), htlc.amount_msat))
       +                ( make_received_htlc(local_revocation_pubkey, local_htlc_pubkey, remote_htlc_pubkey, htlc.payment_hash, htlc.cltv_expiry), htlc.amount_msat + total_fee_local))
        
                htlcs_in_remote = []
                for htlc in self.htlcs_in_remote:
                    htlcs_in_remote.append(
       -                ( make_offered_htlc(remote_revocation_pubkey, remote_htlc_pubkey, local_htlc_pubkey, htlc.payment_hash), htlc.amount_msat))
       +                ( make_offered_htlc(remote_revocation_pubkey, remote_htlc_pubkey, local_htlc_pubkey, htlc.payment_hash), htlc.amount_msat + total_fee_remote))
        
                commit = make_commitment_using_open_channel(self.state, self.state.remote_state.ctn + 1,
       -            True, this_point,
       -            remote_msat, local_msat, htlcs_in_local + htlcs_in_remote)
       +            False, this_point,
       +            remote_msat - total_fee_remote, local_msat - total_fee_local, htlcs_in_local + htlcs_in_remote)
                assert len(commit.outputs()) == 2 + len(htlcs_in_local) + len(htlcs_in_remote)
                return commit
        
            @property
            def local_commitment(self):
       +        from .lnbase import make_commitment_using_open_channel, make_received_htlc, make_offered_htlc, derive_pubkey, derive_blinded_pubkey, get_per_commitment_secret_from_seed, secret_to_pubkey
       +        htlc_value_local, total_fee_local = self.htlcsum(self.htlcs_in_local)
       +        htlc_value_remote, total_fee_remote = self.htlcsum(self.htlcs_in_remote)
       +        print("htlc_value_local, total_fee_local", htlc_value_local, total_fee_local)
                local_msat = self.state.local_state.amount_msat -\
       -          self.htlcsum(self.htlcs_in_local)
       +          htlc_value_local
       +        print("htlc_value_remote, total_fee_remote", htlc_value_remote, total_fee_remote)
                remote_msat = self.state.remote_state.amount_msat -\
       -          self.htlcsum(self.htlcs_in_remote)
       +          htlc_value_remote
                assert local_msat > 0
                assert remote_msat > 0
        
       t@@ -295,35 +315,44 @@ class HTLCStateMachine(PrintError):
        
                htlcs_in_local = []
                for htlc in self.htlcs_in_local:
       +            print("adding local htlc", htlc)
                    htlcs_in_local.append(
       -                ( make_offered_htlc(local_revocation_pubkey, local_htlc_pubkey, remote_htlc_pubkey, htlc.payment_hash), htlc.amount_msat))
       +                ( make_offered_htlc(local_revocation_pubkey, local_htlc_pubkey, remote_htlc_pubkey, htlc.payment_hash), htlc.amount_msat + total_fee_local))
        
                htlcs_in_remote = []
                for htlc in self.htlcs_in_remote:
       +            print("adding remote htlc", htlc)
                    htlcs_in_remote.append(
       -                ( make_received_htlc(remote_revocation_pubkey, remote_htlc_pubkey, local_htlc_pubkey, htlc.payment_hash, htlc.cltv_expiry), htlc.amount_msat))
       +                ( make_received_htlc(remote_revocation_pubkey, remote_htlc_pubkey, local_htlc_pubkey, htlc.payment_hash, htlc.cltv_expiry), htlc.amount_msat + total_fee_remote))
        
                commit = make_commitment_using_open_channel(self.state, self.state.local_state.ctn + 1,
                    True, this_point,
       -            local_msat, remote_msat, htlcs_in_local + htlcs_in_remote)
       +            local_msat - total_fee_local, remote_msat - total_fee_remote, htlcs_in_local + htlcs_in_remote)
                assert len(commit.outputs()) == 2 + len(htlcs_in_local) + len(htlcs_in_remote)
                return commit
        
       -    def gen_htlc_indices(self, update_log):
       -        for num, htlc in enumerate(update_log):
       +    def gen_htlc_indices(self, subject):
       +        assert subject in ["local", "remote"]
       +        update_log = (self.remote_update_log if subject == "remote" else self.local_update_log)
       +        res = []
       +        for htlc in update_log:
                    if type(htlc) is not UpdateAddHtlc:
                        continue
       -            if htlc.locked_in is None or htlc.locked_in < self.current_height:
       +            height = (self.r_current_height if subject == "remote" else self.l_current_height)
       +            locked_in = (htlc.r_locked_in   if subject == "remote" else htlc.l_locked_in)
       +
       +            if locked_in is None or locked_in < height:
                        continue
       -            yield num
       +            res.append(htlc)
       +        return res
        
            @property
            def htlcs_in_local(self):
       -        return [self.local_update_log[x] for x in self.gen_htlc_indices(self.local_update_log)]
       +        return self.gen_htlc_indices("local")
        
            @property
            def htlcs_in_remote(self):
       -        return [self.remote_update_log[x] for x in self.gen_htlc_indices(self.remote_update_log)]
       +        return self.gen_htlc_indices("remote")
        
            def settle_htlc(self, preimage, htlc_id, source_ref, dest_ref, close_key):
                """
   DIR diff --git a/lib/lnworker.py b/lib/lnworker.py
       t@@ -189,7 +189,7 @@ class LNWorker(PrintError):
                amount_msat = int(addr.amount * COIN * 1000)
                path = self.path_finder.find_path_for_payment(self.pubkey, invoice_pubkey, amount_msat)
                if path is None:
       -            return "No path found"
       +            raise Exception("No path found")
                node_id, short_channel_id = path[0]
                peer = self.peers[node_id]
                for chan in self.channels.values():
   DIR diff --git a/lib/tests/test_lnhtlc.py b/lib/tests/test_lnhtlc.py
       t@@ -38,7 +38,7 @@ def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate
        
            return lnbase.OpenChannel(
                    channel_id=channel_id,
       -            short_channel_id=channel_id.to_bytes(32, "big")[:8],
       +            short_channel_id=channel_id[:8],
                    funding_outpoint=lnbase.Outpoint(funding_txid, funding_index),
                    local_config=local_config,
                    remote_config=remote_config,
       t@@ -118,7 +118,7 @@ class TestLNBaseHTLCStateMachine(unittest.TestCase):
                    payment_hash = paymentHash,
                    amount_msat =  htlcAmt,
                    cltv_expiry =  5,
       -            final_cltv_expiry_with_deltas = 5
       +            total_fee = 0
                )
        
                # First Alice adds the outgoing HTLC to her local channel's state
       t@@ -134,6 +134,8 @@ class TestLNBaseHTLCStateMachine(unittest.TestCase):
                # 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.
       t@@ -153,8 +155,8 @@ class TestLNBaseHTLCStateMachine(unittest.TestCase):
                # her prior commitment transaction. Alice shouldn't have any HTLCs to
                # forward since she's sending an outgoing HTLC.
                fwdPkg = alice_channel.receive_revocation(bobRevocation)
       -        self.assertEqual(len(fwdPkg.adds), 0, "alice forwards %s add htlcs, should forward none"% len(fwdPkg.adds))
       -        self.assertEqual(len(fwdPkg.settle_fails), 0, "alice forwards %s settle/fail htlcs, should forward none"% len(fwdPkg.settle_fails))
       +        self.assertEqual(fwdPkg.adds, [], "alice forwards %s add htlcs, should forward none"% len(fwdPkg.adds))
       +        self.assertEqual(fwdPkg.settle_fails, [], "alice forwards %s settle/fail htlcs, should forward none"% len(fwdPkg.settle_fails))
        
                # Alice then processes bob's signature, and since she just received
                # the revocation, she expect this signature to cover everything up to
       t@@ -170,7 +172,7 @@ class TestLNBaseHTLCStateMachine(unittest.TestCase):
                # into both commitment transactions.
                fwdPkg = bob_channel.receive_revocation(aliceRevocation)
                self.assertEqual(len(fwdPkg.adds), 1, "bob forwards %s add htlcs, should only forward one"% len(fwdPkg.adds))
       -        self.assertEqual(len(fwdPkg.settle_fails), 0, "bob forwards %s settle/fail htlcs, should forward none"% len(fwdPkg.settle_fails))
       +        self.assertEqual(fwdPkg.settle_fails, [], "bob forwards %s settle/fail htlcs, should forward none"% len(fwdPkg.settle_fails))
        
                # At this point, both sides should have the proper number of satoshis
                # sent, and commitment height updated within their local channel
       t@@ -182,8 +184,8 @@ class TestLNBaseHTLCStateMachine(unittest.TestCase):
                self.assertEqual(alice_channel.total_msat_received, bobSent, "alice has incorrect milli-satoshis received %s vs %s"% (alice_channel.total_msat_received, bobSent))
                self.assertEqual(bob_channel.total_msat_sent, bobSent, "bob has incorrect milli-satoshis sent %s vs %s"% (bob_channel.total_msat_sent, bobSent))
                self.assertEqual(bob_channel.total_msat_received, aliceSent, "bob has incorrect milli-satoshis received %s vs %s"% (bob_channel.total_msat_received, aliceSent))
       -        self.assertEqual(bob_channel.current_height, 1, "bob has incorrect commitment height, %s vs %s"% (bob_channel.current_height, 1))
       -        self.assertEqual(alice_channel.current_height, 1, "alice has incorrect commitment height, %s vs %s"% (alice_channel.current_height, 1))
       +        self.assertEqual(bob_channel.l_current_height, 1, "bob has incorrect commitment height, %s vs %s"% (bob_channel.l_current_height, 1))
       +        self.assertEqual(alice_channel.l_current_height, 1, "alice has incorrect commitment height, %s vs %s"% (alice_channel.l_current_height, 1))
        
                # Both commitment transactions should have three outputs, and one of
                # them should be exactly the amount of the HTLC.
       t@@ -227,8 +229,8 @@ class TestLNBaseHTLCStateMachine(unittest.TestCase):
                self.assertEqual(alice_channel.total_msat_received, 0, "alice satoshis received incorrect %s vs %s expected"% (alice_channel.total_msat_received, 0))
                self.assertEqual(bob_channel.total_msat_received, mSatTransferred, "bob satoshis received incorrect %s vs %s expected"% (bob_channel.total_msat_received, mSatTransferred))
                self.assertEqual(bob_channel.total_msat_sent, 0, "bob satoshis sent incorrect %s vs %s expected"% (bob_channel.total_msat_sent, 0))
       -        self.assertEqual(bob_channel.current_height, 2, "bob has incorrect commitment height, %s vs %s"% (bob_channel.current_height, 2))
       -        self.assertEqual(alice_channel.current_height, 2, "alice has incorrect commitment height, %s vs %s"% (alice_channel.current_height, 2))
       +        self.assertEqual(bob_channel.l_current_height, 2, "bob has incorrect commitment height, %s vs %s"% (bob_channel.l_current_height, 2))
       +        self.assertEqual(alice_channel.l_current_height, 2, "alice has incorrect commitment height, %s vs %s"% (alice_channel.l_current_height, 2))
        
                # The logs of both sides should now be cleared since the entry adding
                # the HTLC should have been removed once both sides receive the