Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions electrum/bip21.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from . import bitcoin
from .util import format_satoshis_plain
from .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC
from .bolt11 import decode_bolt11_invoice, BOLT11DecodeException
from .bolt11 import decode_bolt11_invoice, BOLT11InvoiceException

# note: when checking against these, use .lower() to support case-insensitivity
BITCOIN_BIP21_URI_SCHEME = 'bitcoin'
Expand Down Expand Up @@ -35,7 +35,11 @@ def parse_bip21_URI(uri: str) -> dict:
raise InvalidBitcoinURI("Not a bitcoin address")
return {'address': uri}

u = urllib.parse.urlparse(uri)
try:
u = urllib.parse.urlparse(uri)
except ValueError as e:
raise InvalidBitcoinURI("failed to parse uri") from e

if u.scheme.lower() != BITCOIN_BIP21_URI_SCHEME:
raise InvalidBitcoinURI("Not a bitcoin URI")
address = u.path
Expand Down Expand Up @@ -94,7 +98,7 @@ def parse_bip21_URI(uri: str) -> dict:
if 'lightning' in out:
try:
lnaddr = decode_bolt11_invoice(out['lightning'])
except BOLT11DecodeException as e:
except BOLT11InvoiceException as e:
raise InvalidBitcoinURI(f"Failed to decode 'lightning' field: {e!r}") from e
amount_sat = out.get('amount')
if amount_sat:
Expand Down
93 changes: 65 additions & 28 deletions electrum/bolt11.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import electrum_ecc as ecc

from .bitcoin import hash160_to_b58_address, b58_address_to_hash160, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC
from .segwit_addr import bech32_encode, bech32_decode, CHARSET, CHARSET_INVERSE, convertbits
from .segwit_addr import bech32_encode, bech32_decode, CHARSET, CHARSET_INVERSE, convertbits, INVALID_BECH32
from . import segwit_addr
from . import constants
from .constants import AbstractNet
Expand Down Expand Up @@ -96,13 +96,24 @@ def encode_fallback_addr(fallback: str, net: Type[AbstractNet]) -> Sequence[int]


def parse_fallback_addr(data5: Sequence[int], net: Type[AbstractNet]) -> Optional[str]:
"""Returns None if the fallback address cannot be parsed (caller should skip the field)."""
if not data5:
return None
wver = data5[0]
data8 = bytes(convertbits(data5[1:], 5, 8, False))
data8 = convertbits(data5[1:], 5, 8, False)
if data8 is None: # invalid padding
return None
data8 = bytes(data8)
if wver == 17:
if len(data8) != 20: # hash160
return None
addr = hash160_to_b58_address(data8, net.ADDRTYPE_P2PKH)
elif wver == 18:
if len(data8) != 20: # hash160
return None
addr = hash160_to_b58_address(data8, net.ADDRTYPE_P2SH)
elif wver <= 16:
# note: encode_segwit_address checks the witness program length
addr = segwit_addr.encode_segwit_address(net.SEGWIT_HRP, wver, data8)
else:
return None
Expand Down Expand Up @@ -404,17 +415,29 @@ def serialize(self):
return self.pubkey.get_public_key_bytes(True)


def decode_bolt11_invoice(invoice: str, *, verbose=False, net=None) -> BOLT11Addr:
def decode_bolt11_invoice(invoice: str, *, verbose=False, net=None, strict=True) -> BOLT11Addr:
"""Parses a string into a BOLT11Addr object.
Can raise BOLT11DecodeException or IncompatibleOrInsaneFeatures.
Can raise BOLT11DecodeException.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The addr.amount setter below can also raise BOLT11InvoiceException, this could be converted to BOLT11DecodeException?


:param strict:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new strict parameter is only used for the wallet upgrades, what do you think about catching the exception in the wallet upgrades instead of introducing an additional parameter?
This would keep the API simpler to understand.

Something like:
diff --git a/electrum/bolt11.py b/electrum/bolt11.py
index 840d805f09..f84b3e290b 100644
--- a/electrum/bolt11.py
+++ b/electrum/bolt11.py
@@ -415,14 +415,9 @@ class SerializableKey:
         return self.pubkey.get_public_key_bytes(True)
 
 
-def decode_bolt11_invoice(invoice: str, *, verbose=False, net=None, strict=True) -> BOLT11Addr:
+def decode_bolt11_invoice(invoice: str, *, verbose=False, net=None) -> BOLT11Addr:
     """Parses a string into a BOLT11Addr object.
     Can raise BOLT11DecodeException.
