electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 7962e17df67f0196af7ebdf2780d04a80a8ccedc
parent 4c177c4c9269a3fb8dda8536f4d7f663c0e80180
Author: SomberNight <somber.night@protonmail.com>
Date:   Wed,  4 Mar 2020 14:24:07 +0100

invoices: deal with expiration of "0" mess

Internally, we've been using an expiration of 0 to mean "never expires".
For LN invoices, BOLT-11 does not specify what an expiration of 0 means.
Other clients seem to treat it as "0 seconds" (i.e. already expired).
This means there is no way to create a BOLT-11 invoice that "never" expires.

For LN invoices,
- we now treat an expiration of 0, , as "0 seconds",
- when creating an invoice, if the user selected never, we will put 100 years as expiration

Diffstat:
Melectrum/gui/kivy/uix/screens.py | 4++--
Melectrum/gui/qt/main_window.py | 6+++---
Melectrum/lnaddr.py | 25+++++++++++++++----------
Melectrum/lnworker.py | 8+++++++-
Melectrum/util.py | 7++++++-
5 files changed, 33 insertions(+), 17 deletions(-)

diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py @@ -24,7 +24,7 @@ from kivy.utils import platform from kivy.logger import Logger from electrum.util import profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds, Fiat -from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN +from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN, PR_DEFAULT_EXPIRATION_WHEN_CREATING from electrum import bitcoin, constants from electrum.transaction import Transaction, tx_from_any, PartialTransaction, PartialTxOutput from electrum.util import (parse_URI, InvalidBitcoinURI, PR_PAID, PR_UNKNOWN, PR_EXPIRED, @@ -419,7 +419,7 @@ class ReceiveScreen(CScreen): Clock.schedule_interval(lambda dt: self.update(), 5) def expiry(self): - return self.app.electrum_config.get('request_expiry', 3600) # 1 hour + return self.app.electrum_config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING) def clear(self): self.screen.address = '' diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py @@ -62,7 +62,7 @@ from electrum.util import (format_time, format_satoshis, format_fee_satoshis, get_new_wallet_name, send_exception_to_crash_reporter, InvalidBitcoinURI, maybe_extract_bolt11_invoice, NotEnoughFunds, NoDynamicFeeEstimates, MultipleSpendMaxTxOutputs) -from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN +from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN, PR_DEFAULT_EXPIRATION_WHEN_CREATING from electrum.transaction import (Transaction, PartialTxInput, PartialTransaction, PartialTxOutput) from electrum.address_synchronizer import AddTransactionException @@ -1007,7 +1007,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): evl = sorted(pr_expiration_values.items()) evl_keys = [i[0] for i in evl] evl_values = [i[1] for i in evl] - default_expiry = self.config.get('request_expiry', 3600) + default_expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING) try: i = evl_keys.index(default_expiry) except ValueError: @@ -1139,7 +1139,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): def create_invoice(self, is_lightning): amount = self.receive_amount_e.get_amount() message = self.receive_message_e.text() - expiry = self.config.get('request_expiry', 3600) + expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING) if is_lightning: key = self.wallet.lnworker.add_request(amount, message, expiry) else: diff --git a/electrum/lnaddr.py b/electrum/lnaddr.py @@ -199,6 +199,8 @@ def lnencode(addr, privkey): # Get minimal length by trimming leading 5 bits at a time. expirybits = bitstring.pack('intbe:64', v)[4:64] while expirybits.startswith('0b00000'): + if len(expirybits) == 5: + break # v == 0 expirybits = expirybits[5:] data += tagged('x', expirybits) elif k == 'h': @@ -259,21 +261,24 @@ class LnAddr(object): return self._min_final_cltv_expiry def get_tag(self, tag): - description = '' - for k,v in self.tags: + for k, v in self.tags: if k == tag: - description = v - break - return description + return v + return None - def get_description(self): - return self.get_tag('d') + def get_description(self) -> str: + return self.get_tag('d') or '' - def get_expiry(self): - return int(self.get_tag('x') or '3600') + def get_expiry(self) -> int: + exp = self.get_tag('x') + if exp is None: + exp = 3600 + return int(exp) - def is_expired(self): + def is_expired(self) -> bool: now = time.time() + # BOLT-11 does not specify what expiration of '0' means. + # we treat it as 0 seconds here (instead of never) return now > self.get_expiry() + self.date diff --git a/electrum/lnworker.py b/electrum/lnworker.py @@ -1112,7 +1112,7 @@ class LNWallet(LNWorker): raise Exception(_("add invoice timed out")) @log_exceptions - async def _add_request_coro(self, amount_sat, message, expiry): + async def _add_request_coro(self, amount_sat, message, expiry: int): timestamp = int(time.time()) routing_hints = await self._calc_routing_hints_for_invoice(amount_sat) if not routing_hints: @@ -1122,6 +1122,12 @@ class LNWallet(LNWorker): payment_hash = sha256(payment_preimage) info = PaymentInfo(payment_hash, amount_sat, RECEIVED, PR_UNPAID) amount_btc = amount_sat/Decimal(COIN) if amount_sat else None + if expiry == 0: + # hack: BOLT-11 is not really clear on what an expiry of 0 means. + # It probably interprets it as 0 seconds, so already expired... + # Our higher level invoices code however uses 0 for "never". + # Hence set some high expiration here + expiry = 100 * 365 * 24 * 60 * 60 # 100 years lnaddr = LnAddr(payment_hash, amount_btc, tags=[('d', message), ('c', MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE), diff --git a/electrum/util.py b/electrum/util.py @@ -104,17 +104,22 @@ pr_tooltips = { PR_FAILED:_('Failed'), } +PR_DEFAULT_EXPIRATION_WHEN_CREATING = 24*60*60 # 1 day pr_expiration_values = { 0: _('Never'), 10*60: _('10 minutes'), 60*60: _('1 hour'), 24*60*60: _('1 day'), - 7*24*60*60: _('1 week') + 7*24*60*60: _('1 week'), } +assert PR_DEFAULT_EXPIRATION_WHEN_CREATING in pr_expiration_values + def get_request_status(req): status = req['status'] exp = req.get('exp', 0) or 0 + if req.get('type') == PR_TYPE_LN and exp == 0: + status = PR_EXPIRED # for BOLT-11 invoices, exp==0 means 0 seconds if req['status'] == PR_UNPAID and exp > 0 and req['time'] + req['exp'] < time.time(): status = PR_EXPIRED status_str = pr_tooltips[status]