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:
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]