-
-    :param strict:
-        if True, fail decode on malformed 'r' and 't' tags. False restores old behavior.
-        False is used when migrating wallet_db: an invoice stored by an older version can be
-        malformed.
     """
 
     def _convertbits_tag(tag, *args, **kwargs):
@@ -497,9 +492,7 @@ def decode_bolt11_invoice(invoice: str, *, verbose=False, net=None, strict=True)
             #    * `feebase` (32 bits, big-endian)
             #    * `feerate` (32 bits, big-endian)
             #    * `cltv_expiry_delta` (16 bits, big-endian)
-            tagdata = convertbits(tagdata, 5, 8, False)
-            if strict and tagdata is None:
-                raise BOLT11DecodeException(f"Failed to decode tag '{tag}'")
+            tagdata = _convertbits_tag(tag, tagdata, 5, 8, False)
             if not tagdata:
                 continue
             route = []
@@ -519,9 +512,7 @@ def decode_bolt11_invoice(invoice: str, *, verbose=False, net=None, strict=True)
             if route:
                 addr.tags.append(('r',route))
         elif tag == 't':
-            tagdata = convertbits(tagdata, 5, 8, False)
-            if strict and tagdata is None:
-                raise BOLT11DecodeException(f"Failed to decode tag '{tag}'")
+            tagdata = _convertbits_tag(tag, tagdata, 5, 8, False)
             if not tagdata:
                 continue
             route = []
diff --git a/electrum/wallet_db.py b/electrum/wallet_db.py
index aba35bf046..7aa8e73c74 100644
--- a/electrum/wallet_db.py
+++ b/electrum/wallet_db.py
@@ -932,13 +932,18 @@ class WalletDBUpgrader(Logger):
         # the new key for all requests is a wallet address, not done here
         for name in ['invoices', 'payment_requests']:
             invoices = self.data.get(name, {})
-            for key, item in invoices.items():
+            for key, item in list(invoices.items()):
                 is_lightning = item['type'] == 2
                 lightning_invoice = item['invoice'] if is_lightning else None
                 outputs = item['outputs'] if not is_lightning else None
                 bip70 = item['bip70'] if not is_lightning else None
                 if is_lightning:
-                    lnaddr = decode_bolt11_invoice(item['invoice'], strict=False)
+                    try:
+                        lnaddr = decode_bolt11_invoice(item['invoice'])
+                    except BOLT11InvoiceException as e:
+                        self.logger.warning(f"removing {name} item {key} that fails bolt11 decode: {e}")
+                        del invoices[key]
+                        continue
                     amount_msat = lnaddr.get_amount_msat()
                     timestamp = lnaddr.date
                     exp_delay = lnaddr.get_expiry()
@@ -999,7 +1004,12 @@ class WalletDBUpgrader(Logger):
         for key, item in list(requests.items()):
             lnaddr = item.get('lightning_invoice')
             if lnaddr:
-                lnaddr = decode_bolt11_invoice(lnaddr, strict=False)
+                try:
+                    lnaddr = decode_bolt11_invoice(lnaddr)
+                except BOLT11InvoiceException as e:
+                    self.logger.warning(f"removing request {key} that fails bolt11 decode: {e}")
+                    del requests[key]
+                    continue
                 rhash = lnaddr.paymenthash.hex()
                 if key != rhash:
                     requests[rhash] = item
@@ -1049,7 +1059,12 @@ class WalletDBUpgrader(Logger):
             if lightning_invoice is None:
                 payment_hash = None
             else:
-                lnaddr = decode_bolt11_invoice(lightning_invoice, strict=False)
+                try:
+                    lnaddr = decode_bolt11_invoice(lightning_invoice)
+                except BOLT11InvoiceException as e:
+                    self.logger.warning(f"removing request {key} that fails bolt11 decode: {e}")
+                    del requests[key]
+                    continue
                 payment_hash = lnaddr.paymenthash.hex()
             item['payment_hash'] = payment_hash
         self.data['seed_version'] = 51
@@ -1486,15 +1501,15 @@ class WalletDBUpgrader(Logger):
         from .bolt11 import decode_bolt11_invoice
         if not self._is_upgrade_method_needed(72, 72):
             return
-        # remove invoices not passing strict bolt11 invoice check
+        # remove invoices not passing stricter bolt11 invoice parsing (https://github.com/spesmilo/electrum/pull/10940)
         invoices = self.data.get('invoices', {})
         for key, item in list(invoices.items()):
             lnaddr = item.get('lightning_invoice')
             if lnaddr:
                 try:
-                    decode_bolt11_invoice(lnaddr, strict=True)
+                    decode_bolt11_invoice(lnaddr)
                 except BOLT11InvoiceException as e:
-                    self.logger.warning(f"removing invoice {key} that fails strict bolt11 check: {e}")
+                    self.logger.warning(f"removing invoice {key} that fails bolt11 decode: {e}")
                     del invoices[key]
         self.data['seed_version'] = 73
 
diff --git a/tests/test_bolt11.py b/tests/test_bolt11.py
index a4b2e9e395..7530f14250 100644
--- a/tests/test_bolt11.py
+++ b/tests/test_bolt11.py
@@ -211,7 +211,7 @@ class TestBolt11(ElectrumTestCase):
                          [17] + [0] * 4,    # p2pkh with a too-short hash160
                          [0] + [0] * 4,     # p2wpkh with a too-short witness program
                          [19, 0, 0]):       # unknown witness version
-            lnaddr = decode_bolt11_invoice(self._encode_invoice_with_raw_tag('f', tagdata5), strict=True)
+            lnaddr = decode_bolt11_invoice(self._encode_invoice_with_raw_tag('f', tagdata5))
             self.assertIsNone(lnaddr.get_tag('f'))
             self.assertEqual('', lnaddr.get_fallback_address())
             self.assertEqual(['f'], [tag for tag, _ in lnaddr.unknown_tags])
@@ -236,7 +236,7 @@ class TestBolt11(ElectrumTestCase):
                               ('t', [0, 1])):
             with self.subTest(tag=tag, tagdata5=tagdata5):
                 with self.assertRaises(BOLT11DecodeException):
-                    decode_bolt11_invoice(self._encode_invoice_with_raw_tag(tag, tagdata5), strict=True)
+                    decode_bolt11_invoice(self._encode_invoice_with_raw_tag(tag, tagdata5))
 
         # control: the same lengths with zero padding bits decode fine
         for tag, tagdata5 in (('d', [0, 0]),
@@ -255,46 +255,20 @@ class TestBolt11(ElectrumTestCase):
                 lnaddr = decode_bolt11_invoice(self._encode_invoice_with_raw_tag(tag, [0] * 51))
                 self.assertEqual([tag], [t for t, _ in lnaddr.unknown_tags])
 
-    def test_malformed_route_tags_are_skipped_when_not_strict(self):
-        # strict=False restores the pre-fix behaviour for 'r' and 't': a payload that cannot be
-        # converted back to bytes is silently dropped instead of aborting the decode. wallet_db
-        # passes strict=False, as an invoice already stored by an older version must stay loadable.
+        # 'r' and 't': an empty payload converts to b'' instead of failing, so it is skipped
         for tag in ('r', 't'):
-            for tagdata5 in ([1],        # 5 bits left over: no valid conversion
-                             [0, 1]):    # non-zero padding bits
-                invoice = self._encode_invoice_with_raw_tag(tag, tagdata5)
-                with self.subTest(tag=tag, tagdata5=tagdata5, strict=False):
-                    lnaddr = decode_bolt11_invoice(invoice, strict=False)
-                    # the malformed field is dropped: it lands in neither tags nor unknown_tags
-                    self.assertIsNone(lnaddr.get_tag(tag))
-                    self.assertEqual([], lnaddr.get_routing_info(tag))
-                    self.assertEqual([], lnaddr.unknown_tags)
-                    # ...and the rest of the invoice still decodes
-                    self.assertEqual(RHASH, lnaddr.paymenthash)
-                    self.assertEqual('test', lnaddr.get_description())
-                # strict is the default, and rejects the same invoice
-                for kwargs in ({}, {'strict': True}):
-                    with self.subTest(tag=tag, tagdata5=tagdata5, kwargs=kwargs):
-                        with self.assertRaises(BOLT11DecodeException):
-                            decode_bolt11_invoice(invoice, **kwargs)
-
-        # an empty payload converts to b'' instead of failing, so it is skipped in both modes
-        for tag in ('r', 't'):
-            for strict in (False, True):
-                with self.subTest(tag=tag, tagdata5=[], strict=strict):
-                    lnaddr = decode_bolt11_invoice(self._encode_invoice_with_raw_tag(tag, []), strict=strict)
-                    self.assertIsNone(lnaddr.get_tag(tag))
-                    self.assertEqual([], lnaddr.unknown_tags)
+            with self.subTest(tag=tag, tagdata5=[]):
+                lnaddr = decode_bolt11_invoice(self._encode_invoice_with_raw_tag(tag, []))
+                self.assertIsNone(lnaddr.get_tag(tag))
+                self.assertEqual([], lnaddr.unknown_tags)
 
-        # control: a well-formed hop is parsed identically in both modes
+        # control: a well-formed hop is parsed
         r_hop = bytes(33) + bytes(8) + (1).to_bytes(4, 'big') + (2).to_bytes(4, 'big') + (3).to_bytes(2, 'big')
         t_hop = bytes(33) + (1).to_bytes(4, 'big') + (2).to_bytes(4, 'big') + (3).to_bytes(2, 'big')
         for tag, hop in (('r', r_hop), ('t', t_hop)):
             with self.subTest(tag=tag):
                 invoice = self._encode_invoice_with_raw_tag(tag, list(convertbits(hop, 8, 5)))
                 self.assertEqual(1, len(decode_bolt11_invoice(invoice).get_routing_info(tag)))
-                self.assertEqual(decode_bolt11_invoice(invoice, strict=False).get_routing_info(tag),
-                                 decode_bolt11_invoice(invoice, strict=True).get_routing_info(tag))
 
     def test_invalid_signature(self):
         # The trailing 65 bytes of an invoice are attacker-controlled: every way the ecc lib
diff --git a/tests/test_storage_upgrade.py b/tests/test_storage_upgrade.py
index e691add066..07f610aeab 100644
--- a/tests/test_storage_upgrade.py
+++ b/tests/test_storage_upgrade.py
@@ -343,10 +343,12 @@ class TestStorageUpgrade(WalletTestCase):
 
     @as_testnet
     async def test_upgrade_removes_invoice_with_malformed_route_tag(self):
-        # Db conversion 72->73 drops stored invoices that do not pass the strict bolt11 check.
+        # Db conversion 72->73 drops stored invoices that fail bolt11 decoding.
         # Older versions decoded a malformed 'r'/'t' tag by silently skipping it, so such an
         # invoice can be sitting in a wallet file; without this conversion it would now abort
         # the load in Invoice._validate_invoice_str, leaving the file unopenable.
+        # The older conversions that decode invoices themselves (45, 47, 51) drop such items
+        # the same way, so a file from before those versions upgrades too.
         # The malformed invoices below are correctly signed, but their 'r'/'t' payload has
         # non-zero padding bits; see TestBolt11._encode_invoice_with_raw_tag.
         bad_r = ('lntb1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq8w3jhxaqrqzq'
@@ -383,6 +385,14 @@ class TestStorageUpgrade(WalletTestCase):
         with self.assertRaises(BOLT11DecodeException):
             self._load_db_from_json_string(wallet_json=json.dumps(data), upgrade=True)
 
+        # a pre-45 file: conversion 45 decodes the invoices itself and drops the bad ones
+        data['seed_version'] = 44
+        data['invoices'] = {key: {'type': 2, 'invoice': invoice_str}
+                            for key, invoice_str in (('bad_r', bad_r), ('bad_t', bad_t), ('good', good))}
+        db = self._load_db_from_json_string(wallet_json=json.dumps(data), upgrade=True)
+        self.assertEqual(73, db.get('seed_version'))
+        self.assertEqual(['good'], list(db.get_dict('invoices').keys()))
+
 
 ##########
 

if True, fail decode on malformed 'r' and 't' tags. False restores old behavior.
False is used when migrating wallet_db: an invoice stored by an older version can be
malformed.
"""

