tlightning: fixup after rebasing on restructured master - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 35adc3231b297d03ad0a0534d65665ad14c0d9f6 DIR parent 1db7a8334afc8b2b60c7b87be1a12917439c1549 HTML Author: Janus <ysangkok@gmail.com> Date: Fri, 13 Jul 2018 17:05:04 +0200 lightning: fixup after rebasing on restructured master Diffstat: A electrum/gui/kivy/uix/dialogs/ligh… | 123 +++++++++++++++++++++++++++++++ A electrum/gui/kivy/uix/dialogs/ligh… | 93 +++++++++++++++++++++++++++++++ R gui/qt/channels_list.py -> electru… | 0 R lib/lightning.json -> electrum/lig… | 0 R lib/lnaddr.py -> electrum/lnaddr.py | 0 R lib/lnbase.py -> electrum/lnbase.py | 0 R lib/lnhtlc.py -> electrum/lnhtlc.py | 0 R lib/lnrouter.py -> electrum/lnrout… | 0 R lib/lnutil.py -> electrum/lnutil.py | 0 R lib/lnwatcher.py -> electrum/lnwat… | 0 R lib/lnworker.py -> electrum/lnwork… | 0 A electrum/tests/test_bolt11.py | 97 ++++++++++++++++++++++++++++++ A electrum/tests/test_lnhtlc.py | 344 ++++++++++++++++++++++++++++++ A electrum/tests/test_lnrouter.py | 151 +++++++++++++++++++++++++++++++ A electrum/tests/test_lnutil.py | 677 +++++++++++++++++++++++++++++++ D gui/kivy/uix/dialogs/lightning_cha… | 123 ------------------------------- D gui/kivy/uix/dialogs/lightning_pay… | 93 ------------------------------- D lib/tests/test_bolt11.py | 97 ------------------------------ D lib/tests/test_lnhtlc.py | 344 ------------------------------ D lib/tests/test_lnrouter.py | 151 ------------------------------- D lib/tests/test_lnutil.py | 677 ------------------------------- 21 files changed, 1485 insertions(+), 1485 deletions(-) --- DIR diff --git a/electrum/gui/kivy/uix/dialogs/lightning_channels.py b/electrum/gui/kivy/uix/dialogs/lightning_channels.py t@@ -0,0 +1,123 @@ +import binascii +from kivy.lang import Builder +from kivy.factory import Factory +from kivy.uix.popup import Popup +from kivy.clock import Clock +from electrum.gui.kivy.uix.context_menu import ContextMenu + +Builder.load_string(''' +<LightningChannelItem@CardItem> + details: {} + active: False + channelId: '<channelId not set>' + Label: + text: root.channelId + +<LightningChannelsDialog@Popup>: + name: 'lightning_channels' + BoxLayout: + id: box + orientation: 'vertical' + spacing: '1dp' + ScrollView: + GridLayout: + cols: 1 + id: lightning_channels_container + size_hint: 1, None + height: self.minimum_height + spacing: '2dp' + padding: '12dp' + +<ChannelDetailsItem@BoxLayout>: + canvas.before: + Color: + rgba: 0.5, 0.5, 0.5, 1 + Rectangle: + size: self.size + pos: self.pos + value: '' + Label: + text: root.value + text_size: self.size # this makes the text not overflow, but wrap + +<ChannelDetailsRow@BoxLayout>: + keyName: '' + value: '' + ChannelDetailsItem: + value: root.keyName + size_hint_x: 0.5 # this makes the column narrower + + # see https://blog.kivy.org/2014/07/wrapping-text-in-kivys-label/ + ScrollView: + Label: + text: root.value + size_hint_y: None + text_size: self.width, None + height: self.texture_size[1] + +<ChannelDetailsList@RecycleView>: + scroll_type: ['bars', 'content'] + scroll_wheel_distance: dp(114) + bar_width: dp(10) + viewclass: 'ChannelDetailsRow' + RecycleBoxLayout: + default_size: None, dp(56) + default_size_hint: 1, None + size_hint_y: None + height: self.minimum_height + orientation: 'vertical' + spacing: dp(2) + +<ChannelDetailsPopup@Popup>: + id: popuproot + data: [] + ChannelDetailsList: + data: popuproot.data +''') + +class ChannelDetailsPopup(Popup): + def __init__(self, data, **kwargs): + super(ChanenlDetailsPopup,self).__init__(**kwargs) + self.data = data + +class LightningChannelsDialog(Factory.Popup): + def __init__(self, app): + super(LightningChannelsDialog, self).__init__() + self.clocks = [] + self.app = app + self.context_menu = None + self.app.wallet.lnworker.subscribe_channel_list_updates_from_other_thread(self.rpc_result_handler) + + def show_channel_details(self, obj): + p = Factory.ChannelDetailsPopup() + p.data = [{'keyName': key, 'value': str(obj.details[key])} for key in obj.details.keys()] + p.open() + + def close_channel(self, obj): + print("UNIMPLEMENTED asked to close channel", obj.channelId) # TODO + + def show_menu(self, obj): + self.hide_menu() + self.context_menu = ContextMenu(obj, [("Close", self.close_channel), + ("Details", self.show_channel_details)]) + self.ids.box.add_widget(self.context_menu) + + def hide_menu(self): + if self.context_menu is not None: + self.ids.box.remove_widget(self.context_menu) + self.context_menu = None + + def rpc_result_handler(self, res): + channel_cards = self.ids.lightning_channels_container + channel_cards.clear_widgets() + if "channels" in res: + for i in res["channels"]: + item = Factory.LightningChannelItem() + item.screen = self + print(i) + item.channelId = i["chan_id"] + item.active = i["active"] + item.details = i + channel_cards.add_widget(item) + else: + self.app.show_info(res) DIR diff --git a/electrum/gui/kivy/uix/dialogs/lightning_payer.py b/electrum/gui/kivy/uix/dialogs/lightning_payer.py t@@ -0,0 +1,93 @@ +import binascii +from kivy.lang import Builder +from kivy.factory import Factory +from electrum.gui.kivy.i18n import _ +from kivy.clock import mainthread +from electrum.lnaddr import lndecode + +Builder.load_string(''' +<LightningPayerDialog@Popup> + id: s + name: 'lightning_payer' + invoice_data: '' + BoxLayout: + orientation: "vertical" + BlueButton: + text: s.invoice_data if s.invoice_data else _('Lightning invoice') + shorten: True + on_release: Clock.schedule_once(lambda dt: app.show_info(_('Copy and paste the lightning invoice using the Paste button, or use the camera to scan a QR code.'))) + GridLayout: + cols: 4 + size_hint: 1, None + height: '48dp' + IconButton: + id: qr + on_release: Clock.schedule_once(lambda dt: app.scan_qr(on_complete=s.on_lightning_qr)) + icon: 'atlas://gui/kivy/theming/light/camera' + Button: + text: _('Paste') + on_release: s.do_paste() + Button: + text: _('Paste using xclip') + on_release: s.do_paste_xclip() + Button: + text: _('Clear') + on_release: s.do_clear() + Button: + size_hint: 1, None + height: '48dp' + text: _('Open channel to pubkey in invoice') + on_release: s.do_open_channel() + Button: + size_hint: 1, None + height: '48dp' + text: _('Pay pasted/scanned invoice') + on_release: s.do_pay() +''') + +class LightningPayerDialog(Factory.Popup): + def __init__(self, app): + super(LightningPayerDialog, self).__init__() + self.app = app + + #def open(self, *args, **kwargs): + # super(LightningPayerDialog, self).open(*args, **kwargs) + #def dismiss(self, *args, **kwargs): + # super(LightningPayerDialog, self).dismiss(*args, **kwargs) + + def do_paste_xclip(self): + import subprocess + proc = subprocess.run(["xclip","-sel","clipboard","-o"], stdout=subprocess.PIPE) + self.invoice_data = proc.stdout.decode("ascii") + + def do_paste(self): + contents = self.app._clipboard.paste() + if not contents: + self.app.show_info(_("Clipboard is empty")) + return + self.invoice_data = contents + + def do_clear(self): + self.invoice_data = "" + + def do_open_channel(self): + compressed_pubkey_bytes = lndecode(self.invoice_data).pubkey.serialize() + hexpubkey = binascii.hexlify(compressed_pubkey_bytes).decode("ascii") + local_amt = 200000 + push_amt = 100000 + + def on_success(pw): + # node_id, local_amt, push_amt, emit_function, get_password + self.app.wallet.lnworker.open_channel_from_other_thread(hexpubkey, local_amt, push_amt, mainthread(lambda parent: self.app.show_info(_("Channel open, waiting for locking..."))), lambda: pw) + + if self.app.wallet.has_keystore_encryption(): + # wallet, msg, on_success (Tuple[str, str] -> ()), on_failure (() -> ()) + self.app.password_dialog(self.app.wallet, _("Password needed for opening channel"), on_success, lambda: self.app.show_error(_("Failed getting password from you"))) + else: + on_success("") + + def do_pay(self): + self.app.wallet.lnworker.pay_invoice_from_other_thread(self.invoice_data) + + def on_lightning_qr(self, data): + self.invoice_data = str(data) DIR diff --git a/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py DIR diff --git a/lib/lightning.json b/electrum/lightning.json DIR diff --git a/lib/lnaddr.py b/electrum/lnaddr.py DIR diff --git a/lib/lnbase.py b/electrum/lnbase.py DIR diff --git a/lib/lnhtlc.py b/electrum/lnhtlc.py DIR diff --git a/lib/lnrouter.py b/electrum/lnrouter.py DIR diff --git a/lib/lnutil.py b/electrum/lnutil.py DIR diff --git a/lib/lnwatcher.py b/electrum/lnwatcher.py DIR diff --git a/lib/lnworker.py b/electrum/lnworker.py DIR diff --git a/electrum/tests/test_bolt11.py b/electrum/tests/test_bolt11.py t@@ -0,0 +1,97 @@ +from hashlib import sha256 +from electrum.lnaddr import shorten_amount, unshorten_amount, LnAddr, lnencode, lndecode, u5_to_bitarray, bitarray_to_u5 +from decimal import Decimal +from binascii import unhexlify, hexlify +from electrum.segwit_addr import bech32_encode, bech32_decode +import pprint +import unittest + +RHASH=unhexlify('0001020304050607080900010203040506070809000102030405060708090102') +CONVERSION_RATE=1200 +PRIVKEY=unhexlify('e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734') +PUBKEY=unhexlify('03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad') + +class TestBolt11(unittest.TestCase): + def test_shorten_amount(self): + tests = { + Decimal(10)/10**12: '10p', + Decimal(1000)/10**12: '1n', + Decimal(1200)/10**12: '1200p', + Decimal(123)/10**6: '123u', + Decimal(123)/1000: '123m', + Decimal(3): '3', + } + + for i, o in tests.items(): + assert shorten_amount(i) == o + assert unshorten_amount(shorten_amount(i)) == i + + @staticmethod + def compare(a, b): + + if len([t[1] for t in a.tags if t[0] == 'h']) == 1: + h1 = sha256([t[1] for t in a.tags if t[0] == 'h'][0].encode('utf-8')).digest() + h2 = [t[1] for t in b.tags if t[0] == 'h'][0] + assert h1 == h2 + + # Need to filter out these, since they are being modified during + # encoding, i.e., hashed + a.tags = [t for t in a.tags if t[0] != 'h' and t[0] != 'n'] + b.tags = [t for t in b.tags if t[0] != 'h' and t[0] != 'n'] + + assert b.pubkey.serialize() == PUBKEY, (hexlify(b.pubkey.serialize()), hexlify(PUBKEY)) + assert b.signature != None + + # Unset these, they are generated during encoding/decoding + b.pubkey = None + b.signature = None + + assert a.__dict__ == b.__dict__, (pprint.pformat([a.__dict__, b.__dict__])) + + def test_roundtrip(self): + longdescription = ('One piece of chocolate cake, one icecream cone, one' + ' pickle, one slice of swiss cheese, one slice of salami,' + ' one lollypop, one piece of cherry pie, one sausage, one' + ' cupcake, and one slice of watermelon') + + + tests = [ + LnAddr(RHASH, tags=[('d', '')]), + LnAddr(RHASH, amount=Decimal('0.001'), + tags=[('d', '1 cup coffee'), ('x', 60)]), + LnAddr(RHASH, amount=Decimal('1'), tags=[('h', longdescription)]), + LnAddr(RHASH, currency='tb', tags=[('f', 'mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP'), ('h', longdescription)]), + LnAddr(RHASH, amount=24, tags=[ + ('r', [(unhexlify('029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), unhexlify('0102030405060708'), 1, 20, 3), (unhexlify('039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), unhexlify('030405060708090a'), 2, 30, 4)]), ('f', '1RustyRX2oai4EYYDpQGWvEL62BBGqN9T'), ('h', longdescription)]), + LnAddr(RHASH, amount=24, tags=[('f', '3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX'), ('h', longdescription)]), + LnAddr(RHASH, amount=24, tags=[('f', 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'), ('h', longdescription)]), + LnAddr(RHASH, amount=24, tags=[('f', 'bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3'), ('h', longdescription)]), + LnAddr(RHASH, amount=24, tags=[('n', PUBKEY), ('h', longdescription)]), + ] + + # Roundtrip + for t in tests: + o = lndecode(lnencode(t, PRIVKEY), False, t.currency) + self.compare(t, o) + + def test_n_decoding(self): + # We flip the signature recovery bit, which would normally give a different + # pubkey. + hrp, data = bech32_decode(lnencode(LnAddr(RHASH, amount=24, tags=[('d', '')]), PRIVKEY), True) + databits = u5_to_bitarray(data) + databits.invert(-1) + lnaddr = lndecode(bech32_encode(hrp, bitarray_to_u5(databits)), True) + assert lnaddr.pubkey.serialize() != PUBKEY + + # But not if we supply expliciy `n` specifier! + hrp, data = bech32_decode(lnencode(LnAddr(RHASH, amount=24, + tags=[('d', ''), + ('n', PUBKEY)]), + PRIVKEY), True) + databits = u5_to_bitarray(data) + databits.invert(-1) + lnaddr = lndecode(bech32_encode(hrp, bitarray_to_u5(databits)), True) + assert lnaddr.pubkey.serialize() == PUBKEY + + def test_min_final_cltv_expiry(self): + self.assertEquals(lndecode("lnsb500u1pdsgyf3pp5nmrqejdsdgs4n9ukgxcp2kcq265yhrxd4k5dyue58rxtp5y83s3qdqqcqzystrggccm9yvkr5yqx83jxll0qjpmgfg9ywmcd8g33msfgmqgyfyvqhku80qmqm8q6v35zvck2y5ccxsz5avtrauz8hgjj3uahppyq20qp6dvwxe", expected_hrp="sb").min_final_cltv_expiry, 144) DIR diff --git a/electrum/tests/test_lnhtlc.py b/electrum/tests/test_lnhtlc.py t@@ -0,0 +1,344 @@ +# ported from lnd 42de4400bff5105352d0552155f73589166d162b + +import unittest +import electrum.bitcoin as bitcoin +import electrum.lnbase as lnbase +import electrum.lnhtlc as lnhtlc +import electrum.lnutil as lnutil +import electrum.util as util +import os +import binascii + +def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate, is_initiator, local_amount, remote_amount, privkeys, other_pubkeys, seed, cur, nex, other_node_id, l_dust, r_dust, l_csv, r_csv): + assert local_amount > 0 + assert remote_amount > 0 + channel_id, _ = lnbase.channel_id_from_funding_tx(funding_txid, funding_index) + their_revocation_store = lnbase.RevocationStore() + local_config=lnbase.ChannelConfig( + payment_basepoint=privkeys[0], + multisig_key=privkeys[1], + htlc_basepoint=privkeys[2], + delayed_basepoint=privkeys[3], + revocation_basepoint=privkeys[4], + to_self_delay=l_csv, + dust_limit_sat=l_dust, + max_htlc_value_in_flight_msat=500000 * 1000, + max_accepted_htlcs=5 + ) + remote_config=lnbase.ChannelConfig( + payment_basepoint=other_pubkeys[0], + multisig_key=other_pubkeys[1], + htlc_basepoint=other_pubkeys[2], + delayed_basepoint=other_pubkeys[3], + revocation_basepoint=other_pubkeys[4], + to_self_delay=r_csv, + dust_limit_sat=r_dust, + max_htlc_value_in_flight_msat=500000 * 1000, + max_accepted_htlcs=5 + ) + + return { + "channel_id":channel_id, + "short_channel_id":channel_id[:8], + "funding_outpoint":lnbase.Outpoint(funding_txid, funding_index), + "local_config":local_config, + "remote_config":remote_config, + "remote_state":lnbase.RemoteState( + ctn = 0, + next_per_commitment_point=nex, + current_per_commitment_point=cur, + amount_msat=remote_amount, + revocation_store=their_revocation_store, + next_htlc_id = 0, + feerate=local_feerate + ), + "local_state":lnbase.LocalState( + ctn = 0, + per_commitment_secret_seed=seed, + amount_msat=local_amount, + next_htlc_id = 0, + funding_locked_received=True, + was_announced=False, + current_commitment_signature=None, + feerate=local_feerate + ), + "constraints":lnbase.ChannelConstraints(capacity=funding_sat, is_initiator=is_initiator, funding_txn_minimum_depth=3), + "node_id":other_node_id + } + +def bip32(sequence): + xprv, xpub = bitcoin.bip32_root(b"9dk", 'standard') + xprv, xpub = bitcoin.bip32_private_derivation(xprv, "m/", sequence) + xtype, depth, fingerprint, child_number, c, k = bitcoin.deserialize_xprv(xprv) + assert len(k) == 32 + assert type(k) is bytes + return k + +def create_test_channels(): + funding_txid = binascii.hexlify(os.urandom(32)).decode("ascii") + funding_index = 0 + funding_sat = bitcoin.COIN * 10 + local_amount = (funding_sat * 1000) // 2 + remote_amount = (funding_sat * 1000) // 2 + alice_raw = [ bip32("m/" + str(i)) for i in range(5) ] + bob_raw = [ bip32("m/" + str(i)) for i in range(5,11) ] + alice_privkeys = [lnbase.Keypair(lnbase.privkey_to_pubkey(x), x) for x in alice_raw] + bob_privkeys = [lnbase.Keypair(lnbase.privkey_to_pubkey(x), x) for x in bob_raw] + alice_pubkeys = [lnbase.OnlyPubkeyKeypair(x.pubkey) for x in alice_privkeys] + bob_pubkeys = [lnbase.OnlyPubkeyKeypair(x.pubkey) for x in bob_privkeys] + + alice_seed = os.urandom(32) + bob_seed = os.urandom(32) + + alice_cur = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(alice_seed, 2**48 - 1), "big")) + alice_next = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(alice_seed, 2**48 - 2), "big")) + bob_cur = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(bob_seed, 2**48 - 1), "big")) + bob_next = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(bob_seed, 2**48 - 2), "big")) + + return \ + lnhtlc.HTLCStateMachine( + create_channel_state(funding_txid, funding_index, funding_sat, 6000, True, local_amount, remote_amount, alice_privkeys, bob_pubkeys, alice_seed, bob_cur, bob_next, b"\x02"*33, l_dust=200, r_dust=1300, l_csv=5, r_csv=4), "alice"), \ + lnhtlc.HTLCStateMachine( + create_channel_state(funding_txid, funding_index, funding_sat, 6000, False, remote_amount, local_amount, bob_privkeys, alice_pubkeys, bob_seed, alice_cur, alice_next, b"\x01"*33, l_dust=1300, r_dust=200, l_csv=4, r_csv=5), "bob") + +one_bitcoin_in_msat = bitcoin.COIN * 1000 + +class TestLNBaseHTLCStateMachine(unittest.TestCase): + def assertOutputExistsByValue(self, tx, amt_sat): + for typ, scr, val in tx.outputs(): + if val == amt_sat: + break + else: + self.assertFalse() + + def setUp(self): + # Create a test channel which will be used for the duration of this + # unittest. The channel will be funded evenly with Alice having 5 BTC, + # and Bob having 5 BTC. + self.alice_channel, self.bob_channel = create_test_channels() + + self.paymentPreimage = b"\x01" * 32 + paymentHash = bitcoin.sha256(self.paymentPreimage) + self.htlc = lnhtlc.UpdateAddHtlc( + payment_hash = paymentHash, + amount_msat = one_bitcoin_in_msat, + cltv_expiry = 5, + total_fee = 0 + ) + + # First Alice adds the outgoing HTLC to her local channel's state + # update log. Then Alice sends this wire message over to Bob who adds + # this htlc to his remote state update log. + self.aliceHtlcIndex = self.alice_channel.add_htlc(self.htlc) + + self.bobHtlcIndex = self.bob_channel.receive_htlc(self.htlc) + + def test_SimpleAddSettleWorkflow(self): + alice_channel, bob_channel = self.alice_channel, self.bob_channel + htlc = self.htlc + + # Next alice commits this change by sending a signature message. Since + # we expect the messages to be ordered, Bob will receive the HTLC we + # just sent before he receives this signature, so the signature will + # cover the HTLC. + aliceSig, aliceHtlcSigs = alice_channel.sign_next_commitment() + + self.assertEqual(len(aliceHtlcSigs), 1, "alice should generate one htlc signature") + + # Bob receives this signature message, and checks that this covers the + # state he has in his remote log. This includes the HTLC just sent + # from Alice. + bob_channel.receive_new_commitment(aliceSig, aliceHtlcSigs) + + # Bob revokes his prior commitment given to him by Alice, since he now + # has a valid signature for a newer commitment. + bobRevocation, _ = bob_channel.revoke_current_commitment() + + # Bob finally send a signature for Alice's commitment transaction. + # This signature will cover the HTLC, since Bob will first send the + # revocation just created. The revocation also acks every received + # HTLC up to the point where Alice sent here signature. + bobSig, bobHtlcSigs = bob_channel.sign_next_commitment() + + # Alice then processes this revocation, sending her own revocation for + # her prior commitment transaction. Alice shouldn't have any HTLCs to + # forward since she's sending an outgoing HTLC. + alice_channel.receive_revocation(bobRevocation) + + # Alice then processes bob's signature, and since she just received + # the revocation, she expect this signature to cover everything up to + # the point where she sent her signature, including the HTLC. + alice_channel.receive_new_commitment(bobSig, bobHtlcSigs) + + # Alice then generates a revocation for bob. + aliceRevocation, _ = alice_channel.revoke_current_commitment() + + # Finally Bob processes Alice's revocation, at this point the new HTLC + # is fully locked in within both commitment transactions. Bob should + # also be able to forward an HTLC now that the HTLC has been locked + # into both commitment transactions. + bob_channel.receive_revocation(aliceRevocation) + + # At this point, both sides should have the proper number of satoshis + # sent, and commitment height updated within their local channel + # state. + aliceSent = 0 + bobSent = 0 + + self.assertEqual(alice_channel.total_msat_sent, aliceSent, "alice has incorrect milli-satoshis sent") + self.assertEqual(alice_channel.total_msat_received, bobSent, "alice has incorrect milli-satoshis received") + self.assertEqual(bob_channel.total_msat_sent, bobSent, "bob has incorrect milli-satoshis sent") + self.assertEqual(bob_channel.total_msat_received, aliceSent, "bob has incorrect milli-satoshis received") + self.assertEqual(bob_channel.local_state.ctn, 1, "bob has incorrect commitment height") + self.assertEqual(alice_channel.local_state.ctn, 1, "alice has incorrect commitment height") + + # Both commitment transactions should have three outputs, and one of + # them should be exactly the amount of the HTLC. + self.assertEqual(len(alice_channel.local_commitment.outputs()), 3, "alice should have three commitment outputs, instead have %s"% len(alice_channel.local_commitment.outputs())) + self.assertEqual(len(bob_channel.local_commitment.outputs()), 3, "bob should have three commitment outputs, instead have %s"% len(bob_channel.local_commitment.outputs())) + self.assertOutputExistsByValue(alice_channel.local_commitment, htlc.amount_msat // 1000) + self.assertOutputExistsByValue(bob_channel.local_commitment, htlc.amount_msat // 1000) + + # Now we'll repeat a similar exchange, this time with Bob settling the + # HTLC once he learns of the preimage. + preimage = self.paymentPreimage + bob_channel.settle_htlc(preimage, self.bobHtlcIndex) + + alice_channel.receive_htlc_settle(preimage, self.aliceHtlcIndex) + + bobSig2, bobHtlcSigs2 = bob_channel.sign_next_commitment() + alice_channel.receive_new_commitment(bobSig2, bobHtlcSigs2) + + aliceRevocation2, _ = alice_channel.revoke_current_commitment() + aliceSig2, aliceHtlcSigs2 = alice_channel.sign_next_commitment() + self.assertEqual(aliceHtlcSigs2, [], "alice should generate no htlc signatures") + + bob_channel.receive_revocation(aliceRevocation2) + + bob_channel.receive_new_commitment(aliceSig2, aliceHtlcSigs2) + + bobRevocation2, _ = bob_channel.revoke_current_commitment() + alice_channel.receive_revocation(bobRevocation2) + + # At this point, Bob should have 6 BTC settled, with Alice still having + # 4 BTC. Alice's channel should show 1 BTC sent and Bob's channel + # should show 1 BTC received. They should also be at commitment height + # two, with the revocation window extended by 1 (5). + mSatTransferred = one_bitcoin_in_msat + self.assertEqual(alice_channel.total_msat_sent, mSatTransferred, "alice satoshis sent incorrect %s vs %s expected"% (alice_channel.total_msat_sent, mSatTransferred)) + 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.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 + # 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)) + + def alice_to_bob_fee_update(self): + fee = 111 + self.alice_channel.update_fee(fee) + self.bob_channel.receive_update_fee(fee) + return fee + + def test_UpdateFeeSenderCommits(self): + fee = self.alice_to_bob_fee_update() + + alice_channel, bob_channel = self.alice_channel, self.bob_channel + + alice_sig, alice_htlc_sigs = alice_channel.sign_next_commitment() + bob_channel.receive_new_commitment(alice_sig, alice_htlc_sigs) + + self.assertNotEqual(fee, bob_channel.local_state.feerate) + rev, _ = bob_channel.revoke_current_commitment() + self.assertEqual(fee, bob_channel.local_state.feerate) + + bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment() + alice_channel.receive_revocation(rev) + alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs) + + self.assertNotEqual(fee, alice_channel.local_state.feerate) + rev, _ = alice_channel.revoke_current_commitment() + self.assertEqual(fee, alice_channel.local_state.feerate) + + bob_channel.receive_revocation(rev) + + + def test_UpdateFeeReceiverCommits(self): + fee = self.alice_to_bob_fee_update() + + alice_channel, bob_channel = self.alice_channel, self.bob_channel + + bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment() + alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs) + + alice_revocation, _ = alice_channel.revoke_current_commitment() + bob_channel.receive_revocation(alice_revocation) + alice_sig, alice_htlc_sigs = alice_channel.sign_next_commitment() + bob_channel.receive_new_commitment(alice_sig, alice_htlc_sigs) + + self.assertNotEqual(fee, bob_channel.local_state.feerate) + bob_revocation, _ = bob_channel.revoke_current_commitment() + self.assertEqual(fee, bob_channel.local_state.feerate) + + bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment() + alice_channel.receive_revocation(bob_revocation) + alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs) + + self.assertNotEqual(fee, alice_channel.local_state.feerate) + alice_revocation, _ = alice_channel.revoke_current_commitment() + self.assertEqual(fee, alice_channel.local_state.feerate) + + bob_channel.receive_revocation(alice_revocation) + + + +class TestLNHTLCDust(unittest.TestCase): + def test_HTLCDustLimit(self): + alice_channel, bob_channel = create_test_channels() + + paymentPreimage = b"\x01" * 32 + paymentHash = bitcoin.sha256(paymentPreimage) + fee_per_kw = alice_channel.local_state.feerate + 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 + total_fee = 0 + ) + + aliceHtlcIndex = alice_channel.add_htlc(htlc) + bobHtlcIndex = bob_channel.receive_htlc(htlc) + force_state_transition(alice_channel, bob_channel) + self.assertEqual(len(alice_channel.local_commitment.outputs()), 3) + self.assertEqual(len(bob_channel.local_commitment.outputs()), 2) + default_fee = calc_static_fee(0) + self.assertEqual(bob_channel.local_commit_fee, default_fee + htlcAmt) + bob_channel.settle_htlc(paymentPreimage, htlc.htlc_id) + alice_channel.receive_htlc_settle(paymentPreimage, aliceHtlcIndex) + force_state_transition(bob_channel, alice_channel) + self.assertEqual(len(alice_channel.local_commitment.outputs()), 2) + self.assertEqual(alice_channel.total_msat_sent // 1000, htlcAmt) + +def force_state_transition(chanA, chanB): + chanB.receive_new_commitment(*chanA.sign_next_commitment()) + rev, _ = chanB.revoke_current_commitment() + bob_sig, bob_htlc_sigs = chanB.sign_next_commitment() + chanA.receive_revocation(rev) + chanA.receive_new_commitment(bob_sig, bob_htlc_sigs) + chanB.receive_revocation(chanA.revoke_current_commitment()[0]) + +# calcStaticFee calculates appropriate fees for commitment transactions. This +# function provides a simple way to allow test balance assertions to take fee +# calculations into account. +def calc_static_fee(numHTLCs): + commitWeight = 724 + htlcWeight = 172 + feePerKw = 24//4 * 1000 + return feePerKw * (commitWeight + htlcWeight*numHTLCs) // 1000 DIR diff --git a/electrum/tests/test_lnrouter.py b/electrum/tests/test_lnrouter.py t@@ -0,0 +1,151 @@ +import unittest + +from electrum.util import bh2u, bfh +from electrum.lnbase import Peer +from electrum.lnrouter import OnionHopsDataSingle, new_onion_packet, OnionPerHop +from electrum import bitcoin, lnrouter + +class Test_LNRouter(unittest.TestCase): + + #@staticmethod + #def parse_witness_list(witness_bytes): + # amount_witnesses = witness_bytes[0] + # witness_bytes = witness_bytes[1:] + # res = [] + # for i in range(amount_witnesses): + # witness_length = witness_bytes[0] + # this_witness = witness_bytes[1:witness_length+1] + # assert len(this_witness) == witness_length + # witness_bytes = witness_bytes[witness_length+1:] + # res += [bytes(this_witness)] + # assert witness_bytes == b"", witness_bytes + # return res + + + + def test_find_path_for_payment(self): + class fake_network: + channel_db = lnrouter.ChannelDB() + trigger_callback = lambda x: None + class fake_ln_worker: + path_finder = lnrouter.LNPathFinder(fake_network.channel_db) + privkey = bitcoin.sha256('privkeyseed') + network = fake_network + channel_state = {} + channels = [] + invoices = {} + p = Peer(fake_ln_worker, '', 0, 'a') + p.on_channel_announcement({'node_id_1': b'b', 'node_id_2': b'c', 'short_channel_id': bfh('0000000000000001')}) + p.on_channel_announcement({'node_id_1': b'b', 'node_id_2': b'e', 'short_channel_id': bfh('0000000000000002')}) + p.on_channel_announcement({'node_id_1': b'a', 'node_id_2': b'b', 'short_channel_id': bfh('0000000000000003')}) + p.on_channel_announcement({'node_id_1': b'c', 'node_id_2': b'd', 'short_channel_id': bfh('0000000000000004')}) + p.on_channel_announcement({'node_id_1': b'd', 'node_id_2': b'e', 'short_channel_id': bfh('0000000000000005')}) + p.on_channel_announcement({'node_id_1': b'a', 'node_id_2': b'd', 'short_channel_id': bfh('0000000000000006')}) + o = lambda i: i.to_bytes(8, "big") + p.on_channel_update({'short_channel_id': bfh('0000000000000001'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)}) + p.on_channel_update({'short_channel_id': bfh('0000000000000001'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)}) + p.on_channel_update({'short_channel_id': bfh('0000000000000002'), 'flags': b'\x00', 'cltv_expiry_delta': o(99), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)}) + p.on_channel_update({'short_channel_id': bfh('0000000000000002'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)}) + p.on_channel_update({'short_channel_id': bfh('0000000000000003'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)}) + p.on_channel_update({'short_channel_id': bfh('0000000000000003'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)}) + p.on_channel_update({'short_channel_id': bfh('0000000000000004'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)}) + p.on_channel_update({'short_channel_id': bfh('0000000000000004'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)}) + p.on_channel_update({'short_channel_id': bfh('0000000000000005'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)}) + p.on_channel_update({'short_channel_id': bfh('0000000000000005'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(999)}) + p.on_channel_update({'short_channel_id': bfh('0000000000000006'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(99999999)}) + p.on_channel_update({'short_channel_id': bfh('0000000000000006'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)}) + self.assertNotEqual(None, fake_ln_worker.path_finder.find_path_for_payment(b'a', b'e', 100000)) + + + + def test_new_onion_packet(self): + # test vector from bolt-04 + payment_path_pubkeys = [ + bfh('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619'), + bfh('0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c'), + bfh('027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007'), + bfh('032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991'), + bfh('02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145'), + ] + session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141') + associated_data = bfh('4242424242424242424242424242424242424242424242424242424242424242') + hops_data = [ + OnionHopsDataSingle(OnionPerHop( + bfh('0000000000000000'), bfh('0000000000000000'), bfh('00000000') + )), + OnionHopsDataSingle(OnionPerHop( + bfh('0101010101010101'), bfh('0000000000000001'), bfh('00000001') + )), + OnionHopsDataSingle(OnionPerHop( + bfh('0202020202020202'), bfh('0000000000000002'), bfh('00000002') + )), + OnionHopsDataSingle(OnionPerHop( + bfh('0303030303030303'), bfh('0000000000000003'), bfh('00000003') + )), + OnionHopsDataSingle(OnionPerHop( + bfh('0404040404040404'), bfh('0000000000000004'), bfh('00000004') + )), + ] + packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data) parazyd.org:70 /git/electrum/commit/35adc3231b297d03ad0a0534d65665ad14c0d9f6.gph:805: line too long