electrum

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

commit 15b0720f5e4130286c5639e1f8c7720fb89a86fc
parent 54edc9488a5b774d672a150b470dc4f2fc7a9ce8
Author: Janus <ysangkok@gmail.com>
Date:   Tue, 23 Oct 2018 20:32:18 +0200

lightning channel reserves

Diffstat:
Melectrum/lnbase.py | 4+++-
Melectrum/lnchan.py | 69+++++++++++++++++++++++++++++++++++++++++++++------------------------
Melectrum/lnutil.py | 2++
Melectrum/tests/test_lnchan.py | 134++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
4 files changed, 176 insertions(+), 33 deletions(-)

diff --git a/electrum/lnbase.py b/electrum/lnbase.py @@ -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 @@ -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, @@ -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, @@ -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) diff --git a/electrum/lnchan.py b/electrum/lnchan.py @@ -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 @@ -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)]: @@ -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 @@ -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") @@ -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 @@ -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]) @@ -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 @@ -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 @@ -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)) @@ -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 @@ -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): @@ -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) diff --git a/electrum/lnutil.py b/electrum/lnutil.py @@ -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) @@ -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? diff --git a/electrum/tests/test_lnchan.py b/electrum/tests/test_lnchan.py @@ -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 @@ -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, @@ -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, @@ -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): """ @@ -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, @@ -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): @@ -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): @@ -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)