Browse Source

Trampoline routing.

- trampoline is enabled by default in config, to prevent download of `gossip_db`.
   (if disabled, `gossip_db` will be downloaded, regardless of the existence of channels)
 - if trampoline is enabled:
    - the wallet can only open channels with trampoline nodes
    - already-existing channels with non-trampoline nodes are frozen for sending.
 - there are two types of trampoline payments: legacy and end-to-end (e2e).
 - we decide to perform legacy or e2e based on the invoice:
    - we use trampoline_routing_opt in features to detect Eclair and Phoenix invoices
    - we use trampoline_routing_hints to detect Electrum invoices
 - when trying a legacy payment, we add a second trampoline to the path to preserve privacy.
   (we fall back to a single trampoline if the payment fails for all trampolines)
 - the trampoline list is hardcoded, it will remain so until `trampoline_routing_opt` feature flag is in INIT.
 - there are currently only two nodes in the hardcoded list, it would be nice to have more.
 - similar to Phoenix, we find the fee/cltv by trial-and-error.
    - if there is a second trampoline in the path, we use the same fee for both.
    - the final spec should add fee info in error messages, so we will be able to fine-tune fees
master
ThomasV 5 years ago
parent
commit
ded449233e
  1. 3
      electrum/daemon.py
  2. 9
      electrum/gui/kivy/main_window.py
  3. 10
      electrum/gui/kivy/uix/dialogs/lightning_channels.py
  4. 6
      electrum/gui/kivy/uix/dialogs/lightning_open_channel.py
  5. 11
      electrum/gui/kivy/uix/dialogs/settings.py
  6. 17
      electrum/gui/qt/channels_list.py
  7. 10
      electrum/gui/qt/main_window.py
  8. 18
      electrum/gui/qt/settings_dialog.py
  9. 11
      electrum/lnaddr.py
  10. 2
      electrum/lnchannel.py
  11. 83
      electrum/lnonion.py
  12. 119
      electrum/lnpeer.py
  13. 10
      electrum/lnrouter.py
  14. 10
      electrum/lnutil.py
  15. 11
      electrum/lnwire/onion_wire.csv
  16. 290
      electrum/lnworker.py
  17. 21
      electrum/network.py
  18. 6
      electrum/sql_db.py
  19. 1
      electrum/tests/regtest/regtest.sh

3
electrum/daemon.py

@ -450,7 +450,8 @@ class Daemon(Logger):
if self.network: if self.network:
self.network.start(jobs=[self.fx.run]) self.network.start(jobs=[self.fx.run])
# prepare lightning functionality, also load channel db early # prepare lightning functionality, also load channel db early
self.network.init_channel_db() if self.config.get('use_gossip', False):
self.network.start_gossip()
self.taskgroup = TaskGroup() self.taskgroup = TaskGroup()
asyncio.run_coroutine_threadsafe(self._run(jobs=daemon_jobs), self.asyncio_loop) asyncio.run_coroutine_threadsafe(self._run(jobs=daemon_jobs), self.asyncio_loop)

9
electrum/gui/kivy/main_window.py

@ -183,6 +183,14 @@ class ElectrumWindow(App, Logger):
def on_use_rbf(self, instance, x): def on_use_rbf(self, instance, x):
self.electrum_config.set_key('use_rbf', self.use_rbf, True) self.electrum_config.set_key('use_rbf', self.use_rbf, True)
use_gossip = BooleanProperty(False)
def on_use_gossip(self, instance, x):
self.electrum_config.set_key('use_gossip', self.use_gossip, True)
if self.use_gossip:
self.network.start_gossip()
else:
self.network.stop_gossip()
android_backups = BooleanProperty(False) android_backups = BooleanProperty(False)
def on_android_backups(self, instance, x): def on_android_backups(self, instance, x):
self.electrum_config.set_key('android_backups', self.android_backups, True) self.electrum_config.set_key('android_backups', self.android_backups, True)
@ -394,6 +402,7 @@ class ElectrumWindow(App, Logger):
self.fx = self.daemon.fx self.fx = self.daemon.fx
self.use_rbf = config.get('use_rbf', True) self.use_rbf = config.get('use_rbf', True)
self.android_backups = config.get('android_backups', False) self.android_backups = config.get('android_backups', False)
self.use_gossip = config.get('use_gossip', False)
self.use_unconfirmed = not config.get('confirmed_only', False) self.use_unconfirmed = not config.get('confirmed_only', False)
# create triggers so as to minimize updating a max of 2 times a sec # create triggers so as to minimize updating a max of 2 times a sec

10
electrum/gui/kivy/uix/dialogs/lightning_channels.py

