URI: 
       tlightning channel reserves - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 15b0720f5e4130286c5639e1f8c7720fb89a86fc
   DIR parent 54edc9488a5b774d672a150b470dc4f2fc7a9ce8
  HTML Author: Janus <ysangkok@gmail.com>
       Date:   Tue, 23 Oct 2018 20:32:18 +0200
       
       lightning channel reserves
       
       Diffstat:
         M electrum/lnbase.py                  |       4 +++-
         M electrum/lnchan.py                  |      69 ++++++++++++++++++++-----------
         M electrum/lnutil.py                  |       2 ++
         M electrum/tests/test_lnchan.py       |     134 +++++++++++++++++++++++++++++--
       
       4 files changed, 176 insertions(+), 33 deletions(-)
       ---
   DIR diff --git a/electrum/lnbase.py b/electrum/lnbase.py
       t@@ -390,6 +390,7 @@ class Peer(PrintError):
                    ctn=-1,
                    next_htlc_id=0,
                    amount_msat=initial_msat,
       +            reserve_sat=546,
                )
                per_commitment_secret_seed = keypair_generator(LnKeyFamily.REVOCATION_ROOT).privkey
                return local_config, per_commitment_secret_seed
       t@@ -451,6 +452,7 @@ class Peer(PrintError):
                    ctn = -1,
                    amount_msat=push_msat,
                    next_htlc_id = 0,
       +            reserve_sat = int.from_bytes(payload["channel_reserve_satoshis"], 'big'),
        
                    next_per_commitment_point=remote_per_commitment_point,
                    current_per_commitment_point=None,
       t@@ -561,6 +563,7 @@ class Peer(PrintError):
                            ctn = -1,
                            amount_msat=remote_balance_sat,
                            next_htlc_id = 0,
       +                    reserve_sat = int.from_bytes(payload['channel_reserve_satoshis'], 'big'),
        
                            next_per_commitment_point=payload['first_per_commitment_point'],
                            current_per_commitment_point=None,
       t@@ -941,7 +944,6 @@ class Peer(PrintError):
                assert final_cltv <= cltv, (final_cltv, cltv)
                secret_key = os.urandom(32)
                onion = new_onion_packet([x.node_id for x in route], secret_key, hops_data, associated_data=payment_hash)
       -        chan.check_can_pay(amount_msat)
                # create htlc
                htlc = {'amount_msat':amount_msat, 'payment_hash':payment_hash, 'cltv_expiry':cltv}
                htlc_id = chan.add_htlc(htlc)
   DIR diff --git a/electrum/lnchan.py b/electrum/lnchan.py
       t@@ -18,7 +18,7 @@ from .lnutil import sign_and_get_sig_string, privkey_to_pubkey, make_htlc_tx_wit
        from .lnutil import make_htlc_tx_with_open_channel, make_commitment, make_received_htlc, make_offered_htlc
        from .lnutil import HTLC_TIMEOUT_WEIGHT, HTLC_SUCCESS_WEIGHT
        from .lnutil import funding_output_script, LOCAL, REMOTE, HTLCOwner, make_closing_tx, make_commitment_outputs
       -from .lnutil import ScriptHtlc, SENT, RECEIVED, PaymentFailure, calc_onchain_fees
       +from .lnutil import ScriptHtlc, SENT, RECEIVED, PaymentFailure, calc_onchain_fees, RemoteMisbehaving
        from .transaction import Transaction, TxOutput, construct_witness
        from .simple_config import SimpleConfig, FEERATE_FALLBACK_STATIC_FEE
        
       t@@ -131,6 +131,7 @@ class Channel(PrintError):
                template = lambda: {
                    'adds': {}, # Dict[HTLC_ID, UpdateAddHtlc]
                    'settles': [], # List[HTLC_ID]
       +            'fails': [], # List[HTLC_ID]
                }
                self.log = {LOCAL: template(), REMOTE: template()}
                for strname, subject in [('remote_log', REMOTE), ('local_log', LOCAL)]:
       t@@ -159,23 +160,23 @@ class Channel(PrintError):
            def get_state(self):
                return self._state
        
       -    def check_can_pay(self, amount_msat: int) -> None:
       -        # FIXME channel reserve
       +    def _check_can_pay(self, amount_msat: int) -> None:
                if self.get_state() != 'OPEN':
                    raise PaymentFailure('Channel not open')
                if self.available_to_spend(LOCAL) < amount_msat:
                    raise PaymentFailure('Not enough local balance')
                if len(self.htlcs(LOCAL, only_pending=True)) + 1 > self.config[REMOTE].max_accepted_htlcs:
                    raise PaymentFailure('Too many HTLCs already in channel')
       -        if htlcsum(self.htlcs(LOCAL, only_pending=True)) + amount_msat > self.config[REMOTE].max_htlc_value_in_flight_msat:
       -            raise PaymentFailure('HTLC value sum would exceed max allowed: {} msat'.format(self.config[REMOTE].max_htlc_value_in_flight_msat))
       +        current_htlc_sum = htlcsum(self.htlcs(LOCAL, only_pending=True))
       +        if current_htlc_sum + amount_msat > self.config[REMOTE].max_htlc_value_in_flight_msat:
       +            raise PaymentFailure(f'HTLC value sum (sum of pending htlcs: {current_htlc_sum/1000} sat plus new htlc: {amount_msat/1000} sat) would exceed max allowed: {self.config[REMOTE].max_htlc_value_in_flight_msat/1000} sat')
                if amount_msat <= 0:  # FIXME htlc_minimum_msat
                    raise PaymentFailure(f'HTLC value too small: {amount_msat} msat')
        
            def can_pay(self, amount_msat):
                try:
       -            self.check_can_pay(amount_msat)
       -        except:
       +            self._check_can_pay(amount_msat)
       +        except PaymentFailure:
                    return False
                return True
        
       t@@ -196,6 +197,7 @@ class Channel(PrintError):
                should be called when preparing to send an outgoing HTLC.
                """
                assert type(htlc) is dict
       +        self._check_can_pay(htlc['amount_msat'])
                htlc = UpdateAddHtlc(**htlc, htlc_id=self.config[LOCAL].next_htlc_id)
                self.log[LOCAL]['adds'][htlc.htlc_id] = htlc
                self.print_error("add_htlc")
       t@@ -210,7 +212,10 @@ class Channel(PrintError):
                """
                assert type(htlc) is dict
                htlc = UpdateAddHtlc(**htlc, htlc_id = self.config[REMOTE].next_htlc_id)
       -        self.log[REMOTE]['adds'][htlc.htlc_id] = htlc
       +        if self.available_to_spend(REMOTE) < htlc.amount_msat:
       +            raise RemoteMisbehaving('Remote dipped below channel reserve')
       +        adds = self.log[REMOTE]['adds']
       +        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
       t@@ -228,10 +233,8 @@ class Channel(PrintError):
                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")
       +        self.lock_in_htlc_changes(LOCAL)
        
                pending_remote_commitment = self.pending_remote_commitment
                sig_64 = sign_and_get_sig_string(pending_remote_commitment, self.config[LOCAL], self.config[REMOTE])
       t@@ -265,6 +268,17 @@ class Channel(PrintError):
        
                return sig_64, htlcsigs
        
       +    def lock_in_htlc_changes(self, subject):
       +        for sub in (LOCAL, REMOTE):
       +            for htlc_id in self.log[-sub]['fails']:
       +                adds = self.log[sub]['adds']
       +                htlc = adds.pop(htlc_id)
       +            self.log[-sub]['fails'].clear()
       +
       +        for htlc in self.log[subject]['adds'].values():
       +            if htlc.locked_in[subject] is None:
       +                htlc.locked_in[subject] = self.config[subject].ctn
       +
            def receive_new_commitment(self, sig, htlc_sigs):
                """
                ReceiveNewCommitment process a signature for a new commitment state sent by
       t@@ -276,11 +290,8 @@ class Channel(PrintError):
                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
       +        self.lock_in_htlc_changes(REMOTE)
                assert len(htlc_sigs) == 0 or type(htlc_sigs[0]) is bytes
        
                pending_local_commitment = self.pending_local_commitment
       t@@ -479,9 +490,16 @@ class Channel(PrintError):
                return initial
        
            def available_to_spend(self, subject):
       -        # FIXME what about channel_reserve_satoshis? will the remote fail the channel if we go below? test.
       -        # FIXME what about tx fees
       -        return self.balance(subject) - htlcsum(self.htlcs(subject, only_pending=True))
       +        return self.balance(subject)\
       +                - htlcsum(self.log[subject]['adds'].values())\
       +                - self.config[subject].reserve_sat * 1000\
       +                - calc_onchain_fees(
       +                      # TODO should we include a potential new htlc, when we are called from receive_htlc?
       +                      len(list(self.included_htlcs(subject, LOCAL)) + list(self.included_htlcs(subject, REMOTE))),
       +                      self.pending_feerate(subject),
       +                      subject == LOCAL,
       +                      self.constraints.is_initiator,
       +                  )[subject]
        
            def amounts(self):
                remote_settled= htlcsum(self.htlcs(REMOTE, False))
       t@@ -536,8 +554,11 @@ class Channel(PrintError):
                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']):
       +            settled = htlc.htlc_id in other_log['settles']
       +            failed =  htlc.htlc_id in other_log['fails']
       +            if locked_in is None:
       +                continue
       +            if only_pending == (settled or failed):
                        continue
                    res.append(htlc)
                return res
       t@@ -561,11 +582,11 @@ class Channel(PrintError):
        
            def fail_htlc(self, htlc_id):
                self.print_error("fail_htlc")
       -        self.log[REMOTE]['adds'].pop(htlc_id)
       +        self.log[LOCAL]['fails'].append(htlc_id)
        
            def receive_fail_htlc(self, htlc_id):
                self.print_error("receive_fail_htlc")
       -        self.log[LOCAL]['adds'].pop(htlc_id)
       +        self.log[REMOTE]['fails'].append(htlc_id)
        
            @property
            def current_height(self):
       t@@ -665,8 +686,8 @@ class Channel(PrintError):
        
            def make_commitment(self, subject, this_point) -> Transaction:
                remote_msat, local_msat = self.amounts()
       -        assert local_msat >= 0
       -        assert remote_msat >= 0
       +        assert local_msat >= 0, local_msat
       +        assert remote_msat >= 0, remote_msat
                this_config = self.config[subject]
                other_config = self.config[-subject]
                other_htlc_pubkey = derive_pubkey(other_config.htlc_basepoint.pubkey, this_point)
   DIR diff --git a/electrum/lnutil.py b/electrum/lnutil.py
       t@@ -37,6 +37,7 @@ common = [
            ('max_htlc_value_in_flight_msat' , int),
            ('max_accepted_htlcs' , int),
            ('initial_msat' , int),
       +    ('reserve_sat', int),
        ]
        
        ChannelConfig = NamedTuple('ChannelConfig', common)
       t@@ -65,6 +66,7 @@ class HandshakeFailed(LightningError): pass
        class PaymentFailure(LightningError): pass
        class ConnStringFormatError(LightningError): pass
        class UnknownPaymentHash(LightningError): pass
       +class RemoteMisbehaving(LightningError): pass
        
        
        # TODO make configurable?
   DIR diff --git a/electrum/tests/test_lnchan.py b/electrum/tests/test_lnchan.py
       t@@ -11,6 +11,8 @@ import binascii
        
        from electrum.lnutil import SENT, LOCAL, REMOTE, RECEIVED
        
       +one_bitcoin_in_msat = bitcoin.COIN * 1000
       +
        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
       t@@ -29,12 +31,13 @@ def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate
                        revocation_basepoint=other_pubkeys[4],
                        to_self_delay=r_csv,
                        dust_limit_sat=r_dust,
       -                max_htlc_value_in_flight_msat=500000 * 1000,
       +                max_htlc_value_in_flight_msat=one_bitcoin_in_msat * 5,
                        max_accepted_htlcs=5,
                        initial_msat=remote_amount,
                        ctn = 0,
                        next_htlc_id = 0,
                        amount_msat=remote_amount,
       +                reserve_sat=0,
        
                        next_per_commitment_point=nex,
                        current_per_commitment_point=cur,
       t@@ -48,12 +51,13 @@ def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate
                        revocation_basepoint=privkeys[4],
                        to_self_delay=l_csv,
                        dust_limit_sat=l_dust,
       -                max_htlc_value_in_flight_msat=500000 * 1000,
       +                max_htlc_value_in_flight_msat=one_bitcoin_in_msat * 5,
                        max_accepted_htlcs=5,
                        initial_msat=local_amount,
                        ctn = 0,
                        next_htlc_id = 0,
                        amount_msat=local_amount,
       +                reserve_sat=0,
        
                        per_commitment_secret_seed=seed,
                        funding_locked_received=True,
       t@@ -101,13 +105,15 @@ def create_test_channels(feerate=6000, local=None, remote=None):
            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 \
       +    alice, bob = \
                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
       +    alice.set_state('OPEN')
       +    bob.set_state('OPEN')
       +    return alice, bob
        
        class TestFee(unittest.TestCase):
            """
       t@@ -134,7 +140,7 @@ class TestChannel(unittest.TestCase):
        
                self.paymentPreimage = b"\x01" * 32
                paymentHash = bitcoin.sha256(self.paymentPreimage)
       -        self.htlc = {
       +        self.htlc_dict = {
                    'payment_hash' : paymentHash,
                    'amount_msat' :  one_bitcoin_in_msat,
                    'cltv_expiry' :  5,
       t@@ -143,9 +149,9 @@ class TestChannel(unittest.TestCase):
                # 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.aliceHtlcIndex = self.alice_channel.add_htlc(self.htlc_dict)
        
       -        self.bobHtlcIndex = self.bob_channel.receive_htlc(self.htlc)
       +        self.bobHtlcIndex = self.bob_channel.receive_htlc(self.htlc_dict)
                self.htlc = self.bob_channel.log[lnutil.REMOTE]['adds'][0]
        
            def test_SimpleAddSettleWorkflow(self):
       t@@ -318,7 +324,120 @@ class TestChannel(unittest.TestCase):
                bob_channel.receive_revocation(alice_revocation)
                self.assertEqual(fee, bob_channel.constraints.feerate)
        
       +    def test_AddHTLCNegativeBalance(self):
       +        self.htlc_dict['payment_hash'] = bitcoin.sha256(32 * b'\x02')
       +        self.alice_channel.add_htlc(self.htlc_dict)
       +        self.htlc_dict['payment_hash'] = bitcoin.sha256(32 * b'\x03')
       +        self.alice_channel.add_htlc(self.htlc_dict)
       +        # now there are three htlcs (one was in setUp)
       +
       +        # Alice now has an available balance of 2 BTC. We'll add a new HTLC of
       +        # value 2 BTC, which should make Alice's balance negative (since she
       +        # has to pay a commitment fee).
       +        new = dict(self.htlc_dict)
       +        new['amount_msat'] *= 2
       +        new['payment_hash'] = bitcoin.sha256(32 * b'\x04')
       +        with self.assertRaises(lnutil.PaymentFailure) as cm:
       +            self.alice_channel.add_htlc(new)
       +        self.assertEqual(('Not enough local balance',), cm.exception.args)
       +
       +class TestAvailableToSpend(unittest.TestCase):
       +    def test_DesyncHTLCs(self):
       +        alice_channel, bob_channel = create_test_channels()
       +
       +        paymentPreimage = b"\x01" * 32
       +        paymentHash = bitcoin.sha256(paymentPreimage)
       +        htlc_dict = {
       +            'payment_hash' : paymentHash,
       +            'amount_msat' :  int(4.1 * one_bitcoin_in_msat),
       +            'cltv_expiry' :  5,
       +        }
       +
       +        alice_idx = alice_channel.add_htlc(htlc_dict)
       +        bob_idx = bob_channel.receive_htlc(htlc_dict)
       +        force_state_transition(alice_channel, bob_channel)
       +        bob_channel.fail_htlc(bob_idx)
       +        alice_channel.receive_fail_htlc(alice_idx)
       +        # Alice now has gotten all her original balance (5 BTC) back, however,
       +        # adding a new HTLC at this point SHOULD fail, since if she adds the
       +        # HTLC and signs the next state, Bob cannot assume she received the
       +        # FailHTLC, and must assume she doesn't have the necessary balance
       +        # available.
       +        # We try adding an HTLC of value 1 BTC, which should fail because the
       +        # balance is unavailable.
       +        htlc_dict = {
       +            'payment_hash' : paymentHash,
       +            'amount_msat' :  one_bitcoin_in_msat,
       +            'cltv_expiry' :  5,
       +        }
       +        with self.assertRaises(lnutil.PaymentFailure):
       +            alice_channel.add_htlc(htlc_dict)
       +        # Now do a state transition, which will ACK the FailHTLC, making Alice
       +        # able to add the new HTLC.
       +        force_state_transition(alice_channel, bob_channel)
       +        alice_channel.add_htlc(htlc_dict)
        
       +class TestChanReserve(unittest.TestCase):
       +    def setUp(self):
       +        alice_channel, bob_channel = create_test_channels()
       +        alice_min_reserve = int(.5 * one_bitcoin_in_msat // 1000)
       +        alice_channel.config[LOCAL] =\
       +            alice_channel.config[LOCAL]._replace(reserve_sat=alice_min_reserve)
       +        bob_channel.config[REMOTE] =\
       +            bob_channel.config[REMOTE]._replace(reserve_sat=alice_min_reserve)
       +        # We set Bob's channel reserve to a value that is larger than
       +        # his current balance in the channel. This will ensure that
       +        # after a channel is first opened, Bob can still receive HTLCs
       +        # even though his balance is less than his channel reserve.
       +        bob_min_reserve = 6 * one_bitcoin_in_msat // 1000
       +        bob_channel.config[LOCAL] =\
       +            bob_channel.config[LOCAL]._replace(reserve_sat=bob_min_reserve)
       +        alice_channel.config[REMOTE] =\
       +            alice_channel.config[REMOTE]._replace(reserve_sat=bob_min_reserve)
       +
       +        self.bob_min = bob_min_reserve
       +        self.alice_min = bob_min_reserve
       +        self.alice_channel = alice_channel
       +        self.bob_channel = bob_channel
       +
       +    def test_part1(self):
       +        # Add an HTLC that will increase Bob's balance. This should succeed,
       +        # since Alice stays above her channel reserve, and Bob increases his
       +        # balance (while still being below his channel reserve).
       +        #
       +        # Resulting balances:
       +        #        Alice:        4.5
       +        #        Bob:        5.0
       +        paymentPreimage = b"\x01" * 32
       +        paymentHash = bitcoin.sha256(paymentPreimage)
       +        htlc_dict = {
       +            'payment_hash' : paymentHash,
       +            'amount_msat' :  int(.5 * one_bitcoin_in_msat),
       +            'cltv_expiry' :  5,
       +        }
       +        self.alice_channel.add_htlc(htlc_dict)
       +        self.bob_channel.receive_htlc(htlc_dict)
       +        # Force a state transition, making sure this HTLC is considered valid
       +        # even though the channel reserves are not met.
       +        force_state_transition(self.alice_channel, self.bob_channel)
       +
       +        aliceSelfBalance = self.alice_channel.balance(LOCAL)\
       +                - lnchan.htlcsum(self.alice_channel.htlcs(LOCAL, True))
       +        bobBalance = self.bob_channel.balance(REMOTE)\
       +                - lnchan.htlcsum(self.alice_channel.htlcs(REMOTE, True))
       +        self.assertEqual(aliceSelfBalance, one_bitcoin_in_msat*4.5)
       +        self.assertEqual(bobBalance, one_bitcoin_in_msat*5)
       +        # Now let Bob try to add an HTLC. This should fail, since it will
       +        # decrease his balance, which is already below the channel reserve.
       +        #
       +        # Resulting balances:
       +        #        Alice:        4.5
       +        #        Bob:        5.0
       +        with self.assertRaises(lnutil.PaymentFailure):
       +            htlc_dict['payment_hash'] = bitcoin.sha256(32 * b'\x02')
       +            self.bob_channel.add_htlc(htlc_dict)
       +        with self.assertRaises(lnutil.RemoteMisbehaving):
       +            self.alice_channel.receive_htlc(htlc_dict)
        
        class TestDust(unittest.TestCase):
            def test_DustLimit(self):
       t@@ -339,7 +458,6 @@ class TestDust(unittest.TestCase):
                aliceHtlcIndex = alice_channel.add_htlc(htlc)
                bobHtlcIndex = bob_channel.receive_htlc(htlc)
                force_state_transition(alice_channel, bob_channel)
       -        self.assertEqual(alice_channel.available_to_spend(LOCAL), alice_channel.balance(LOCAL) - htlc['amount_msat'])
                self.assertEqual(len(alice_channel.local_commitment.outputs()), 3)
                self.assertEqual(len(bob_channel.local_commitment.outputs()), 2)
                default_fee = calc_static_fee(0)