def _convertbits_tag(tag, *args, **kwargs):
if (intseq := convertbits(*args, **kwargs)) is None:
raise BOLT11DecodeException(f"Failed to decode tag '{tag}'")
return intseq

if net is None:
net = constants.net
decoded_bech32 = bech32_decode(invoice, ignore_long_length=True)
if decoded_bech32 is INVALID_BECH32:
raise BOLT11DecodeException("Invalid bech32 checksum")
hrp = decoded_bech32.hrp
data5 = decoded_bech32.data # "5" as in list of 5-bit integers
if decoded_bech32.encoding is None:
raise BOLT11DecodeException("Bad bech32 checksum")
assert data5 is not None
if decoded_bech32.encoding != segwit_addr.Encoding.BECH32:
raise BOLT11DecodeException("Bad bech32 encoding: must be using vanilla BECH32")

Expand Down Expand Up @@ -451,7 +474,10 @@ def decode_bolt11_invoice(invoice: str, *, verbose=False, net=None) -> BOLT11Add
data5_remaining = data5_remaining[7:]

while data5_remaining:
tag, tagdata = pull_tagged(data5_remaining) # mutates arg
try:
tag, tagdata = pull_tagged(data5_remaining) # mutates arg
except ValueError as e:
raise BOLT11DecodeException(f"Corrupt tag data: {str(e)}")

