commit 029ec5a5ab4546f8ecd03119cde567208952edd0
parent 09c3e52e62d1bcc1ed46c7b48ecda2105c7ad50c
Author: SomberNight <somber.night@protonmail.com>
Date: Mon, 8 Oct 2018 20:31:15 +0200
make our channels private, and put routing hints in invoices we create
Diffstat:
5 files changed, 85 insertions(+), 26 deletions(-)
diff --git a/electrum/lnbase.py b/electrum/lnbase.py
@@ -12,6 +12,7 @@ import time
import hashlib
import hmac
from functools import partial
+from typing import List
import cryptography.hazmat.primitives.ciphers.aead as AEAD
import aiorpcx
@@ -31,6 +32,7 @@ from .lnutil import (Outpoint, ChannelConfig, LocalState,
funding_output_script, get_ecdh, get_per_commitment_secret_from_seed,
secret_to_pubkey, LNPeerAddr, PaymentFailure,
LOCAL, REMOTE, HTLCOwner, generate_keypair, LnKeyFamily)
+from .lnrouter import NotFoundChanAnnouncementForUpdate, RouteEdge
def channel_id_from_funding_tx(funding_txid, funding_index):
@@ -443,7 +445,16 @@ class Peer(PrintError):
pass
def on_channel_update(self, payload):
- self.channel_db.on_channel_update(payload)
+ try:
+ self.channel_db.on_channel_update(payload)
+ except NotFoundChanAnnouncementForUpdate:
+ # If it's for a direct channel with this peer, save it in chan.
+ # Note that this is prone to a race.. we might not have a short_channel_id
+ # associated with the channel in some cases
+ short_channel_id = payload['short_channel_id']
+ for chan in self.channels.values():
+ if chan.short_channel_id_predicted == short_channel_id:
+ chan.pending_channel_update_message = payload
def on_channel_announcement(self, payload):
self.channel_db.on_channel_announcement(payload)
@@ -550,7 +561,7 @@ class Peer(PrintError):
first_per_commitment_point=per_commitment_point_first,
to_self_delay=local_config.to_self_delay,
max_htlc_value_in_flight_msat=local_config.max_htlc_value_in_flight_msat,
- channel_flags=0x01, # publicly announcing channel
+ channel_flags=0x00, # not willing to announce channel
channel_reserve_satoshis=546
)
self.send_message(msg)
@@ -833,6 +844,9 @@ class Peer(PrintError):
Runs on the Network thread.
"""
if not chan.local_state.was_announced and funding_tx_depth >= 6:
+ # don't announce our channels
+ # FIXME should this be a field in chan.local_state maybe?
+ return
chan.local_state=chan.local_state._replace(was_announced=True)
coro = self.handle_announcements(chan)
self.lnworker.save_channel(chan)
@@ -887,25 +901,39 @@ class Peer(PrintError):
chan.set_state("OPEN")
self.network.trigger_callback('channel', chan)
# add channel to database
- node_ids = [self.pubkey, self.lnworker.node_keypair.pubkey]
+ pubkey_ours = self.lnworker.node_keypair.pubkey
+ pubkey_theirs = self.pubkey
+ node_ids = [pubkey_theirs, pubkey_ours]
bitcoin_keys = [chan.local_config.multisig_key.pubkey, chan.remote_config.multisig_key.pubkey]
sorted_node_ids = list(sorted(node_ids))
if sorted_node_ids != node_ids:
node_ids = sorted_node_ids
bitcoin_keys.reverse()
- now = int(time.time()).to_bytes(4, byteorder="big")
+ # note: we inject a channel announcement, and a channel update (for outgoing direction)
+ # This is atm needed for
+ # - finding routes
+ # - the ChanAnn is needed so that we can anchor to it a future ChanUpd
+ # that the remote sends, even if the channel was not announced
+ # (from BOLT-07: "MAY create a channel_update to communicate the channel
+ # parameters to the final node, even though the channel has not yet been announced")
self.channel_db.on_channel_announcement({"short_channel_id": chan.short_channel_id, "node_id_1": node_ids[0], "node_id_2": node_ids[1],
'chain_hash': constants.net.rev_genesis_bytes(), 'len': b'\x00\x00', 'features': b'',
'bitcoin_key_1': bitcoin_keys[0], 'bitcoin_key_2': bitcoin_keys[1]},
trusted=True)
- self.channel_db.on_channel_update({"short_channel_id": chan.short_channel_id, 'flags': b'\x01', 'cltv_expiry_delta': b'\x90',
- 'htlc_minimum_msat': b'\x03\xe8', 'fee_base_msat': b'\x03\xe8', 'fee_proportional_millionths': b'\x01',
- 'chain_hash': constants.net.rev_genesis_bytes(), 'timestamp': now},
- trusted=True)
- self.channel_db.on_channel_update({"short_channel_id": chan.short_channel_id, 'flags': b'\x00', 'cltv_expiry_delta': b'\x90',
+ # only inject outgoing direction:
+ flags = b'\x00' if node_ids[0] == pubkey_ours else b'\x01'
+ now = int(time.time()).to_bytes(4, byteorder="big")
+ self.channel_db.on_channel_update({"short_channel_id": chan.short_channel_id, 'flags': flags, 'cltv_expiry_delta': b'\x90',
'htlc_minimum_msat': b'\x03\xe8', 'fee_base_msat': b'\x03\xe8', 'fee_proportional_millionths': b'\x01',
'chain_hash': constants.net.rev_genesis_bytes(), 'timestamp': now},
trusted=True)
+ # peer may have sent us a channel update for the incoming direction previously
+ # note: if we were offline when the 3rd conf happened, lnd will never send us this channel_update
+ # see https://github.com/lightningnetwork/lnd/issues/1347
+ #self.send_message(gen_msg("query_short_channel_ids", chain_hash=constants.net.rev_genesis_bytes(),
+ # len=9, encoded_short_ids=b'\x00'+chan.short_channel_id))
+ if hasattr(chan, 'pending_channel_update_message'):
+ self.on_channel_update(chan.pending_channel_update_message)
self.print_error("CHANNEL OPENING COMPLETED")
diff --git a/electrum/lnhtlc.py b/electrum/lnhtlc.py
@@ -143,6 +143,7 @@ class HTLCStateMachine(PrintError):
self.funding_outpoint = Outpoint(**decodeAll(state["funding_outpoint"])) if type(state["funding_outpoint"]) is not Outpoint else state["funding_outpoint"]
self.node_id = maybeDecode("node_id", state["node_id"]) if type(state["node_id"]) is not bytes else state["node_id"]
self.short_channel_id = maybeDecode("short_channel_id", state["short_channel_id"]) if type(state["short_channel_id"]) is not bytes else state["short_channel_id"]
+ self.short_channel_id_predicted = self.short_channel_id
self.onion_keys = {int(k): bfh(v) for k,v in state['onion_keys'].items()} if 'onion_keys' in state else {}
# FIXME this is a tx serialised in the custom electrum partial tx format.
diff --git a/electrum/lnrouter.py b/electrum/lnrouter.py
@@ -45,6 +45,9 @@ from .lnutil import LN_GLOBAL_FEATURE_BITS, LNPeerAddr
class UnknownEvenFeatureBits(Exception): pass
+class NotFoundChanAnnouncementForUpdate(Exception): pass
+
+
class ChannelInfo(PrintError):
def __init__(self, channel_announcement_payload):
@@ -126,7 +129,7 @@ class ChannelInfo(PrintError):
else:
self.policy_node2 = new_policy
- def get_policy_for_node(self, node_id):
+ def get_policy_for_node(self, node_id: bytes) -> 'ChannelInfoDirectedPolicy':
if node_id == self.node_id_1:
return self.policy_node1
elif node_id == self.node_id_2:
@@ -271,7 +274,7 @@ class ChannelDB(JsonDB):
JsonDB.__init__(self, path)
self.lock = threading.RLock()
- self._id_to_channel_info = {}
+ self._id_to_channel_info = {} # type: Dict[bytes, ChannelInfo]
self._channels_for_node = defaultdict(set) # node -> set(short_channel_id)
self.nodes = {} # node_id -> NodeInfo
self._recent_peers = []
@@ -340,7 +343,7 @@ class ChannelDB(JsonDB):
# number of channels
return len(self._id_to_channel_info)
- def get_channel_info(self, channel_id) -> Optional[ChannelInfo]:
+ def get_channel_info(self, channel_id: bytes) -> Optional[ChannelInfo]:
return self._id_to_channel_info.get(channel_id, None)
def get_channels_for_node(self, node_id):
@@ -401,7 +404,7 @@ class ChannelDB(JsonDB):
channel_info = self._id_to_channel_info.get(short_channel_id, None)
if channel_info is None:
self.print_error("could not find", short_channel_id)
- return
+ raise NotFoundChanAnnouncementForUpdate()
channel_info.on_channel_update(msg_payload, trusted=trusted)
def on_node_announcement(self, msg_payload):
diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py
@@ -24,6 +24,7 @@ class LNWatcher(PrintError):
path = os.path.join(network.config.path, "watcher_db")
storage = WalletStorage(path)
self.addr_sync = AddressSynchronizer(storage)
+ self.addr_sync.diagnostic_name = lambda: 'LnWatcherAS'
self.addr_sync.start_network(network)
self.lock = threading.RLock()
self.watched_addresses = set()
diff --git a/electrum/lnworker.py b/electrum/lnworker.py
@@ -48,8 +48,8 @@ class LNWorker(PrintError):
self.ln_keystore = self._read_ln_keystore()
self.node_keypair = generate_keypair(self.ln_keystore, LnKeyFamily.NODE_KEY, 0)
self.config = network.config
- self.peers = {} # pubkey -> Peer
- self.channels = {x.channel_id: x for x in map(HTLCStateMachine, wallet.storage.get("channels", []))}
+ self.peers = {} # type: Dict[bytes, Peer] # pubkey -> Peer
+ self.channels = {x.channel_id: x for x in map(HTLCStateMachine, wallet.storage.get("channels", []))} # type: Dict[bytes, HTLCStateMachine]
for c in self.channels.values():
c.lnwatcher = network.lnwatcher
c.sweep_address = self.sweep_address
@@ -126,21 +126,19 @@ class LNWorker(PrintError):
def save_short_chan_id(self, chan):
"""
- Checks if the Funding TX has been mined. If it has save the short channel ID to disk and return the new OpenChannel.
-
- If the Funding TX has not been mined, return None
+ Checks if Funding TX has been mined. If it has, save the short channel ID in chan;
+ if it's also deep enough, also save to disk.
+ Returns tuple (mined_deep_enough, num_confirmations).
"""
assert chan.get_state() in ["OPEN", "OPENING"]
- peer = self.peers[chan.node_id]
addr_sync = self.network.lnwatcher.addr_sync
conf = addr_sync.get_tx_height(chan.funding_outpoint.txid).conf
- if conf >= chan.constraints.funding_txn_minimum_depth:
+ if conf > 0:
block_height, tx_pos = addr_sync.get_txpos(chan.funding_outpoint.txid)
- if tx_pos == -1:
- self.print_error('funding tx is not yet SPV verified.. but there are '
- 'already enough confirmations (currently {})'.format(conf))
- return False, conf
- chan.short_channel_id = calc_short_channel_id(block_height, tx_pos, chan.funding_outpoint.output_index)
+ assert tx_pos >= 0
+ chan.short_channel_id_predicted = calc_short_channel_id(block_height, tx_pos, chan.funding_outpoint.output_index)
+ if conf >= chan.constraints.funding_txn_minimum_depth > 0:
+ chan.short_channel_id = chan.short_channel_id_predicted
self.save_channel(chan)
return True, conf
return False, conf
@@ -244,6 +242,7 @@ class LNWorker(PrintError):
if amount_sat is None:
raise InvoiceError(_("Missing amount"))
amount_msat = int(amount_sat * 1000)
+ # TODO use 'r' field from invoice
path = self.network.path_finder.find_path_for_payment(self.node_keypair.pubkey, invoice_pubkey, amount_msat)
if path is None:
raise PaymentFailure(_("No path found"))
@@ -263,12 +262,39 @@ class LNWorker(PrintError):
payment_preimage = os.urandom(32)
RHASH = sha256(payment_preimage)
amount_btc = amount_sat/Decimal(COIN) if amount_sat else None
- pay_req = lnencode(LnAddr(RHASH, amount_btc, tags=[('d', message)]), self.node_keypair.privkey)
+ routing_hints = self._calc_routing_hints_for_invoice(amount_sat)
+ pay_req = lnencode(LnAddr(RHASH, amount_btc, tags=[('d', message)]+routing_hints),
+ self.node_keypair.privkey)
self.invoices[bh2u(payment_preimage)] = pay_req
self.wallet.storage.put('lightning_invoices', self.invoices)
self.wallet.storage.write()
return pay_req
+ def _calc_routing_hints_for_invoice(self, amount_sat):
+ """calculate routing hints (BOLT-11 'r' field)"""
+ routing_hints = []
+ with self.lock:
+ channels = list(self.channels.values())
+ # note: currently we add *all* our channels; but this might be a privacy leak?
+ for chan in channels:
+ # check channel is open
+ if chan.get_state() != "OPEN": continue
+ # check channel has sufficient balance
+ # FIXME because of on-chain fees of ctx, this check is insufficient
+ if amount_sat and chan.balance(REMOTE) // 1000 < amount_sat: continue
+ chan_id = chan.short_channel_id
+ assert type(chan_id) is bytes, chan_id
+ channel_info = self.channel_db.get_channel_info(chan_id)
+ if not channel_info: continue
+ policy = channel_info.get_policy_for_node(chan.node_id)
+ if not policy: continue
+ routing_hints.append(('r', [(chan.node_id,
+ chan_id,
+ policy.fee_base_msat,
+ policy.fee_proportional_millionths,
+ policy.cltv_expiry_delta)]))
+ return routing_hints
+
def delete_invoice(self, payreq_key):
try:
del self.invoices[payreq_key]