@ -234,6 +234,7 @@ Builder.load_string(r'''
can_send:'' can_send:''
can_receive:'' can_receive:''
is_open:False is_open:False
warning: ''
BoxLayout: BoxLayout:
padding: '12dp', '12dp', '12dp', '12dp' padding: '12dp', '12dp', '12dp', '12dp'
spacing: '12dp' spacing: '12dp'
@ -246,6 +247,9 @@ Builder.load_string(r'''
height: self.minimum_height height: self.minimum_height
size_hint_y: None size_hint_y: None
spacing: '5dp' spacing: '5dp'
TopLabel:
text: root.warning
color: .905, .709, .509, 1
BoxLabel: BoxLabel:
text: _('Channel ID') text: _('Channel ID')
value: root.short_id value: root.short_id
@ -470,6 +474,12 @@ class ChannelDetailsPopup(Popup, Logger):
closed = chan.get_closing_height() closed = chan.get_closing_height()
if closed: if closed:
self.closing_txid, closing_height, closing_timestamp = closed self.closing_txid, closing_height, closing_timestamp = closed
msg = ' '.join([
_("Trampoline routing is enabled, but this channel is with a non-trampoline node."),
_("This channel may still be used for receiving, but it is frozen for sending."),
_("If you want to keep using this channel, you need to disable trampoline routing in your preferences."),
])
self.warning = '' if self.app.wallet.lnworker.channel_db or chan.is_trampoline() else _('Warning') + ': ' + msg
def close(self): def close(self):
Question(_('Close channel?'), self._close).open() Question(_('Close channel?'), self._close).open()

6
electrum/gui/kivy/uix/dialogs/lightning_open_channel.py

@ -107,13 +107,11 @@ class LightningOpenChannelDialog(Factory.Popup, Logger):
d.open() d.open()
def suggest_node(self): def suggest_node(self):
self.app.wallet.network.start_gossip() suggested = self.app.wallet.lnworker.suggest_peer()
suggested = self.app.wallet.lnworker.lnrater.suggest_peer()
_, _, percent = self.app.wallet.network.lngossip.get_sync_progress_estimate()
if suggested: if suggested:
self.pubkey = suggested.hex() self.pubkey = suggested.hex()
else: else:
_, _, percent = self.app.wallet.network.lngossip.get_sync_progress_estimate()
if percent is None: if percent is None:
percent = "??" percent = "??"
self.pubkey = f"Please wait, graph is updating ({percent}% / 30% done)." self.pubkey = f"Please wait, graph is updating ({percent}% / 30% done)."

11
electrum/gui/kivy/uix/dialogs/settings.py

@ -95,6 +95,17 @@ Builder.load_string('''
description: _('Change your password') if app._use_single_password else _("Change your password for this wallet.") description: _('Change your password') if app._use_single_password else _("Change your password for this wallet.")
action: root.change_password action: root.change_password
CardSeparator CardSeparator
SettingsItem:
status: _('Trampoline') if not app.use_gossip else _('Gossip')
title: _('Lightning Routing') + ': ' + self.status
description: _("Use trampoline routing or gossip.")
message:
_('Lightning payments require finding a path through the Lightning Network.')\
+ ' ' + ('You may use trampoline routing, or local routing (gossip).')\
+ ' ' + ('Downloading the network gossip uses quite some bandwidth and storage, and is not recommended on mobile devices.')\
+ ' ' + ('If you use trampoline, you can only open channels with trampoline nodes.')
action: partial(root.boolean_dialog, 'use_gossip', _('Download Gossip'), self.message)
CardSeparator
SettingsItem: SettingsItem:
status: _('Yes') if app.android_backups else _('No') status: _('Yes') if app.android_backups else _('No')
title: _('Backups') + ': ' + self.status title: _('Backups') + ': ' + self.status

17
electrum/gui/qt/channels_list.py

@ -145,6 +145,17 @@ class ChannelsList(MyTreeView):
self.main_window.show_message('success') self.main_window.show_message('success')
WaitingDialog(self, 'please wait..', task, on_success, self.on_failure) WaitingDialog(self, 'please wait..', task, on_success, self.on_failure)
def freeze_channel_for_sending(self, chan, b):
if self.lnworker.channel_db or self.lnworker.is_trampoline_peer(chan.node_id):
chan.set_frozen_for_sending(b)
else:
msg = ' '.join([
_("Trampoline routing is enabled, but this channel is with a non-trampoline node."),
_("This channel may still be used for receiving, but it is frozen for sending."),
_("If you want to keep using this channel, you need to disable trampoline routing in your preferences."),
])
self.main_window.show_warning(msg, title=_('Channel is frozen for sending'))
def create_menu(self, position): def create_menu(self, position):
menu = QMenu() menu = QMenu()
menu.setSeparatorsCollapsible(True) # consecutive separators are merged together menu.setSeparatorsCollapsible(True) # consecutive separators are merged together
@ -177,9 +188,9 @@ class ChannelsList(MyTreeView):
channel_id.hex(), title=_("Long Channel ID"))) channel_id.hex(), title=_("Long Channel ID")))
if not chan.is_closed(): if not chan.is_closed():
if not chan.is_frozen_for_sending(): if not chan.is_frozen_for_sending():
menu.addAction(_("Freeze (for sending)"), lambda: chan.set_frozen_for_sending(True)) menu.addAction(_("Freeze (for sending)"), lambda: self.freeze_channel_for_sending(chan, True))
else: else:
menu.addAction(_("Unfreeze (for sending)"), lambda: chan.set_frozen_for_sending(False)) menu.addAction(_("Unfreeze (for sending)"), lambda: self.freeze_channel_for_sending(chan, False))
if not chan.is_frozen_for_receiving(): if not chan.is_frozen_for_receiving():
menu.addAction(_("Freeze (for receiving)"), lambda: chan.set_frozen_for_receiving(True)) menu.addAction(_("Freeze (for receiving)"), lambda: chan.set_frozen_for_receiving(True))
else: else:
@ -359,7 +370,7 @@ class ChannelsList(MyTreeView):
suggest_button = QPushButton(d, text=_('Suggest Peer')) suggest_button = QPushButton(d, text=_('Suggest Peer'))
def on_suggest(): def on_suggest():
self.parent.wallet.network.start_gossip() self.parent.wallet.network.start_gossip()
nodeid = bh2u(lnworker.lnrater.suggest_peer() or b'') nodeid = bh2u(lnworker.suggest_peer() or b'')
if not nodeid: if not nodeid:
remote_nodeid.setText("") remote_nodeid.setText("")
remote_nodeid.setPlaceholderText( remote_nodeid.setPlaceholderText(

10
electrum/gui/qt/main_window.py

@ -742,7 +742,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
tools_menu.addAction(_("Electrum preferences"), self.settings_dialog) tools_menu.addAction(_("Electrum preferences"), self.settings_dialog)
tools_menu.addAction(_("&Network"), self.gui_object.show_network_dialog).setEnabled(bool(self.network)) tools_menu.addAction(_("&Network"), self.gui_object.show_network_dialog).setEnabled(bool(self.network))
tools_menu.addAction(_("&Lightning Network"), self.gui_object.show_lightning_dialog).setEnabled(bool(self.wallet.has_lightning() and self.network)) tools_menu.addAction(_("&Lightning Gossip"), self.gui_object.show_lightning_dialog).setEnabled(bool(self.wallet.has_lightning() and self.network))
tools_menu.addAction(_("Local &Watchtower"), self.gui_object.show_watchtower_dialog).setEnabled(bool(self.network and self.network.local_watchtower)) tools_menu.addAction(_("Local &Watchtower"), self.gui_object.show_watchtower_dialog).setEnabled(bool(self.network and self.network.local_watchtower))
tools_menu.addAction(_("&Plugins"), self.plugins_dialog) tools_menu.addAction(_("&Plugins"), self.plugins_dialog)
tools_menu.addSeparator() tools_menu.addSeparator()
@ -2205,8 +2205,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.seed_button = StatusBarButton(read_QIcon("seed.png"), _("Seed"), self.show_seed_dialog ) self.seed_button = StatusBarButton(read_QIcon("seed.png"), _("Seed"), self.show_seed_dialog )
sb.addPermanentWidget(self.seed_button) sb.addPermanentWidget(self.seed_button)
self.lightning_button = None self.lightning_button = None
if self.wallet.has_lightning() and self.network: if self.wallet.has_lightning():
self.lightning_button = StatusBarButton(read_QIcon("lightning_disconnected.png"), _("Lightning Network"), self.gui_object.show_lightning_dialog) self.lightning_button = StatusBarButton(read_QIcon("lightning.png"), _("Lightning Network"), self.gui_object.show_lightning_dialog)
self.update_lightning_icon() self.update_lightning_icon()
sb.addPermanentWidget(self.lightning_button) sb.addPermanentWidget(self.lightning_button)
self.status_button = None self.status_button = None
@ -2247,10 +2247,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
if self.lightning_button is None: if self.lightning_button is None:
return return
if self.network.lngossip is None: if self.network.lngossip is None:
self.lightning_button.setVisible(False)
return return
# display colorful lightning icon to signal connection self.lightning_button.setVisible(True)
self.lightning_button.setIcon(read_QIcon("lightning.png"))
cur, total, progress_percent = self.network.lngossip.get_sync_progress_estimate() cur, total, progress_percent = self.network.lngossip.get_sync_progress_estimate()
# self.logger.debug(f"updating lngossip sync progress estimate: cur={cur}, total={total}") # self.logger.debug(f"updating lngossip sync progress estimate: cur={cur}, total={total}")

18
electrum/gui/qt/settings_dialog.py

@ -130,6 +130,24 @@ class SettingsDialog(WindowModalDialog):
# lightning # lightning
lightning_widgets = [] lightning_widgets = []
help_gossip = _("""If this option is enabled, Electrum will download the network
channels graph and compute payment path locally, instead of using trampoline payments. """)
gossip_cb = QCheckBox(_("Download network graph"))
gossip_cb.setToolTip(help_gossip)
gossip_cb.setChecked(bool(self.config.get('use_gossip', False)))
def on_gossip_checked(x):
use_gossip = bool(x)
self.config.set_key('use_gossip', use_gossip)
if use_gossip:
self.window.network.start_gossip()
else:
self.window.network.stop_gossip()
util.trigger_callback('ln_gossip_sync_progress')
# FIXME: update all wallet windows
util.trigger_callback('channels_updated', self.wallet)
gossip_cb.stateChanged.connect(on_gossip_checked)
lightning_widgets.append((gossip_cb, None))
help_local_wt = _("""If this option is checked, Electrum will help_local_wt = _("""If this option is checked, Electrum will
run a local watchtower and protect your channels even if your wallet is not run a local watchtower and protect your channels even if your wallet is not
open. For this to work, your computer needs to be online regularly.""") open. For this to work, your computer needs to be online regularly.""")

11
electrum/lnaddr.py

@ -215,6 +215,10 @@ def lnencode(addr: 'LnAddr', privkey) -> str:
pubkey, channel, feebase, feerate, cltv = step pubkey, channel, feebase, feerate, cltv = step
route.append(bitstring.BitArray(pubkey) + bitstring.BitArray(channel) + bitstring.pack('intbe:32', feebase) + bitstring.pack('intbe:32', feerate) + bitstring.pack('intbe:16', cltv)) route.append(bitstring.BitArray(pubkey) + bitstring.BitArray(channel) + bitstring.pack('intbe:32', feebase) + bitstring.pack('intbe:32', feerate) + bitstring.pack('intbe:16', cltv))
data += tagged('r', route) data += tagged('r', route)
elif k == 't':
pubkey, feebase, feerate, cltv = v
route = bitstring.BitArray(pubkey) + bitstring.pack('intbe:32', feebase) + bitstring.pack('intbe:32', feerate) + bitstring.pack('intbe:16', cltv)
data += tagged('t', route)
elif k == 'f': elif k == 'f':
data += encode_fallback(v, addr.currency) data += encode_fallback(v, addr.currency)
elif k == 'd': elif k == 'd':
@ -409,6 +413,13 @@ def lndecode(invoice: str, *, verbose=False, expected_hrp=None) -> LnAddr:
s.read(32).uintbe, s.read(32).uintbe,
s.read(16).uintbe)) s.read(16).uintbe))
addr.tags.append(('r',route)) addr.tags.append(('r',route))
elif tag == 't':
s = bitstring.ConstBitStream(tagdata)
e = (s.read(264).tobytes(),
s.read(32).uintbe,
s.read(32).uintbe,
s.read(16).uintbe)
addr.tags.append(('t', e))
elif tag == 'f': elif tag == 'f':
fallback = parse_fallback(tagdata, addr.currency) fallback = parse_fallback(tagdata, addr.currency)
if fallback: if fallback:

2
electrum/lnchannel.py

@ -720,6 +720,8 @@ class Channel(AbstractChannel):
return self.can_send_ctx_updates() and not self.is_closing() return self.can_send_ctx_updates() and not self.is_closing()
def is_frozen_for_sending(self) -> bool: def is_frozen_for_sending(self) -> bool:
if self.lnworker and self.lnworker.channel_db is None and not self.lnworker.is_trampoline_peer(self.node_id):
return True
return self.storage.get('frozen_for_sending', False) return self.storage.get('frozen_for_sending', False)
def set_frozen_for_sending(self, b: bool) -> None: def set_frozen_for_sending(self, b: bool) -> None:

83
electrum/lnonion.py

@ -40,8 +40,8 @@ if TYPE_CHECKING:
HOPS_DATA_SIZE = 1300 # also sometimes called routingInfoSize in bolt-04 HOPS_DATA_SIZE = 1300 # also sometimes called routingInfoSize in bolt-04
TRAMPOLINE_HOPS_DATA_SIZE = 400
LEGACY_PER_HOP_FULL_SIZE = 65 LEGACY_PER_HOP_FULL_SIZE = 65
NUM_STREAM_BYTES = 2 * HOPS_DATA_SIZE
PER_HOP_HMAC_SIZE = 32 PER_HOP_HMAC_SIZE = 32
@ -169,7 +169,7 @@ class OnionPacket:
def __init__(self, public_key: bytes, hops_data: bytes, hmac: bytes): def __init__(self, public_key: bytes, hops_data: bytes, hmac: bytes):
assert len(public_key) == 33 assert len(public_key) == 33
assert len(hops_data) == HOPS_DATA_SIZE assert len(hops_data) in [ HOPS_DATA_SIZE, TRAMPOLINE_HOPS_DATA_SIZE ]
assert len(hmac) == PER_HOP_HMAC_SIZE assert len(hmac) == PER_HOP_HMAC_SIZE
self.version = 0 self.version = 0
self.public_key = public_key self.public_key = public_key
@ -183,21 +183,21 @@ class OnionPacket:
ret += self.public_key ret += self.public_key
ret += self.hops_data ret += self.hops_data
ret += self.hmac ret += self.hmac
if len(ret) != 1366: if len(ret) - 66 not in [ HOPS_DATA_SIZE, TRAMPOLINE_HOPS_DATA_SIZE ]:
raise Exception('unexpected length {}'.format(len(ret))) raise Exception('unexpected length {}'.format(len(ret)))
return ret return ret
@classmethod @classmethod
def from_bytes(cls, b: bytes): def from_bytes(cls, b: bytes):
if len(b) != 1366: if len(b) - 66 not in [ HOPS_DATA_SIZE, TRAMPOLINE_HOPS_DATA_SIZE ]:
raise Exception('unexpected length {}'.format(len(b))) raise Exception('unexpected length {}'.format(len(b)))
version = b[0] version = b[0]
if version != 0: if version != 0:
raise UnsupportedOnionPacketVersion('version {} is not supported'.format(version)) raise UnsupportedOnionPacketVersion('version {} is not supported'.format(version))
return OnionPacket( return OnionPacket(
public_key=b[1:34], public_key=b[1:34],
hops_data=b[34:1334], hops_data=b[34:-32],
hmac=b[1334:] hmac=b[-32:]
) )
@ -226,25 +226,26 @@ def get_shared_secrets_along_route(payment_path_pubkeys: Sequence[bytes],
def new_onion_packet(payment_path_pubkeys: Sequence[bytes], session_key: bytes, def new_onion_packet(payment_path_pubkeys: Sequence[bytes], session_key: bytes,
hops_data: Sequence[OnionHopsDataSingle], associated_data: bytes) -> OnionPacket: hops_data: Sequence[OnionHopsDataSingle], associated_data: bytes, trampoline=False) -> OnionPacket:
num_hops = len(payment_path_pubkeys) num_hops = len(payment_path_pubkeys)
assert num_hops == len(hops_data) assert num_hops == len(hops_data)
hop_shared_secrets = get_shared_secrets_along_route(payment_path_pubkeys, session_key) hop_shared_secrets = get_shared_secrets_along_route(payment_path_pubkeys, session_key)
filler = _generate_filler(b'rho', hops_data, hop_shared_secrets) data_size = TRAMPOLINE_HOPS_DATA_SIZE if trampoline else HOPS_DATA_SIZE
filler = _generate_filler(b'rho', hops_data, hop_shared_secrets, data_size)
next_hmac = bytes(PER_HOP_HMAC_SIZE) next_hmac = bytes(PER_HOP_HMAC_SIZE)
# Our starting packet needs to be filled out with random bytes, we # Our starting packet needs to be filled out with random bytes, we
# generate some deterministically using the session private key. # generate some deterministically using the session private key.
pad_key = get_bolt04_onion_key(b'pad', session_key) pad_key = get_bolt04_onion_key(b'pad', session_key)
mix_header = generate_cipher_stream(pad_key, HOPS_DATA_SIZE) mix_header = generate_cipher_stream(pad_key, data_size)
# compute routing info and MAC for each hop # compute routing info and MAC for each hop
for i in range(num_hops-1, -1, -1): for i in range(num_hops-1, -1, -1):
rho_key = get_bolt04_onion_key(b'rho', hop_shared_secrets[i]) rho_key = get_bolt04_onion_key(b'rho', hop_shared_secrets[i])
mu_key = get_bolt04_onion_key(b'mu', hop_shared_secrets[i]) mu_key = get_bolt04_onion_key(b'mu', hop_shared_secrets[i])
hops_data[i].hmac = next_hmac hops_data[i].hmac = next_hmac
stream_bytes = generate_cipher_stream(rho_key, HOPS_DATA_SIZE) stream_bytes = generate_cipher_stream(rho_key, data_size)
hop_data_bytes = hops_data[i].to_bytes() hop_data_bytes = hops_data[i].to_bytes()
mix_header = mix_header[:-len(hop_data_bytes)] mix_header = mix_header[:-len(hop_data_bytes)]
mix_header = hop_data_bytes + mix_header mix_header = hop_data_bytes + mix_header
@ -283,21 +284,28 @@ def calc_hops_data_for_payment(route: 'LNPaymentRoute', amount_msat: int,
# payloads, backwards from last hop (but excluding the first edge): # payloads, backwards from last hop (but excluding the first edge):
for edge_index in range(len(route) - 1, 0, -1): for edge_index in range(len(route) - 1, 0, -1):
route_edge = route[edge_index] route_edge = route[edge_index]
is_trampoline = route_edge.is_trampoline()
if is_trampoline:
amt += route_edge.fee_for_edge(amt)
cltv += route_edge.cltv_expiry_delta
hop_payload = { hop_payload = {
"amt_to_forward": {"amt_to_forward": amt}, "amt_to_forward": {"amt_to_forward": amt},
"outgoing_cltv_value": {"outgoing_cltv_value": cltv}, "outgoing_cltv_value": {"outgoing_cltv_value": cltv},
"short_channel_id": {"short_channel_id": route_edge.short_channel_id}, "short_channel_id": {"short_channel_id": route_edge.short_channel_id},
} }
hops_data += [OnionHopsDataSingle(is_tlv_payload=route[edge_index-1].has_feature_varonion(), hops_data.append(
payload=hop_payload)] OnionHopsDataSingle(
amt += route_edge.fee_for_edge(amt) is_tlv_payload=route[edge_index-1].has_feature_varonion(),
cltv += route_edge.cltv_expiry_delta payload=hop_payload))
if not is_trampoline:
amt += route_edge.fee_for_edge(amt)
cltv += route_edge.cltv_expiry_delta
hops_data.reverse() hops_data.reverse()
return hops_data, amt, cltv return hops_data, amt, cltv
def _generate_filler(key_type: bytes, hops_data: Sequence[OnionHopsDataSingle], def _generate_filler(key_type: bytes, hops_data: Sequence[OnionHopsDataSingle],
shared_secrets: Sequence[bytes]) -> bytes: shared_secrets: Sequence[bytes], data_size:int) -> bytes:
num_hops = len(hops_data) num_hops = len(hops_data)
# generate filler that matches all but the last hop (no HMAC for last hop) # generate filler that matches all but the last hop (no HMAC for last hop)
@ -308,16 +316,16 @@ def _generate_filler(key_type: bytes, hops_data: Sequence[OnionHopsDataSingle],
for i in range(0, num_hops-1): # -1, as last hop does not obfuscate for i in range(0, num_hops-1): # -1, as last hop does not obfuscate
# Sum up how many frames were used by prior hops. # Sum up how many frames were used by prior hops.
filler_start = HOPS_DATA_SIZE filler_start = data_size
for hop_data in hops_data[:i]: for hop_data in hops_data[:i]:
filler_start -= len(hop_data.to_bytes()) filler_start -= len(hop_data.to_bytes())
# The filler is the part dangling off of the end of the # The filler is the part dangling off of the end of the
# routingInfo, so offset it from there, and use the current # routingInfo, so offset it from there, and use the current
# hop's frame count as its size. # hop's frame count as its size.
filler_end = HOPS_DATA_SIZE + len(hops_data[i].to_bytes()) filler_end = data_size + len(hops_data[i].to_bytes())
stream_key = get_bolt04_onion_key(key_type, shared_secrets[i]) stream_key = get_bolt04_onion_key(key_type, shared_secrets[i])
stream_bytes = generate_cipher_stream(stream_key, NUM_STREAM_BYTES) stream_bytes = generate_cipher_stream(stream_key, 2 * data_size)
filler = xor_bytes(filler, stream_bytes[filler_start:filler_end]) filler = xor_bytes(filler, stream_bytes[filler_start:filler_end])
filler += bytes(filler_size - len(filler)) # right pad with zeroes filler += bytes(filler_size - len(filler)) # right pad with zeroes
@ -334,48 +342,59 @@ class ProcessedOnionPacket(NamedTuple):
are_we_final: bool are_we_final: bool
hop_data: OnionHopsDataSingle hop_data: OnionHopsDataSingle
next_packet: OnionPacket next_packet: OnionPacket
trampoline_onion_packet: OnionPacket
# TODO replay protection # TODO replay protection
def process_onion_packet(onion_packet: OnionPacket, associated_data: bytes, def process_onion_packet(
our_onion_private_key: bytes) -> ProcessedOnionPacket: onion_packet: OnionPacket,
associated_data: bytes,
our_onion_private_key: bytes) -> ProcessedOnionPacket:
if not ecc.ECPubkey.is_pubkey_bytes(onion_packet.public_key): if not ecc.ECPubkey.is_pubkey_bytes(onion_packet.public_key):
raise InvalidOnionPubkey() raise InvalidOnionPubkey()
shared_secret = get_ecdh(our_onion_private_key, onion_packet.public_key) shared_secret = get_ecdh(our_onion_private_key, onion_packet.public_key)
# check message integrity # check message integrity
mu_key = get_bolt04_onion_key(b'mu', shared_secret) mu_key = get_bolt04_onion_key(b'mu', shared_secret)
calculated_mac = hmac_oneshot(mu_key, msg=onion_packet.hops_data+associated_data, calculated_mac = hmac_oneshot(
digest=hashlib.sha256) mu_key, msg=onion_packet.hops_data+associated_data,
digest=hashlib.sha256)
if onion_packet.hmac != calculated_mac: if onion_packet.hmac != calculated_mac:
raise InvalidOnionMac() raise InvalidOnionMac()
# peel an onion layer off # peel an onion layer off
rho_key = get_bolt04_onion_key(b'rho', shared_secret) rho_key = get_bolt04_onion_key(b'rho', shared_secret)
stream_bytes = generate_cipher_stream(rho_key, NUM_STREAM_BYTES) stream_bytes = generate_cipher_stream(rho_key, 2 * HOPS_DATA_SIZE)
padded_header = onion_packet.hops_data + bytes(HOPS_DATA_SIZE) padded_header = onion_packet.hops_data + bytes(HOPS_DATA_SIZE)
next_hops_data = xor_bytes(padded_header, stream_bytes) next_hops_data = xor_bytes(padded_header, stream_bytes)
next_hops_data_fd = io.BytesIO(next_hops_data) next_hops_data_fd = io.BytesIO(next_hops_data)
hop_data = OnionHopsDataSingle.from_fd(next_hops_data_fd)
# trampoline
trampoline_onion_packet = hop_data.payload.get('trampoline_onion_packet')
if trampoline_onion_packet:
top_version = trampoline_onion_packet.get('version')
top_public_key = trampoline_onion_packet.get('public_key')
top_hops_data = trampoline_onion_packet.get('hops_data')
top_hops_data_fd = io.BytesIO(top_hops_data)
top_hmac = trampoline_onion_packet.get('hmac')
trampoline_onion_packet = OnionPacket(
public_key=top_public_key,
hops_data=top_hops_data_fd.read(TRAMPOLINE_HOPS_DATA_SIZE),
hmac=top_hmac)
# calc next ephemeral key # calc next ephemeral key
blinding_factor = sha256(onion_packet.public_key + shared_secret) blinding_factor = sha256(onion_packet.public_key + shared_secret)
blinding_factor_int = int.from_bytes(blinding_factor, byteorder="big") blinding_factor_int = int.from_bytes(blinding_factor, byteorder="big")
next_public_key_int = ecc.ECPubkey(onion_packet.public_key) * blinding_factor_int next_public_key_int = ecc.ECPubkey(onion_packet.public_key) * blinding_factor_int
next_public_key = next_public_key_int.get_public_key_bytes() next_public_key = next_public_key_int.get_public_key_bytes()
hop_data = OnionHopsDataSingle.from_fd(next_hops_data_fd)
next_onion_packet = OnionPacket( next_onion_packet = OnionPacket(
public_key=next_public_key, public_key=next_public_key,
hops_data=next_hops_data_fd.read(HOPS_DATA_SIZE), hops_data=next_hops_data_fd.read(HOPS_DATA_SIZE),
hmac=hop_data.hmac hmac=hop_data.hmac)
)
if hop_data.hmac == bytes(PER_HOP_HMAC_SIZE): if hop_data.hmac == bytes(PER_HOP_HMAC_SIZE):
# we are the destination / exit node # we are the destination / exit node
are_we_final = True are_we_final = True
else: else:
# we are an intermediate node; forwarding # we are an intermediate node; forwarding
are_we_final = False are_we_final = False
return ProcessedOnionPacket(are_we_final, hop_data, next_onion_packet) return ProcessedOnionPacket(are_we_final, hop_data, next_onion_packet, trampoline_onion_packet)
class FailedToDecodeOnionError(Exception): pass class FailedToDecodeOnionError(Exception): pass
@ -498,6 +517,8 @@ class OnionFailureCode(IntEnum):
EXPIRY_TOO_FAR = 21 EXPIRY_TOO_FAR = 21
INVALID_ONION_PAYLOAD = PERM | 22 INVALID_ONION_PAYLOAD = PERM | 22
MPP_TIMEOUT = 23 MPP_TIMEOUT = 23
TRAMPOLINE_FEE_INSUFFICIENT = NODE | 51
TRAMPOLINE_EXPIRY_TOO_SOON = NODE | 52
# don't use these elsewhere, the names are ambiguous without context # don't use these elsewhere, the names are ambiguous without context

119
electrum/lnpeer.py

@ -247,14 +247,17 @@ class Peer(Logger):
self.maybe_set_initialized() self.maybe_set_initialized()
def on_node_announcement(self, payload): def on_node_announcement(self, payload):
self.gossip_queue.put_nowait(('node_announcement', payload)) if self.lnworker.channel_db:
self.gossip_queue.put_nowait(('node_announcement', payload))
def on_channel_announcement(self, payload): def on_channel_announcement(self, payload):
self.gossip_queue.put_nowait(('channel_announcement', payload)) if self.lnworker.channel_db:
self.gossip_queue.put_nowait(('channel_announcement', payload))
def on_channel_update(self, payload): def on_channel_update(self, payload):
self.maybe_save_remote_update(payload) self.maybe_save_remote_update(payload)
self.gossip_queue.put_nowait(('channel_update', payload)) if self.lnworker.channel_db:
self.gossip_queue.put_nowait(('channel_update', payload))
def maybe_save_remote_update(self, payload): def maybe_save_remote_update(self, payload):
if not self.channels: if not self.channels:
@ -312,6 +315,8 @@ class Peer(Logger):
async def process_gossip(self): async def process_gossip(self):
while True: while True:
await asyncio.sleep(5) await asyncio.sleep(5)
if not self.network.lngossip:
continue
chan_anns = [] chan_anns = []
chan_upds = [] chan_upds = []
node_anns = [] node_anns = []
@ -330,7 +335,8 @@ class Peer(Logger):
# verify in peer's TaskGroup so that we fail the connection # verify in peer's TaskGroup so that we fail the connection
self.verify_channel_announcements(chan_anns) self.verify_channel_announcements(chan_anns)
self.verify_node_announcements(node_anns) self.verify_node_announcements(node_anns)
await self.network.lngossip.process_gossip(chan_anns, node_anns, chan_upds) if self.network.lngossip:
await self.network.lngossip.process_gossip(chan_anns, node_anns, chan_upds)
def verify_channel_announcements(self, chan_anns): def verify_channel_announcements(self, chan_anns):
for payload in chan_anns: for payload in chan_anns:
@ -579,6 +585,9 @@ class Peer(Logger):
""" """
# will raise if init fails # will raise if init fails
await asyncio.wait_for(self.initialized, LN_P2P_NETWORK_TIMEOUT) await asyncio.wait_for(self.initialized, LN_P2P_NETWORK_TIMEOUT)
# trampoline is not yet in features
if not self.lnworker.channel_db and not self.lnworker.is_trampoline_peer(self.pubkey):
raise Exception(_('Not a trampoline node') + str(self.their_features))
feerate = self.lnworker.current_feerate_per_kw() feerate = self.lnworker.current_feerate_per_kw()
local_config = self.make_local_config(funding_sat, push_msat, LOCAL) local_config = self.make_local_config(funding_sat, push_msat, LOCAL)
@ -1195,21 +1204,68 @@ class Peer(Logger):
# add features learned during "init" for direct neighbour: # add features learned during "init" for direct neighbour:
route[0].node_features |= self.features route[0].node_features |= self.features
local_height = self.network.get_local_height() local_height = self.network.get_local_height()
# create onion packet
final_cltv = local_height + min_final_cltv_expiry final_cltv = local_height + min_final_cltv_expiry
hops_data, amount_msat, cltv = calc_hops_data_for_payment(route, amount_msat, final_cltv, hops_data, amount_msat, cltv = calc_hops_data_for_payment(route, amount_msat, final_cltv,
payment_secret=payment_secret) payment_secret=payment_secret)
self.logger.info(f"lnpeer.pay len(route)={len(route)}")
for i in range(len(route)):
self.logger.info(f" {i}: edge={route[i].short_channel_id} hop_data={hops_data[i]!r}")
assert final_cltv <= cltv, (final_cltv, cltv) assert final_cltv <= cltv, (final_cltv, cltv)
secret_key = os.urandom(32) session_key = os.urandom(32) # session_key
onion = new_onion_packet([x.node_id for x in route], secret_key, hops_data, associated_data=payment_hash) # detect trampoline hops
payment_path_pubkeys = [x.node_id for x in route]
num_hops = len(payment_path_pubkeys)
for i in range(num_hops-1):
route_edge = route[i]
next_edge = route[i+1]
if route_edge.is_trampoline():
assert next_edge.is_trampoline()
self.logger.info(f'trampoline hop at position {i}')
hops_data[i].payload["outgoing_node_id"] = {"outgoing_node_id":next_edge.node_id}
if route_edge.invoice_features:
hops_data[i].payload["invoice_features"] = {"invoice_features":route_edge.invoice_features}
if route_edge.invoice_routing_info:
hops_data[i].payload["invoice_routing_info"] = {"invoice_routing_info":route_edge.invoice_routing_info}
# create trampoline onion
for i in range(num_hops):
route_edge = route[i]
if route_edge.is_trampoline():
self.logger.info(f'first trampoline hop at position {i}')
self.logger.info(f'inner onion: {hops_data[i:]}')
trampoline_session_key = os.urandom(32)
trampoline_onion = new_onion_packet(payment_path_pubkeys[i:], trampoline_session_key, hops_data[i:], associated_data=payment_hash, trampoline=True)
# drop hop_data
payment_path_pubkeys = payment_path_pubkeys[:i]
hops_data = hops_data[:i]
# we must generate a different secret for the outer onion
outer_payment_secret = os.urandom(32)
# trampoline_payload is a final payload
trampoline_payload = hops_data[i-1].payload
p = trampoline_payload.pop('short_channel_id')
amt_to_forward = trampoline_payload["amt_to_forward"]["amt_to_forward"]
trampoline_payload["payment_data"] = {
"payment_secret":outer_payment_secret,
"total_msat": amt_to_forward
}
trampoline_payload["trampoline_onion_packet"] = {
"version": trampoline_onion.version,
"public_key": trampoline_onion.public_key,
"hops_data": trampoline_onion.hops_data,
"hmac": trampoline_onion.hmac
}
break
# create onion packet
onion = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data=payment_hash) # must use another sessionkey
self.logger.info(f"starting payment. len(route)={len(hops_data)}.")
# create htlc # create htlc
if cltv > local_height + lnutil.NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE: if cltv > local_height + lnutil.NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE:
raise PaymentFailure(f"htlc expiry too far into future. (in {cltv-local_height} blocks)") raise PaymentFailure(f"htlc expiry too far into future. (in {cltv-local_height} blocks)")
htlc = UpdateAddHtlc(amount_msat=amount_msat, payment_hash=payment_hash, cltv_expiry=cltv, timestamp=int(time.time())) htlc = UpdateAddHtlc(amount_msat=amount_msat, payment_hash=payment_hash, cltv_expiry=cltv, timestamp=int(time.time()))
htlc = chan.add_htlc(htlc) htlc = chan.add_htlc(htlc)
chan.set_onion_key(htlc.htlc_id, secret_key) chan.set_onion_key(htlc.htlc_id, session_key) # should it be the outer onion secret?
self.logger.info(f"starting payment. len(route)={len(route)}. route: {route}. " self.logger.info(f"starting payment. htlc: {htlc}")
f"htlc: {htlc}. hops_data={hops_data!r}")
self.send_message( self.send_message(
"update_add_htlc", "update_add_htlc",
channel_id=chan.channel_id, channel_id=chan.channel_id,
@ -1372,20 +1428,9 @@ class Peer(Logger):
self, *, self, *,
chan: Channel, chan: Channel,
htlc: UpdateAddHtlc, htlc: UpdateAddHtlc,
processed_onion: ProcessedOnionPacket) -> Tuple[Optional[bytes], Optional[OnionRoutingFailure]]: processed_onion: ProcessedOnionPacket,
is_trampoline:bool = False) -> Optional[bytes]:
info = self.lnworker.get_payment_info(htlc.payment_hash)
if info is None:
raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
preimage = self.lnworker.get_preimage(htlc.payment_hash)
try:
payment_secret_from_onion = processed_onion.hop_data.payload["payment_data"]["payment_secret"]
except:
pass # skip
else:
if payment_secret_from_onion != derive_payment_secret_from_payment_preimage(preimage):
raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
expected_received_msat = info.amount_msat
# Check that our blockchain tip is sufficiently recent so that we have an approx idea of the height. # Check that our blockchain tip is sufficiently recent so that we have an approx idea of the height.
# We should not release the preimage for an HTLC that its sender could already time out as # We should not release the preimage for an HTLC that its sender could already time out as
# then they might try to force-close and it becomes a race. # then they might try to force-close and it becomes a race.
@ -1412,12 +1457,38 @@ class Peer(Logger):
except: except:
total_msat = amt_to_forward # fall back to "amt_to_forward" total_msat = amt_to_forward # fall back to "amt_to_forward"
if amt_to_forward != htlc.amount_msat: if not is_trampoline and amt_to_forward != htlc.amount_msat:
raise OnionRoutingFailure( raise OnionRoutingFailure(
code=OnionFailureCode.FINAL_INCORRECT_HTLC_AMOUNT, code=OnionFailureCode.FINAL_INCORRECT_HTLC_AMOUNT,
data=total_msat.to_bytes(8, byteorder="big")) data=total_msat.to_bytes(8, byteorder="big"))
# if there is a trampoline_onion, perform the above checks on it
if processed_onion.trampoline_onion_packet:
trampoline_onion = process_onion_packet(
processed_onion.trampoline_onion_packet,
associated_data=htlc.payment_hash,
our_onion_private_key=self.privkey)
return self.maybe_fulfill_htlc(
chan=chan,
htlc=htlc,
processed_onion=trampoline_onion,
is_trampoline=True)
info = self.lnworker.get_payment_info(htlc.payment_hash)
if info is None:
raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
preimage = self.lnworker.get_preimage(htlc.payment_hash)
try:
payment_secret_from_onion = processed_onion.hop_data.payload["payment_data"]["payment_secret"]
except:
pass # skip
else:
if payment_secret_from_onion != derive_payment_secret_from_payment_preimage(preimage):
raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
expected_received_msat = info.amount_msat
if expected_received_msat is None: if expected_received_msat is None:
return preimage return preimage
if not (expected_received_msat <= total_msat <= 2 * expected_received_msat): if not (expected_received_msat <= total_msat <= 2 * expected_received_msat):
raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'') raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
accepted, expired = self.lnworker.htlc_received(chan.short_channel_id, htlc, expected_received_msat) accepted, expired = self.lnworker.htlc_received(chan.short_channel_id, htlc, expected_received_msat)

10
electrum/lnrouter.py

@ -99,6 +99,16 @@ class RouteEdge(PathEdge):
features = self.node_features features = self.node_features
return bool(features & LnFeatures.VAR_ONION_REQ or features & LnFeatures.VAR_ONION_OPT) return bool(features & LnFeatures.VAR_ONION_REQ or features & LnFeatures.VAR_ONION_OPT)
def is_trampoline(self):
return False
@attr.s
class TrampolineEdge(RouteEdge):
invoice_routing_info = attr.ib(type=bytes, default=None)
invoice_features = attr.ib(type=int, default=None)
short_channel_id = attr.ib(0)
def is_trampoline(self):
return True
LNPaymentPath = Sequence[PathEdge] LNPaymentPath = Sequence[PathEdge]
LNPaymentRoute = Sequence[RouteEdge] LNPaymentRoute = Sequence[RouteEdge]

10
electrum/lnutil.py

@ -949,6 +949,15 @@ class LnFeatures(IntFlag):
_ln_feature_contexts[OPTION_SUPPORT_LARGE_CHANNEL_OPT] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.CHAN_ANN_ALWAYS_EVEN) _ln_feature_contexts[OPTION_SUPPORT_LARGE_CHANNEL_OPT] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.CHAN_ANN_ALWAYS_EVEN)
_ln_feature_contexts[OPTION_SUPPORT_LARGE_CHANNEL_REQ] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.CHAN_ANN_ALWAYS_EVEN) _ln_feature_contexts[OPTION_SUPPORT_LARGE_CHANNEL_REQ] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.CHAN_ANN_ALWAYS_EVEN)
OPTION_TRAMPOLINE_ROUTING_REQ = 1 << 50
OPTION_TRAMPOLINE_ROUTING_OPT = 1 << 51
# We do not set trampoline_routing_opt in invoices, because the spec is not ready.
# This ensures that current version of Phoenix can pay us
# It also prevents Electrum from using t_tags from future implementations
_ln_feature_contexts[OPTION_TRAMPOLINE_ROUTING_REQ] = (LNFC.INIT | LNFC.NODE_ANN) # | LNFC.INVOICE)
_ln_feature_contexts[OPTION_TRAMPOLINE_ROUTING_OPT] = (LNFC.INIT | LNFC.NODE_ANN) # | LNFC.INVOICE)
def validate_transitive_dependencies(self) -> bool: def validate_transitive_dependencies(self) -> bool:
# for all even bit set, set corresponding odd bit: # for all even bit set, set corresponding odd bit:
features = self # copy features = self # copy
@ -1014,6 +1023,7 @@ LN_FEATURES_IMPLEMENTED = (
| LnFeatures.OPTION_STATIC_REMOTEKEY_OPT | LnFeatures.OPTION_STATIC_REMOTEKEY_REQ | LnFeatures.OPTION_STATIC_REMOTEKEY_OPT | LnFeatures.OPTION_STATIC_REMOTEKEY_REQ
| LnFeatures.VAR_ONION_OPT | LnFeatures.VAR_ONION_REQ | LnFeatures.VAR_ONION_OPT | LnFeatures.VAR_ONION_REQ
| LnFeatures.PAYMENT_SECRET_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.PAYMENT_SECRET_OPT | LnFeatures.PAYMENT_SECRET_REQ
| LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT | LnFeatures.OPTION_TRAMPOLINE_ROUTING_REQ
) )

11
electrum/lnwire/onion_wire.csv

@ -7,6 +7,17 @@ tlvdata,tlv_payload,short_channel_id,short_channel_id,short_channel_id,
tlvtype,tlv_payload,payment_data,8 tlvtype,tlv_payload,payment_data,8
tlvdata,tlv_payload,payment_data,payment_secret,byte,32 tlvdata,tlv_payload,payment_data,payment_secret,byte,32
tlvdata,tlv_payload,payment_data,total_msat,tu64, tlvdata,tlv_payload,payment_data,total_msat,tu64,
tlvtype,tlv_payload,invoice_features,66097
tlvdata,tlv_payload,invoice_features,invoice_features,u64,
tlvtype,tlv_payload,outgoing_node_id,66098
tlvdata,tlv_payload,outgoing_node_id,outgoing_node_id,byte,33
tlvtype,tlv_payload,invoice_routing_info,66099
tlvdata,tlv_payload,invoice_routing_info,invoice_routing_info,byte,...
tlvtype,tlv_payload,trampoline_onion_packet,66100
tlvdata,tlv_payload,trampoline_onion_packet,version,byte,1
tlvdata,tlv_payload,trampoline_onion_packet,public_key,byte,33
tlvdata,tlv_payload,trampoline_onion_packet,hops_data,byte,400
tlvdata,tlv_payload,trampoline_onion_packet,hmac,byte,32
msgtype,invalid_realm,PERM|1 msgtype,invalid_realm,PERM|1
msgtype,temporary_node_failure,NODE|2 msgtype,temporary_node_failure,NODE|2
msgtype,permanent_node_failure,PERM|NODE|2 msgtype,permanent_node_failure,PERM|NODE|2

1 tlvtype,tlv_payload,amt_to_forward,2
7 tlvtype,tlv_payload,payment_data,8
8 tlvdata,tlv_payload,payment_data,payment_secret,byte,32
9 tlvdata,tlv_payload,payment_data,total_msat,tu64,
10 tlvtype,tlv_payload,invoice_features,66097
11 tlvdata,tlv_payload,invoice_features,invoice_features,u64,
12 tlvtype,tlv_payload,outgoing_node_id,66098
13 tlvdata,tlv_payload,outgoing_node_id,outgoing_node_id,byte,33
14 tlvtype,tlv_payload,invoice_routing_info,66099
15 tlvdata,tlv_payload,invoice_routing_info,invoice_routing_info,byte,...
16 tlvtype,tlv_payload,trampoline_onion_packet,66100
17 tlvdata,tlv_payload,trampoline_onion_packet,version,byte,1
18 tlvdata,tlv_payload,trampoline_onion_packet,public_key,byte,33
19 tlvdata,tlv_payload,trampoline_onion_packet,hops_data,byte,400
20 tlvdata,tlv_payload,trampoline_onion_packet,hmac,byte,32
21 msgtype,invalid_realm,PERM|1
22 msgtype,temporary_node_failure,NODE|2
23 msgtype,permanent_node_failure,PERM|NODE|2

290
electrum/lnworker.py

@ -58,6 +58,7 @@ from .lnutil import (Outpoint, LNPeerAddr,
UpdateAddHtlc, Direction, LnFeatures, ShortChannelID, UpdateAddHtlc, Direction, LnFeatures, ShortChannelID,
HtlcLog, derive_payment_secret_from_payment_preimage) HtlcLog, derive_payment_secret_from_payment_preimage)
from .lnutil import ln_dummy_address, ln_compare_features, IncompatibleLightningFeatures from .lnutil import ln_dummy_address, ln_compare_features, IncompatibleLightningFeatures
from .lnrouter import TrampolineEdge
from .transaction import PartialTxOutput, PartialTransaction, PartialTxInput from .transaction import PartialTxOutput, PartialTransaction, PartialTxInput
from .lnonion import OnionFailureCode, process_onion_packet, OnionPacket, OnionRoutingFailure from .lnonion import OnionFailureCode, process_onion_packet, OnionPacket, OnionRoutingFailure
from .lnmsg import decode_msg from .lnmsg import decode_msg
@ -73,6 +74,7 @@ from .lnchannel import ChannelBackup
from .channel_db import UpdateStatus from .channel_db import UpdateStatus
from .channel_db import get_mychannel_info, get_mychannel_policy from .channel_db import get_mychannel_info, get_mychannel_policy
from .submarine_swaps import SwapManager from .submarine_swaps import SwapManager
from .channel_db import ChannelInfo, Policy
if TYPE_CHECKING: if TYPE_CHECKING:
from .network import Network from .network import Network
@ -136,6 +138,60 @@ FALLBACK_NODE_LIST_MAINNET = [
] ]
# hardcoded list
TRAMPOLINE_NODES_MAINNET = {
'ACINQ': LNPeerAddr(host='34.239.230.56', port=9735, pubkey=bfh('03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f')),
'Electrum trampoline': LNPeerAddr(host='144.76.99.209', port=9740, pubkey=bfh('03ecef675be448b615e6176424070673ef8284e0fd19d8be062a6cb5b130a0a0d1')),
}
def hardcoded_trampoline_nodes():
return TRAMPOLINE_NODES_MAINNET if constants.net in (constants.BitcoinMainnet, ) else {}
def trampolines_by_id():
return dict([(x.pubkey, x) for x in hardcoded_trampoline_nodes().values()])
is_hardcoded_trampoline = lambda node_id: node_id in trampolines_by_id().keys()
# trampoline nodes are supposed to advertise their fee and cltv in node_update message
TRAMPOLINE_FEES = [
{
'fee_base_msat': 0,
'fee_proportional_millionths': 0,
'cltv_expiry_delta': 576,
},
{
'fee_base_msat': 1000,
'fee_proportional_millionths': 100,
'cltv_expiry_delta': 576,
},
{
'fee_base_msat': 3000,
'fee_proportional_millionths': 100,
'cltv_expiry_delta': 576,
},
{
'fee_base_msat': 5000,
'fee_proportional_millionths': 500,
'cltv_expiry_delta': 576,
},
{
'fee_base_msat': 7000,
'fee_proportional_millionths': 1000,
'cltv_expiry_delta': 576,
},
{
'fee_base_msat': 12000,
'fee_proportional_millionths': 3000,
'cltv_expiry_delta': 576,
},
{
'fee_base_msat': 100000,
'fee_proportional_millionths': 3000,
'cltv_expiry_delta': 576,
},
]
class PaymentInfo(NamedTuple): class PaymentInfo(NamedTuple):
payment_hash: bytes payment_hash: bytes
amount_msat: Optional[int] amount_msat: Optional[int]
@ -165,7 +221,8 @@ LNWALLET_FEATURES = BASE_FEATURES\
| LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ\ | LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ\
| LnFeatures.OPTION_STATIC_REMOTEKEY_REQ\ | LnFeatures.OPTION_STATIC_REMOTEKEY_REQ\
| LnFeatures.GOSSIP_QUERIES_REQ\ | LnFeatures.GOSSIP_QUERIES_REQ\
| LnFeatures.BASIC_MPP_OPT | LnFeatures.BASIC_MPP_OPT\
| LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT
LNGOSSIP_FEATURES = BASE_FEATURES\ LNGOSSIP_FEATURES = BASE_FEATURES\
| LnFeatures.GOSSIP_QUERIES_OPT\ | LnFeatures.GOSSIP_QUERIES_OPT\
@ -191,10 +248,13 @@ class LNWorker(Logger, NetworkRetryManager[LNPeerAddr]):
self.features = features self.features = features
self.network = None # type: Optional[Network] self.network = None # type: Optional[Network]
self.config = None # type: Optional[SimpleConfig] self.config = None # type: Optional[SimpleConfig]
self.channel_db = None # type: Optional[ChannelDB]
util.register_callback(self.on_proxy_changed, ['proxy_set']) util.register_callback(self.on_proxy_changed, ['proxy_set'])
@property
def channel_db(self):
return self.network.channel_db if self.network else None
@property @property
def peers(self) -> Mapping[bytes, Peer]: def peers(self) -> Mapping[bytes, Peer]:
"""Returns a read-only copy of peers.""" """Returns a read-only copy of peers."""
@ -209,7 +269,12 @@ class LNWorker(Logger, NetworkRetryManager[LNPeerAddr]):
node_info = self.channel_db.get_node_info_for_node_id(node_id) node_info = self.channel_db.get_node_info_for_node_id(node_id)
node_alias = (node_info.alias if node_info else '') or node_id.hex() node_alias = (node_info.alias if node_info else '') or node_id.hex()
else: else:
node_alias = '' for k, v in hardcoded_trampoline_nodes().items():
if v.pubkey == node_id:
node_alias = k
break
else:
node_alias = 'unknown'
return node_alias return node_alias
async def maybe_listen(self): async def maybe_listen(self):
@ -294,7 +359,6 @@ class LNWorker(Logger, NetworkRetryManager[LNPeerAddr]):
assert network assert network
self.network = network self.network = network
self.config = network.config self.config = network.config
self.channel_db = self.network.channel_db
self._add_peers_from_config() self._add_peers_from_config()
asyncio.run_coroutine_threadsafe(self.main_loop(), self.network.asyncio_loop) asyncio.run_coroutine_threadsafe(self.main_loop(), self.network.asyncio_loop)
@ -451,10 +515,16 @@ class LNWorker(Logger, NetworkRetryManager[LNPeerAddr]):
if rest is not None: if rest is not None:
host, port = split_host_port(rest) host, port = split_host_port(rest)
else: else:
addrs = self.channel_db.get_node_addresses(node_id) if not self.channel_db:
if not addrs: addr = trampolines_by_id().get(node_id)
raise ConnStringFormatError(_('Don\'t know any addresses for node:') + ' ' + bh2u(node_id)) if not addr:
host, port, timestamp = self.choose_preferred_address(list(addrs)) raise ConnStringFormatError(_('Address unknown for node:') + ' ' + bh2u(node_id))
host, port = addr.host, addr.port
else:
addrs = self.channel_db.get_node_addresses(node_id)
if not addrs:
raise ConnStringFormatError(_('Don\'t know any addresses for node:') + ' ' + bh2u(node_id))
host, port, timestamp = self.choose_preferred_address(list(addrs))
port = int(port) port = int(port)
# Try DNS-resolving the host (if needed). This is simply so that # Try DNS-resolving the host (if needed). This is simply so that
# the caller gets a nice exception if it cannot be resolved. # the caller gets a nice exception if it cannot be resolved.
@ -648,7 +718,6 @@ class LNWallet(LNWorker):
assert network assert network
self.network = network self.network = network
self.config = network.config self.config = network.config
self.channel_db = self.network.channel_db
self.lnwatcher = LNWalletWatcher(self, network) self.lnwatcher = LNWalletWatcher(self, network)
self.lnwatcher.start_network(network) self.lnwatcher.start_network(network)
self.swap_manager.start_network(network=network, lnwatcher=self.lnwatcher) self.swap_manager.start_network(network=network, lnwatcher=self.lnwatcher)
@ -923,10 +992,6 @@ class LNWallet(LNWorker):
chan, funding_tx = fut.result() chan, funding_tx = fut.result()
except concurrent.futures.TimeoutError: except concurrent.futures.TimeoutError:
raise Exception(_("open_channel timed out")) raise Exception(_("open_channel timed out"))
# at this point the channel opening was successful
# if this is the first channel that got opened, we start gossiping
if self.channels:
self.network.start_gossip()
return chan, funding_tx return chan, funding_tx
def get_channel_by_short_id(self, short_channel_id: bytes) -> Optional[Channel]: def get_channel_by_short_id(self, short_channel_id: bytes) -> Optional[Channel]:
@ -958,6 +1023,7 @@ class LNWallet(LNWorker):
invoice_pubkey = lnaddr.pubkey.serialize() invoice_pubkey = lnaddr.pubkey.serialize()
invoice_features = lnaddr.get_tag('9') or 0 invoice_features = lnaddr.get_tag('9') or 0
r_tags = lnaddr.get_routing_info('r') r_tags = lnaddr.get_routing_info('r')
t_tags = lnaddr.get_routing_info('t')
amount_to_pay = lnaddr.get_amount_msat() amount_to_pay = lnaddr.get_amount_msat()
status = self.get_payment_status(payment_hash) status = self.get_payment_status(payment_hash)
if status == PR_PAID: if status == PR_PAID:
@ -967,12 +1033,18 @@ class LNWallet(LNWorker):
info = PaymentInfo(payment_hash, amount_to_pay, SENT, PR_UNPAID) info = PaymentInfo(payment_hash, amount_to_pay, SENT, PR_UNPAID)
self.save_payment_info(info) self.save_payment_info(info)
self.wallet.set_label(key, lnaddr.get_description()) self.wallet.set_label(key, lnaddr.get_description())
if self.channel_db is None:
self.trampoline_fee_level = 0
self.trampoline2_list = list(trampolines_by_id().keys())
random.shuffle(self.trampoline2_list)
self.set_invoice_status(key, PR_INFLIGHT) self.set_invoice_status(key, PR_INFLIGHT)
util.trigger_callback('invoice_status', self.wallet, key) util.trigger_callback('invoice_status', self.wallet, key)
try: try:
await self.pay_to_node( await self.pay_to_node(
invoice_pubkey, payment_hash, payment_secret, amount_to_pay, invoice_pubkey, payment_hash, payment_secret, amount_to_pay,
min_cltv_expiry, r_tags, invoice_features, min_cltv_expiry, r_tags, t_tags, invoice_features,
attempts=attempts, full_path=full_path) attempts=attempts, full_path=full_path)
success = True success = True
except PaymentFailure as e: except PaymentFailure as e:
@ -991,7 +1063,7 @@ class LNWallet(LNWorker):
async def pay_to_node( async def pay_to_node(
self, node_pubkey, payment_hash, payment_secret, amount_to_pay, self, node_pubkey, payment_hash, payment_secret, amount_to_pay,
min_cltv_expiry, r_tags, invoice_features, *, attempts: int = 1, min_cltv_expiry, r_tags, t_tags, invoice_features, *, attempts: int = 1,
full_path: LNPaymentPath = None): full_path: LNPaymentPath = None):
self.logs[payment_hash.hex()] = log = [] self.logs[payment_hash.hex()] = log = []
@ -1002,9 +1074,14 @@ class LNWallet(LNWorker):
# 1. create a set of routes for remaining amount. # 1. create a set of routes for remaining amount.
# note: path-finding runs in a separate thread so that we don't block the asyncio loop # note: path-finding runs in a separate thread so that we don't block the asyncio loop
# graph updates might occur during the computation # graph updates might occur during the computation
routes = await run_in_thread(partial( if self.channel_db:
self.create_routes_for_payment, amount_to_send, node_pubkey, routes = await run_in_thread(partial(
min_cltv_expiry, r_tags, invoice_features, full_path=full_path)) self.create_routes_for_payment, amount_to_send, node_pubkey,
min_cltv_expiry, r_tags, invoice_features, full_path=full_path))
else:
route = await self.create_trampoline_route(
amount_to_send, node_pubkey, invoice_features, r_tags, t_tags)
routes = [(route, amount_to_send)]
# 2. send htlcs # 2. send htlcs
for route, amount_msat in routes: for route, amount_msat in routes:
await self.pay_to_route(route, amount_msat, payment_hash, payment_secret, min_cltv_expiry) await self.pay_to_route(route, amount_msat, payment_hash, payment_secret, min_cltv_expiry)
@ -1049,6 +1126,21 @@ class LNWallet(LNWorker):
code, data = failure_msg.code, failure_msg.data code, data = failure_msg.code, failure_msg.data
self.logger.info(f"UPDATE_FAIL_HTLC {repr(code)} {data}") self.logger.info(f"UPDATE_FAIL_HTLC {repr(code)} {data}")
self.logger.info(f"error reported by {bh2u(route[sender_idx].node_id)}") self.logger.info(f"error reported by {bh2u(route[sender_idx].node_id)}")
if code == OnionFailureCode.MPP_TIMEOUT:
raise PaymentFailure(failure_msg.code_name())
# trampoline
if self.channel_db is None:
if code == OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT:
# todo: parse the node parameters here (not returned by eclair yet)
self.trampoline_fee_level += 1
return
elif len(route) > 2:
edge = route[2]
if edge.is_trampoline() and edge.node_id in self.trampoline2_list:
self.logger.info(f"blacklisting second trampoline {edge.node_id.hex()}")
self.trampoline2_list.remove(edge.node_id)
return
raise PaymentFailure(failure_msg.code_name())
# handle some specific error codes # handle some specific error codes
failure_codes = { failure_codes = {
OnionFailureCode.TEMPORARY_CHANNEL_FAILURE: 0, OnionFailureCode.TEMPORARY_CHANNEL_FAILURE: 0,
@ -1113,7 +1205,6 @@ class LNWallet(LNWorker):
if not (blacklist or update): if not (blacklist or update):
raise PaymentFailure(htlc_log.failure_msg.code_name()) raise PaymentFailure(htlc_log.failure_msg.code_name())
@classmethod @classmethod
def _decode_channel_update_msg(cls, chan_upd_msg: bytes) -> Optional[Dict[str, Any]]: def _decode_channel_update_msg(cls, chan_upd_msg: bytes) -> Optional[Dict[str, Any]]:
channel_update_as_received = chan_upd_msg channel_update_as_received = chan_upd_msg
@ -1152,6 +1243,131 @@ class LNWallet(LNWorker):
f"min_final_cltv_expiry: {addr.get_min_final_cltv_expiry()}")) f"min_final_cltv_expiry: {addr.get_min_final_cltv_expiry()}"))
return addr return addr
def encode_routing_info(self, r_tags):
import bitstring
result = bitstring.BitArray()
for route in r_tags:
result.append(bitstring.pack('uint:8', len(route)))
for step in route:
pubkey, channel, feebase, feerate, cltv = step
result.append(bitstring.BitArray(pubkey) + bitstring.BitArray(channel) + bitstring.pack('intbe:32', feebase) + bitstring.pack('intbe:32', feerate) + bitstring.pack('intbe:16', cltv))
return result.tobytes()
def is_trampoline_peer(self, node_id):
# until trampoline is advertised in lnfeatures, check against hardcoded list
if is_hardcoded_trampoline(node_id):
return True
peer = self._peers.get(node_id)
if peer and bool(peer.their_features & LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT):
return True
return False
def suggest_peer(self):
return self.lnrater.suggest_peer() if self.channel_db else random.choice(list(hardcoded_trampoline_nodes().values())).pubkey
@log_exceptions
async def create_trampoline_route(
self, amount_msat:int, invoice_pubkey:bytes, invoice_features:int,
r_tags, t_tags) -> LNPaymentRoute:
""" return the route that leads to trampoline, and the trampoline fake edge"""
# We do not set trampoline_routing_opt in our invoices, because the spec is not ready
# Do not use t_tags if the flag is set, because we the format is not decided yet
if invoice_features & LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT:
is_legacy = False
if len(r_tags) > 0 and len(r_tags[0]) == 1:
pubkey, scid, feebase, feerate, cltv = r_tags[0][0]
t_tag = pubkey, feebase, feerate, cltv
else:
t_tag = None
elif len(t_tags) > 0:
is_legacy = False
t_tag = t_tags[0]
else:
is_legacy = True
# Find a trampoline. We assume we have a direct channel to trampoline
for chan in list(self.channels.values()):
if not self.is_trampoline_peer(chan.node_id):
continue
if chan.is_active() and chan.can_pay(amount_msat, check_frozen=True):
trampoline_short_channel_id = chan.short_channel_id
trampoline_node_id = chan.node_id
break
else:
raise NoPathFound()
# use attempt number to decide fee and second trampoline
# we need a state with the list of nodes we have not tried
# add optional second trampoline
trampoline2 = None
if is_legacy:
for node_id in self.trampoline2_list:
if node_id != trampoline_node_id:
trampoline2 = node_id
break
# fee level. the same fee is used for all trampolines
if self.trampoline_fee_level < len(TRAMPOLINE_FEES):
params = TRAMPOLINE_FEES[self.trampoline_fee_level]
else:
raise NoPathFound()
self.logger.info(f'create route with trampoline: fee_level={self.trampoline_fee_level}, is legacy: {is_legacy}')
self.logger.info(f'first trampoline: {trampoline_node_id.hex()}')
self.logger.info(f'second trampoline: {trampoline2.hex() if trampoline2 else None}')
self.logger.info(f'params: {params}')
# node_features is only used to determine is_tlv
trampoline_features = LnFeatures.VAR_ONION_OPT
# hop to trampoline
route = [
RouteEdge(
node_id=trampoline_node_id,
short_channel_id=trampoline_short_channel_id,
fee_base_msat=0,
fee_proportional_millionths=0,
cltv_expiry_delta=0,
node_features=trampoline_features)
]
# trampoline hop
route.append(
TrampolineEdge(
node_id=trampoline_node_id,
fee_base_msat=params['fee_base_msat'],
fee_proportional_millionths=params['fee_proportional_millionths'],
cltv_expiry_delta=params['cltv_expiry_delta'],
node_features=trampoline_features))
if trampoline2:
route.append(
TrampolineEdge(
node_id=trampoline2,
fee_base_msat=params['fee_base_msat'],
fee_proportional_millionths=params['fee_proportional_millionths'],
cltv_expiry_delta=params['cltv_expiry_delta'],
node_features=trampoline_features))
# add routing info
if is_legacy:
invoice_routing_info = self.encode_routing_info(r_tags)
route[-1].invoice_routing_info = invoice_routing_info
route[-1].invoice_features = invoice_features
else:
if t_tag:
pubkey, feebase, feerate, cltv = t_tag
if route[-1].node_id != pubkey:
route.append(
TrampolineEdge(
node_id=pubkey,
fee_base_msat=feebase,
fee_proportional_millionths=feerate,
cltv_expiry_delta=cltv,
node_features=trampoline_features))
# Fake edge (not part of actual route, needed by calc_hops_data)
route.append(
TrampolineEdge(
node_id=invoice_pubkey,
fee_base_msat=0,
fee_proportional_millionths=0,
cltv_expiry_delta=0,
node_features=trampoline_features))
return route
@profiler @profiler
def create_routes_for_payment( def create_routes_for_payment(
self, self,
@ -1256,6 +1472,16 @@ class LNWallet(LNWorker):
if not routing_hints: if not routing_hints:
self.logger.info("Warning. No routing hints added to invoice. " self.logger.info("Warning. No routing hints added to invoice. "
"Other clients will likely not be able to send to us.") "Other clients will likely not be able to send to us.")
# if not all hints are trampoline, do not create trampoline invoice
invoice_features = self.features.for_invoice()
#
trampoline_hints = []
for r in routing_hints:
node_id, short_channel_id, fee_base_msat, fee_proportional_millionths, cltv_expiry_delta = r[1][0]
if len(r[1])== 1 and self.is_trampoline_peer(node_id):
trampoline_hints.append(('t', (node_id, fee_base_msat, fee_proportional_millionths, cltv_expiry_delta)))
payment_preimage = os.urandom(32) payment_preimage = os.urandom(32)
payment_hash = sha256(payment_preimage) payment_hash = sha256(payment_preimage)
info = PaymentInfo(payment_hash, amount_msat, RECEIVED, PR_UNPAID) info = PaymentInfo(payment_hash, amount_msat, RECEIVED, PR_UNPAID)
@ -1267,8 +1493,9 @@ class LNWallet(LNWorker):
tags=[('d', message), tags=[('d', message),
('c', MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE), ('c', MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE),
('x', expiry), ('x', expiry),
('9', self.features.for_invoice())] ('9', invoice_features)]
+ routing_hints, + routing_hints
+ trampoline_hints,
date=timestamp, date=timestamp,
payment_secret=derive_payment_secret_from_payment_preimage(payment_preimage)) payment_secret=derive_payment_secret_from_payment_preimage(payment_preimage))
invoice = lnencode(lnaddr, self.node_keypair.privkey) invoice = lnencode(lnaddr, self.node_keypair.privkey)
@ -1531,14 +1758,19 @@ class LNWallet(LNWorker):
async def reestablish_peer_for_given_channel(self, chan: Channel) -> None: async def reestablish_peer_for_given_channel(self, chan: Channel) -> None:
now = time.time() now = time.time()
peer_addresses = [] peer_addresses = []
# will try last good address first, from gossip if not self.channel_db:
last_good_addr = self.channel_db.get_last_good_address(chan.node_id) addr = trampolines_by_id().get(chan.node_id)
if last_good_addr: if addr:
peer_addresses.append(last_good_addr) peer_addresses.append(addr)
# will try addresses for node_id from gossip else:
addrs_from_gossip = self.channel_db.get_node_addresses(chan.node_id) or [] # will try last good address first, from gossip
for host, port, ts in addrs_from_gossip: last_good_addr = self.channel_db.get_last_good_address(chan.node_id)
peer_addresses.append(LNPeerAddr(host, port, chan.node_id)) if last_good_addr:
peer_addresses.append(last_good_addr)
# will try addresses for node_id from gossip
addrs_from_gossip = self.channel_db.get_node_addresses(chan.node_id) or []
for host, port, ts in addrs_from_gossip:
peer_addresses.append(LNPeerAddr(host, port, chan.node_id))
# will try addresses stored in channel storage # will try addresses stored in channel storage
peer_addresses += list(chan.get_peer_addresses()) peer_addresses += list(chan.get_peer_addresses())
# Done gathering addresses. # Done gathering addresses.

21
electrum/network.py

@ -359,22 +359,25 @@ class Network(Logger, NetworkRetryManager[ServerAddr]):
def has_channel_db(self): def has_channel_db(self):
return self.channel_db is not None return self.channel_db is not None
def init_channel_db(self): def start_gossip(self):
if self.channel_db is None: from . import lnrouter
from . import lnrouter from . import channel_db
from . import channel_db from . import lnworker
if not self.config.get('use_gossip'):
return
if self.lngossip is None:
self.channel_db = channel_db.ChannelDB(self) self.channel_db = channel_db.ChannelDB(self)
self.path_finder = lnrouter.LNPathFinder(self.channel_db) self.path_finder = lnrouter.LNPathFinder(self.channel_db)
self.channel_db.load_data() self.channel_db.load_data()
def start_gossip(self):
if self.lngossip is None:
from . import lnworker
self.lngossip = lnworker.LNGossip() self.lngossip = lnworker.LNGossip()
self.lngossip.start_network(self) self.lngossip.start_network(self)
def stop_gossip(self): def stop_gossip(self):
self.lngossip.stop() if self.lngossip:
self.lngossip.stop()
self.lngossip = None
self.channel_db.stop()
self.channel_db = None
def run_from_another_thread(self, coro, *, timeout=None): def run_from_another_thread(self, coro, *, timeout=None):
assert self._loop_thread != threading.current_thread(), 'must not be called from network thread' assert self._loop_thread != threading.current_thread(), 'must not be called from network thread'

6
electrum/sql_db.py

@ -24,6 +24,7 @@ class SqlDB(Logger):
def __init__(self, asyncio_loop: asyncio.BaseEventLoop, path, commit_interval=None): def __init__(self, asyncio_loop: asyncio.BaseEventLoop, path, commit_interval=None):
Logger.__init__(self) Logger.__init__(self)
self.asyncio_loop = asyncio_loop self.asyncio_loop = asyncio_loop
self.stopping = False
self.path = path self.path = path
test_read_write_permissions(path) test_read_write_permissions(path)
self.commit_interval = commit_interval self.commit_interval = commit_interval
@ -31,6 +32,9 @@ class SqlDB(Logger):
self.sql_thread = threading.Thread(target=self.run_sql) self.sql_thread = threading.Thread(target=self.run_sql)
self.sql_thread.start() self.sql_thread.start()
def stop(self):
self.stopping = True
def filesize(self): def filesize(self):
return os.stat(self.path).st_size return os.stat(self.path).st_size
@ -40,7 +44,7 @@ class SqlDB(Logger):
self.logger.info("Creating database") self.logger.info("Creating database")
self.create_database() self.create_database()
i = 0 i = 0
while self.asyncio_loop.is_running(): while not self.stopping and self.asyncio_loop.is_running():
try: try:
future, func, args, kwargs = self.db_requests.get(timeout=0.1) future, func, args, kwargs = self.db_requests.get(timeout=0.1)
except queue.Empty: except queue.Empty:

1
electrum/tests/regtest/regtest.sh

@ -77,6 +77,7 @@ if [[ $1 == "init" ]]; then
agent="./run_electrum --regtest -D /tmp/$2" agent="./run_electrum --regtest -D /tmp/$2"
$agent create --offline > /dev/null $agent create --offline > /dev/null
$agent setconfig --offline log_to_file True $agent setconfig --offline log_to_file True
$agent setconfig --offline use_gossip True
$agent setconfig --offline server 127.0.0.1:51001:t $agent setconfig --offline server 127.0.0.1:51001:t
$agent setconfig --offline lightning_to_self_delay 144 $agent setconfig --offline lightning_to_self_delay 144
# alice is funded, bob is listening # alice is funded, bob is listening

Loading…
Cancel
Save