# BOLT #11:
#
Expand All @@ -472,6 +498,8 @@ def decode_bolt11_invoice(invoice: str, *, verbose=False, net=None) -> BOLT11Add
# * `feerate` (32 bits, big-endian)
# * `cltv_expiry_delta` (16 bits, big-endian)
tagdata = convertbits(tagdata, 5, 8, False)
if strict and tagdata is None:
raise BOLT11DecodeException(f"Failed to decode tag '{tag}'")
if not tagdata:
continue
route = []
Expand All @@ -492,6 +520,8 @@ def decode_bolt11_invoice(invoice: str, *, verbose=False, net=None) -> BOLT11Add
addr.tags.append(('r',route))
elif tag == 't':
tagdata = convertbits(tagdata, 5, 8, False)
if strict and tagdata is None:
raise BOLT11DecodeException(f"Failed to decode tag '{tag}'")
if not tagdata:
continue
route = []
Expand All @@ -516,13 +546,15 @@ def decode_bolt11_invoice(invoice: str, *, verbose=False, net=None) -> BOLT11Add
continue

elif tag == 'd':
addr.tags.append(('d', bytes(convertbits(tagdata, 5, 8, False)).decode('utf-8')))

try:
addr.tags.append(('d', bytes(_convertbits_tag(tag, tagdata, 5, 8, False)).decode('utf-8')))
except UnicodeDecodeError as e:
raise BOLT11DecodeException(f"Invalid UTF-8 content in invoice: {str(e)}")
elif tag == 'h':
if data_length != 52:

@f321x f321x Sep 8, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of adding these tags with incorrect data_length to unknown_tags we should probably just raise BOLT11DecodeException too:
https://github.com/lightning/bolts/blob/152897261850d93c4f4597f39cf22d7d22d6ede6/11-payment-encoding.md?plain=1#L213

addr.unknown_tags.append((tag, tagdata))
continue
addr.tags.append(('h', bytes(convertbits(tagdata, 5, 8, False))))
addr.tags.append(('h', bytes(_convertbits_tag(tag, tagdata, 5, 8, False))))

elif tag == 'x':
addr.tags.append(('x', int_from_data5(tagdata)))

@f321x f321x Sep 8, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should also limit expiry timestamps to 'sane' values (like < 2**37):

>>> util.format_time(2 ** 37)
'6325-04-08 17:04'
>>> util.format_time(2 ** 38)
Traceback (most recent call last):
  File "<python-input-5>", line 1, in <module>
    util.format_time(2 ** 38)
    ~~~~~~~~~~~~~~~~^^^^^^^^^
  File "/var/home/user/code/code_vm/electrum/electrum/util.py", line 915, in format_time
    date = timestamp_to_datetime(timestamp)
  File "/var/home/user/code/code_vm/electrum/electrum/util.py", line 911, in timestamp_to_datetime
    return datetime.fromtimestamp(timestamp, tz=tz)
           ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
