tlightning: Save invoices and preimages separately. Save preimages when forwarding - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 62be0c481c9fcdd26eb4b7c4a2c72eea8cec9b92 DIR parent e475617b7566556b337ccdc12fca64be57787453 HTML Author: ThomasV <thomasv@electrum.org> Date: Wed, 13 Feb 2019 15:46:35 +0100 lightning: Save invoices and preimages separately. Save preimages when forwarding Diffstat: M electrum/gui/qt/channel_details.py | 2 +- M electrum/gui/qt/invoice_list.py | 2 +- M electrum/gui/qt/request_list.py | 2 +- M electrum/lnpeer.py | 7 ++++--- M electrum/lnsweep.py | 4 ++-- M electrum/lnworker.py | 108 +++++++++++++++++-------------- M electrum/tests/test_lnpeer.py | 6 +++++- 7 files changed, 72 insertions(+), 59 deletions(-) --- DIR diff --git a/electrum/gui/qt/channel_details.py b/electrum/gui/qt/channel_details.py t@@ -73,7 +73,7 @@ class ChannelDetailsDialog(QtWidgets.QDialog): chan_id, i, direction, status = item lnaddr = None if pay_hash in invoices: - invoice = invoices[pay_hash][1] + invoice = invoices[pay_hash][0] lnaddr = lndecode(invoice) if status == 'inflight': if lnaddr is not None: DIR diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py t@@ -84,7 +84,7 @@ class InvoiceList(MyTreeView): self.model().insertRow(idx, items) lnworker = self.parent.wallet.lnworker - for key, (preimage_hex, invoice, is_received, pay_timestamp) in lnworker.invoices.items(): + for key, (invoice, is_received) in lnworker.invoices.items(): if is_received: continue status = lnworker.get_invoice_status(key) DIR diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py t@@ -134,7 +134,7 @@ class RequestList(MyTreeView): self.filter() # lightning lnworker = self.wallet.lnworker - for key, (preimage_hex, invoice, is_received, pay_timestamp) in lnworker.invoices.items(): + for key, (invoice, is_received) in lnworker.invoices.items(): if not is_received: continue status = lnworker.get_invoice_status(key) DIR diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py t@@ -377,7 +377,7 @@ class Peer(PrintError): sweep_address=self.lnworker.sweep_address, payment_completed=self.lnworker.payment_completed) chan.lnwatcher = self.lnwatcher - chan.get_preimage_and_invoice = self.lnworker.get_invoice # FIXME hack. + chan.get_preimage = self.lnworker.get_preimage # FIXME hack. sig_64, _ = chan.sign_next_commitment() self.send_message("funding_created", temporary_channel_id=temp_channel_id, t@@ -470,7 +470,7 @@ class Peer(PrintError): sweep_address=self.lnworker.sweep_address, payment_completed=self.lnworker.payment_completed) chan.lnwatcher = self.lnwatcher - chan.get_preimage_and_invoice = self.lnworker.get_invoice # FIXME hack. + chan.get_preimage = self.lnworker.get_preimage # FIXME hack. remote_sig = funding_created['signature'] chan.receive_new_commitment(remote_sig, []) sig_64, _ = chan.sign_next_commitment() t@@ -975,7 +975,8 @@ class Peer(PrintError): await self.await_local(chan, local_ctn) await self.await_remote(chan, remote_ctn) try: - preimage, invoice = self.lnworker.get_invoice(payment_hash) + invoice = self.lnworker.get_invoice(payment_hash) + preimage = self.lnworker.get_preimage(payment_hash) except UnknownPaymentHash: reason = OnionRoutingFailureMessage(code=OnionFailureCode.UNKNOWN_PAYMENT_HASH, data=b'') await self.fail_htlc(chan, htlc_id, onion_packet, reason) DIR diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py t@@ -157,7 +157,7 @@ def create_sweeptxs_for_our_latest_ctx(chan: 'Channel', ctx: Transaction, def create_txns_for_htlc(htlc: 'UpdateAddHtlc', is_received_htlc: bool) -> Tuple[Optional[Transaction], Optional[Transaction]]: if is_received_htlc: try: - preimage, invoice = chan.get_preimage_and_invoice(htlc.payment_hash) + preimage = chan.get_preimage(htlc.payment_hash) except UnknownPaymentHash as e: print_error(f'trying to sweep htlc from our latest ctx but getting {repr(e)}') return None, None t@@ -260,7 +260,7 @@ def create_sweeptxs_for_their_latest_ctx(chan: 'Channel', ctx: Transaction, def create_sweeptx_for_htlc(htlc: 'UpdateAddHtlc', is_received_htlc: bool) -> Optional[Transaction]: if not is_received_htlc: try: - preimage, invoice = chan.get_preimage_and_invoice(htlc.payment_hash) + preimage = chan.get_preimage(htlc.payment_hash) except UnknownPaymentHash as e: print_error(f'trying to sweep htlc from their latest ctx but getting {repr(e)}') return None DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py t@@ -68,8 +68,9 @@ class LNWorker(PrintError): def __init__(self, wallet: 'Abstract_Wallet'): self.wallet = wallet - # type: Dict[str, Tuple[str,str,bool,int]] # RHASH -> (preimage, invoice, is_received, timestamp) - self.invoices = self.wallet.storage.get('lightning_invoices', {}) + self.storage = wallet.storage + self.invoices = self.storage.get('lightning_invoices', {}) # RHASH -> (invoice, is_received) + self.preimages = self.storage.get('lightning_preimages', {}) # RHASH -> (preimage, timestamp) self.sweep_address = wallet.get_receiving_address() self.lock = threading.RLock() self.ln_keystore = self._read_ln_keystore() t@@ -78,12 +79,12 @@ class LNWorker(PrintError): self.channels = {} # type: Dict[bytes, Channel] for x in wallet.storage.get("channels", []): c = Channel(x, sweep_address=self.sweep_address, payment_completed=self.payment_completed) - c.get_preimage_and_invoice = self.get_invoice + c.get_preimage = self.get_preimage self.channels[c.channel_id] = c c.set_remote_commitment() c.set_local_commitment(c.current_commitment(LOCAL)) # timestamps of opening and closing transactions - self.channel_timestamps = self.wallet.storage.get('lightning_channel_timestamps', {}) + self.channel_timestamps = self.storage.get('lightning_channel_timestamps', {}) def start_network(self, network: 'Network'): self.network = network t@@ -106,7 +107,7 @@ class LNWorker(PrintError): if self.first_timestamp_requested is None: self.first_timestamp_requested = time.time() first_request = True - first_timestamp = self.wallet.storage.get('lightning_gossip_until', 0) + first_timestamp = self.storage.get('lightning_gossip_until', 0) if first_timestamp == 0: self.print_error('requesting whole channel graph') else: t@@ -120,28 +121,21 @@ class LNWorker(PrintError): while True: await asyncio.sleep(GRAPH_DOWNLOAD_SECONDS) yesterday = int(time.time()) - 24*60*60 # now minus a day - self.wallet.storage.put('lightning_gossip_until', yesterday) - self.wallet.storage.write() + self.storage.put('lightning_gossip_until', yesterday) + self.storage.write() self.print_error('saved lightning gossip timestamp') def payment_completed(self, chan, direction, htlc, _preimage): chan_id = chan.channel_id - key = bh2u(htlc.payment_hash) - if key not in self.invoices: - return - preimage, invoice, is_received, timestamp = self.invoices.get(key) - if direction == SENT: - preimage = bh2u(_preimage) - now = time.time() - self.invoices[key] = preimage, invoice, is_received, now - self.wallet.storage.put('lightning_invoices', self.invoices) - self.wallet.storage.write() - self.network.trigger_callback('ln_payment_completed', now, direction, htlc, preimage, chan_id) + preimage = _preimage if _preimage else self.get_preimage(htlc.payment_hash) + timestamp = time.time() + self.save_preimage(htlc.payment_hash, preimage, timestamp) + self.network.trigger_callback('ln_payment_completed', timestamp, direction, htlc, preimage, chan_id) def get_invoice_status(self, payment_hash): - if payment_hash not in self.invoices: + if payment_hash not in self.preimages: return PR_UNKNOWN - preimage, _addr, is_received, timestamp = self.invoices.get(payment_hash) + preimage, timestamp = self.preimages.get(payment_hash) if timestamp is None: return PR_UNPAID return PR_PAID t@@ -157,7 +151,7 @@ class LNWorker(PrintError): out = [] for chan_id, htlc, direction, status in self.get_payments().values(): key = bh2u(htlc.payment_hash) - timestamp = self.invoices[key][3] if key in self.invoices else None + timestamp = self.preimages[key][1] if key in self.preimages else None item = { 'type':'payment', 'timestamp':timestamp or 0, t@@ -205,21 +199,21 @@ class LNWorker(PrintError): return out def _read_ln_keystore(self) -> BIP32_KeyStore: - xprv = self.wallet.storage.get('lightning_privkey2') + xprv = self.storage.get('lightning_privkey2') if xprv is None: # TODO derive this deterministically from wallet.keystore at keystore generation time # probably along a hardened path ( lnd-equivalent would be m/1017'/coinType'/ ) seed = os.urandom(32) xprv, xpub = bip32_root(seed, xtype='standard') - self.wallet.storage.put('lightning_privkey2', xprv) + self.storage.put('lightning_privkey2', xprv) return keystore.from_xprv(xprv) def get_and_inc_counter_for_channel_keys(self): with self.lock: - ctr = self.wallet.storage.get('lightning_channel_key_der_ctr', -1) + ctr = self.storage.get('lightning_channel_key_der_ctr', -1) ctr += 1 - self.wallet.storage.put('lightning_channel_key_der_ctr', ctr) - self.wallet.storage.write() + self.storage.put('lightning_channel_key_der_ctr', ctr) + self.storage.write() return ctr def _add_peers_from_config(self): t@@ -264,8 +258,8 @@ class LNWorker(PrintError): with self.lock: self.channels[openchannel.channel_id] = openchannel dumped = [x.serialize() for x in self.channels.values()] - self.wallet.storage.put("channels", dumped) - self.wallet.storage.write() + self.storage.put("channels", dumped) + self.storage.write() self.network.trigger_callback('channel', openchannel) def save_short_chan_id(self, chan): t@@ -300,7 +294,7 @@ class LNWorker(PrintError): return self.print_error('on_channel_open', funding_outpoint) self.channel_timestamps[bh2u(chan.channel_id)] = funding_txid, funding_height.height, funding_height.timestamp, None, None, None - self.wallet.storage.put('lightning_channel_timestamps', self.channel_timestamps) + self.storage.put('lightning_channel_timestamps', self.channel_timestamps) chan.set_funding_txo_spentness(False) # send event to GUI self.network.trigger_callback('channel', chan) t@@ -312,7 +306,7 @@ class LNWorker(PrintError): return self.print_error('on_channel_closed', funding_outpoint) self.channel_timestamps[bh2u(chan.channel_id)] = funding_txid, funding_height.height, funding_height.timestamp, closing_txid, closing_height.height, closing_height.timestamp - self.wallet.storage.put('lightning_channel_timestamps', self.channel_timestamps) + self.storage.put('lightning_channel_timestamps', self.channel_timestamps) chan.set_funding_txo_spentness(True) if chan.get_state() != 'FORCE_CLOSING': chan.set_state("CLOSED") t@@ -473,7 +467,7 @@ class LNWorker(PrintError): if not chan: raise Exception("PathFinder returned path with short_channel_id {} that is not in channel list".format(bh2u(short_channel_id))) peer = self.peers[route[0].node_id] - self.save_invoice(None, pay_req, SENT) + self.save_invoice(addr.paymenthash, pay_req, SENT) htlc = await peer.pay(route, chan, int(addr.amount * COIN * 1000), addr.paymenthash, addr.get_min_final_cltv_expiry()) self.network.trigger_callback('htlc_added', htlc, addr, SENT) t@@ -546,34 +540,50 @@ class LNWorker(PrintError): def add_invoice(self, amount_sat, message): payment_preimage = os.urandom(32) - RHASH = sha256(payment_preimage) + payment_hash = sha256(payment_preimage) amount_btc = amount_sat/Decimal(COIN) if amount_sat else None routing_hints = self._calc_routing_hints_for_invoice(amount_sat) if not routing_hints: self.print_error("Warning. No routing hints added to invoice. " "Other clients will likely not be able to send to us.") - pay_req = lnencode(LnAddr(RHASH, amount_btc, + invoice = lnencode(LnAddr(payment_hash, amount_btc, tags=[('d', message), ('c', MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE)] + routing_hints), self.node_keypair.privkey) + self.save_invoice(payment_hash, invoice, RECEIVED) + self.save_preimage(payment_hash, payment_preimage, 0) + return invoice + + def save_preimage(self, payment_hash:bytes, preimage:bytes, timestamp:int): + assert sha256(preimage) == payment_hash + key = bh2u(payment_hash) + self.preimages[key] = bh2u(preimage), timestamp + self.storage.put('lightning_preimages', self.preimages) + self.storage.write() + + def get_preimage_and_timestamp(self, payment_hash: bytes) -> bytes: + try: + preimage_hex, timestamp = self.preimages[bh2u(payment_hash)] + preimage = bfh(preimage_hex) + assert sha256(preimage) == payment_hash + return preimage, timestamp + except KeyError as e: + raise UnknownPaymentHash(payment_hash) from e - self.save_invoice(bh2u(payment_preimage), pay_req, RECEIVED) - return pay_req + def get_preimage(self, payment_hash: bytes) -> bytes: + return self.get_preimage_and_timestamp(payment_hash)[0] - def save_invoice(self, preimage, invoice, direction): - lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP) - key = bh2u(lnaddr.paymenthash) - self.invoices[key] = preimage, invoice, direction==RECEIVED, None - self.wallet.storage.put('lightning_invoices', self.invoices) - self.wallet.storage.write() + def save_invoice(self, payment_hash:bytes, invoice, direction): + key = bh2u(payment_hash) + self.invoices[key] = invoice, direction==RECEIVED + self.storage.put('lightning_invoices', self.invoices) + self.storage.write() - def get_invoice(self, payment_hash: bytes) -> Tuple[bytes, LnAddr]: + def get_invoice(self, payment_hash: bytes) -> LnAddr: try: - preimage_hex, pay_req, is_received, timestamp = self.invoices[bh2u(payment_hash)] - preimage = bfh(preimage_hex) - assert sha256(preimage) == payment_hash - return preimage, lndecode(pay_req, expected_hrp=constants.net.SEGWIT_HRP) + invoice, is_received = self.invoices[bh2u(payment_hash)] + return lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP) except KeyError as e: raise UnknownPaymentHash(payment_hash) from e t@@ -618,14 +628,12 @@ class LNWorker(PrintError): return routing_hints def delete_invoice(self, payment_hash_hex: str): - # FIXME we will now LOSE the preimage!! is this feature a good idea? - # maybe instead of deleting, we could have a feature to "hide" invoices (e.g. for GUI) try: del self.invoices[payment_hash_hex] except KeyError: return - self.wallet.storage.put('lightning_invoices', self.invoices) - self.wallet.storage.write() + self.storage.put('lightning_invoices', self.invoices) + self.storage.write() def get_balance(self): with self.lock: DIR diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py t@@ -82,6 +82,7 @@ class MockLNWorker: self.network = MockNetwork(tx_queue) self.channels = {self.chan.channel_id: self.chan} self.invoices = {} + self.preimages = {} self.inflight = {} self.wallet = MockWallet() t@@ -112,6 +113,8 @@ class MockLNWorker: pass get_invoice = LNWorker.get_invoice + get_preimage = LNWorker.get_preimage + get_preimage_and_timestamp = LNWorker.get_preimage_and_timestamp _create_route_from_invoice = LNWorker._create_route_from_invoice _check_invoice = staticmethod(LNWorker._check_invoice) _pay_to_route = LNWorker._pay_to_route t@@ -204,7 +207,8 @@ class TestPeer(unittest.TestCase): ('d', 'coffee') ]) pay_req = lnencode(addr, w2.node_keypair.privkey) - w2.invoices[bh2u(RHASH)] = (bh2u(payment_preimage), pay_req, True, None) + w2.preimages[bh2u(RHASH)] = (bh2u(payment_preimage), 0) + w2.invoices[bh2u(RHASH)] = (pay_req, True) return pay_req @staticmethod