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