ValueError: year must be in 1..9999, not 10680

Expand All @@ -531,19 +563,19 @@ def decode_bolt11_invoice(invoice: str, *, verbose=False, net=None) -> BOLT11Add
if data_length != 52:
addr.unknown_tags.append((tag, tagdata))
continue
addr.paymenthash = bytes(convertbits(tagdata, 5, 8, False))
addr.paymenthash = bytes(_convertbits_tag(tag, tagdata, 5, 8, False))

elif tag == 's':
if data_length != 52:
addr.unknown_tags.append((tag, tagdata))
continue
addr.payment_secret = bytes(convertbits(tagdata, 5, 8, False))
addr.payment_secret = bytes(_convertbits_tag(tag, tagdata, 5, 8, False))

elif tag == 'n':
if data_length != 53:
addr.unknown_tags.append((tag, tagdata))
continue
pubkeybytes = bytes(convertbits(tagdata, 5, 8, False))
pubkeybytes = bytes(_convertbits_tag(tag, tagdata, 5, 8, False))
addr.pubkey = pubkeybytes

elif tag == 'c':
Expand Down Expand Up @@ -575,20 +607,25 @@ def decode_bolt11_invoice(invoice: str, *, verbose=False, net=None) -> BOLT11Add
# field specified below).
addr.signature = sigdecoded[:65]
hrp_hash = sha256(hrp.encode("ascii") + bytes(convertbits(data5, 5, 8, True))).digest()
if addr.pubkey: # Specified by `n`
# BOLT #11:
#
# A reader MUST use the `n` field to validate the signature instead of
# performing signature recovery if a valid `n` field is provided.
if not ecc.ECPubkey(addr.pubkey).ecdsa_verify(sigdecoded[:64], hrp_hash):
raise BOLT11DecodeException("bad signature")
pubkey_copy = addr.pubkey

class WrappedBytesKey:
serialize = lambda: pubkey_copy

addr.pubkey = WrappedBytesKey
else: # Recover pubkey from signature.
addr.pubkey = SerializableKey(ecc.ECPubkey.from_ecdsa_sig64(sigdecoded[:64], sigdecoded[64], hrp_hash))
try:
if addr.pubkey: # Specified by `n`
# BOLT #11:
#
# A reader MUST use the `n` field to validate the signature instead of
# performing signature recovery if a valid `n` field is provided.
if not ecc.ECPubkey(addr.pubkey).ecdsa_verify(sigdecoded[:64], hrp_hash):
raise BOLT11DecodeException("bad signature")
pubkey_copy = addr.pubkey

class WrappedBytesKey:
serialize = lambda: pubkey_copy

addr.pubkey = WrappedBytesKey
else: # Recover pubkey from signature.
addr.pubkey = SerializableKey(ecc.ECPubkey.from_ecdsa_sig64(sigdecoded[:64], sigdecoded[64], hrp_hash))
except Exception as e:
if isinstance(e, BOLT11DecodeException):
raise
raise BOLT11DecodeException(f"Invalid signature: {e}") from e

return addr
26 changes: 22 additions & 4 deletions electrum/wallet_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@

from . import bitcoin
from . import constants
from .bolt11 import BOLT11InvoiceException
from .util import profiler, WalletFileException, multisig_type, TxMinedInfo, MyEncoder, bfh
from .keystore import bip44_derivation
from .transaction import (Transaction, TxOutpoint, tx_from_any, PartialTransaction, PartialTxOutput, BadHeaderMagic,
Expand Down Expand Up @@ -72,7 +73,7 @@ def __init__(self, wallet_db: 'WalletDB'):
# seed_version is now used for the version of the wallet file
OLD_SEED_VERSION = 4 # electrum versions < 2.0
NEW_SEED_VERSION = 11 # electrum versions >= 2.0
FINAL_SEED_VERSION = 72 # electrum >= 2.7 will set this to prevent
FINAL_SEED_VERSION = 73 # electrum >= 2.7 will set this to prevent
# old versions from overwriting new format


Expand Down Expand Up @@ -262,6 +263,7 @@ def upgrade(self):
self._convert_version_70()
self._convert_version_71()
self._convert_version_72()
self._convert_version_73()
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure

def _convert_wallet_type(self):
Expand Down Expand Up @@ -936,7 +938,7 @@ def _convert_version_45(self):
outputs = item['outputs'] if not is_lightning else None
bip70 = item['bip70'] if not is_lightning else None
if is_lightning:
lnaddr = decode_bolt11_invoice(item['invoice'])
lnaddr = decode_bolt11_invoice(item['invoice'], strict=False)
amount_msat = lnaddr.get_amount_msat()
timestamp = lnaddr.date
exp_delay = lnaddr.get_expiry()
Expand Down Expand Up @@ -997,7 +999,7 @@ def _convert_version_47(self):
for key, item in list(requests.items()):
lnaddr = item.get('lightning_invoice')
if lnaddr:
lnaddr = decode_bolt11_invoice(lnaddr)
lnaddr = decode_bolt11_invoice(lnaddr, strict=False)
rhash = lnaddr.paymenthash.hex()
if key != rhash:
requests[rhash] = item
Expand Down Expand Up @@ -1047,7 +1049,7 @@ def _convert_version_51(self):
if lightning_invoice is None:
payment_hash = None
else:
lnaddr = decode_bolt11_invoice(lightning_invoice)
lnaddr = decode_bolt11_invoice(lightning_invoice, strict=False)
payment_hash = lnaddr.paymenthash.hex()
item['payment_hash'] = payment_hash
self.data['seed_version'] = 51
Expand Down Expand Up @@ -1480,6 +1482,22 @@ def _serialize_imported_channel_backup(cb: dict) -> str:
channel_backups[channel_id] = _serialize_imported_channel_backup(storage)
self.data['seed_version'] = 72

def _convert_version_73(self):
from .bolt11 import decode_bolt11_invoice
if not self._is_upgrade_method_needed(72, 72):
return
# remove invoices not passing strict bolt11 invoice check
invoices = self.data.get('invoices', {})
for key, item in list(invoices.items()):
lnaddr = item.get('lightning_invoice')
if lnaddr:
try:
decode_bolt11_invoice(lnaddr, strict=True)
except BOLT11InvoiceException as e:
self.logger.warning(f"removing invoice {key} that fails strict bolt11 check: {e}")
del invoices[key]
self.data['seed_version'] = 73

def _convert_imported(self):
if not self._is_upgrade_method_needed(0, 13):
return
Expand Down
Loading