Merge pull request #10970 from f321x/fix_bolt11_bugs_2
What changed, and why it matters
This commit fixes several bugs in Electrum's handling of BOLT11 Lightning invoices. The most user-visible fixes are: stricter validation of invoice amounts (rejecting zero, negative, sub-millisatoshi, and leading-zero amounts), correct padding of the timestamp field so small dates don't get corrupted, preserving the 'n' (public key) tag when decoding so re-encoding doesn't silently drop it, and removing support for the non-standard 't' routing tag that could previously hide malformed data. There is also a fix to how description fields are truncated so they don't produce invalid UTF-8. These are correctness and robustness fixes rather than a single critical vulnerability, but they could have allowed malformed or ambiguous invoices to be accepted or re-encoded incorrectly.
Review and merge if not already merged; run the expanded test suite; consider whether any previously accepted malformed invoices in user wallets need the storage upgrade path already included. No immediate emergency response is indicated, but users should upgrade to a version containing this fix to avoid handling of non-conformant BOLT11 invoices.
Security signals we found
Stricter BOLT11 amount validation prevents acceptance of zero, negative, sub-millisatoshi, and leading-zero amounts
Fixed int_to_data5 padding bug that corrupted small timestamp values during invoice round-trip
Malformed 'r' routing tags now raise exceptions instead of being silently skipped
Non-standard 't' routing tag support removed, reducing attack surface
'n' pubkey tag now preserved on decode, preventing silent information loss on re-encoding
Description truncation now avoids invalid UTF-8 byte sequences
Storage upgrade conversion drops previously stored invoices with malformed route tags to prevent wallet load failures
Amount setter now rejects zero and NaN, preventing creation of invalid invoices
Evidence from the diff
The patch is a collection of BOLT11 encode/decode bugfixes in electrum/bolt11.py with corresponding test and caller updates. Key changes: (1) unshorten_amount now requires a positive integer with optional known multiplier, rejects leading zeros, zero, and sub-millisatoshi ‘p’ amounts; (2) int_to_data5 padding calculation is corrected from len(ret)-bit_len//5 to bit_len//5-len(ret), fixing timestamp field width for small values; (3) encode_bolt11_invoice checks addr.amount is not None rather than truthy, allowing zero-amount invoices to encode correctly; (4) amount setter rejects zero and NaN; (5) ‘n’ tag is now appended to addr.tags during decode so round-trips preserve it; (6) ‘t’ trampoline routing tag handling is removed entirely, malformed ‘r’ tags now raise BOLT11DecodeException instead of being silently skipped; (7) description truncation now decodes/re-encodes to avoid slicing a UTF-8 character; (8) ‘h’ tag accepts either raw 32-byte hash or description string. Tests are added/updated for all of these behaviors and a storage upgrade path drops old malformed invoices.
Changed components
electrum/bolt11.pyelectrum/gui/qml/qeinvoice.pyelectrum/gui/qt/main_window.pyelectrum/lnworker.pytests/test_bolt11.pytests/test_invoices.pytests/test_lnwallet.pytests/test_storage_upgrade.pytests/lnhelpers.pyInspect captured patch +82 / −93
### electrum/bolt11.py
@@ -49,7 +49,7 @@ def shorten_amount(amount):
unit = ''
return str(amount) + unit
-def unshorten_amount(amount) -> Decimal:
+def unshorten_amount(amount: str) -> Decimal:
""" Given a shortened amount, convert it into a decimal
"""
# BOLT #11:
@@ -65,14 +65,18 @@ def unshorten_amount(amount) -> Decimal:
'u': 10**6,
'm': 10**3,
}
- unit = str(amount)[-1]
+
# BOLT #11:
# A reader SHOULD fail if `amount` contains a non-digit, or is followed by
# anything except a `multiplier` in the table above.
- if not re.fullmatch("\\d+[pnum]?", str(amount)):
+ if not re.fullmatch(f"[1-9][0-9]*[{''.join(units)}]?", amount):
raise BOLT11DecodeException("Invalid amount '{}'".format(amount))
+ unit = amount[-1]
if unit in units.keys():
+ # if multiplier is `p` and the last decimal of `amount` is not 0: MUST fail the payment.
+ if unit == 'p' and amount[-2] != '0':
+ raise BOLT11DecodeException("Sub-millisatoshi amount '{}'".format(amount))
return Decimal(amount[:-1]) / units[unit]
else:
return Decimal(amount)
@@ -144,7 +148,7 @@ def int_to_data5(val: int, *, bit_len: int | None = None) -> Sequence[int]:
ret.append(val % 32)
val //= 32
if bit_len is not None:
- ret.extend([0] * (len(ret) - bit_len // 5))
+ ret.extend([0] * (bit_len // 5 - len(ret)))
ret.reverse()
return ret
@@ -170,7 +174,7 @@ def pull_tagged(data5: bytearray) -> Tuple[str, Sequence[int]]:
def encode_bolt11_invoice(addr: 'BOLT11Addr', privkey) -> str:
- if addr.amount:
+ if addr.amount is not None:
amount = addr.net.BOLT11_HRP + shorten_amount(addr.amount)
else:
amount = addr.net.BOLT11_HRP if addr.net else ''
@@ -210,25 +214,20 @@ def encode_bolt11_invoice(addr: 'BOLT11Addr', privkey) -> str:
route += int.to_bytes(feerate, length=4, byteorder="big", signed=False)
route += int.to_bytes(cltv, length=2, byteorder="big", signed=False)
data5 += tagged8('r', route)
- elif k == 't':
- pubkey, feebase, feerate, cltv = v
- route = bytearray()
- route += pubkey
- route += int.to_bytes(feebase, length=4, byteorder="big", signed=False)
- route += int.to_bytes(feerate, length=4, byteorder="big", signed=False)
- route += int.to_bytes(cltv, length=2, byteorder="big", signed=False)
- data5 += tagged8('t', route)
elif k == 'f':
if v is not None:
data5 += encode_fallback_addr(v, addr.net)
elif k == 'd':
- # truncate to max length: 1024*5 bits = 639 bytes
- data5 += tagged8('d', v.encode()[0:639])
+ # truncate to max length: 1024*5 bits = 639 bytes, drop trailing, sliced utf-8 char
+ data5 += tagged8('d', v.encode()[0:639].decode('utf-8', errors='ignore').encode())
elif k == 'x':
expirybits = int_to_data5(v)
data5 += tagged5('x', expirybits)
elif k == 'h':
- data5 += tagged8('h', sha256(v.encode('utf-8')).digest())
+ deschash = v if isinstance(v, bytes) else sha256(v.encode('utf-8')).digest()
+ if len(deschash) != 32:
+ raise BOLT11EncodeException(f"'h' tag must be a description or its sha256, not {v!r}")
+ data5 += tagged8('h', deschash)
elif k == 'n':
data5 += tagged8('n', v)
elif k == 'c':
@@ -277,7 +276,7 @@ def __init__(
date: Optional[int | float] = None,
payment_secret: bytes = None
):
- self.date = int(time.time()) if not date else int(date)
+ self.date = int(time.time()) if date is None else int(date)
self.tags = [] if not tags else tags
self.unknown_tags = []
self.paymenthash = paymenthash
@@ -301,7 +300,7 @@ def amount(self, value: Optional[int | Decimal]):
if isinstance(value, int):
value = Decimal(value)
assert isinstance(value, Decimal)
- if value.is_nan() or not (0 <= value <= TOTAL_COIN_SUPPLY_LIMIT_IN_BTC):
+ if value.is_nan() or not (0 < value <= TOTAL_COIN_SUPPLY_LIMIT_IN_BTC):
raise BOLT11InvoiceException(f"amount is out-of-bounds: {value!r} BTC")
if value * 10**12 % 10:
# max resolution is millisatoshi
@@ -329,9 +328,8 @@ def get_amount_sat(self) -> Optional[Decimal]:
return None
return self.amount * COIN
- def get_routing_info(self, tag):
- # note: tag will be 't' for trampoline
- r_tags = list(filter(lambda x: x[0] == tag, self.tags))
+ def get_routing_info(self):
+ r_tags = list(filter(lambda x: x[0] == 'r', self.tags))
# strip the tag type, it's implicitly 'r' now
r_tags = list(map(lambda x: x[1], r_tags))
# if there are multiple hints, we will use the first one that works,
@@ -429,7 +427,7 @@ def to_debug_json(self) -> Dict[str, Any]:
'tags': self.tags,
'unknown_tags': self.unknown_tags,
}
- if ln_routing_info := self.get_routing_info('r'):
+ if ln_routing_info := self.get_routing_info():
d['r_tags'] = self.format_bolt11_routing_info_as_human_readable(ln_routing_info)
return d
@@ -556,22 +554,6 @@ def _check_minimal_data5(tag: str, data5: Sequence[int]) -> None:
route.append((pubkey, scid, feebase, feerate, cltv))
if route:
addr.tags.append(('r',route))
- elif tag == 't':
- tagdata = _convertbits_tag(tag, tagdata, 5, 8, False)
- if not tagdata:
- continue
- route = []
- with io.BytesIO(bytes(tagdata)) as s:
- pubkey = s.read(33)
- feebase = s.read(4)
- feerate = s.read(4)
- cltv = s.read(2)
- if len(cltv) == 2: # no EOF
- feebase = int.from_bytes(feebase, byteorder="big")
- feerate = int.from_bytes(feerate, byteorder="big")
- cltv = int.from_bytes(cltv, byteorder="big")
- route.append((pubkey, feebase, feerate, cltv))
- addr.tags.append(('t', route))
elif tag == 'f':
fallback = parse_fallback_addr(tagdata, addr.net)
if fallback:
@@ -613,6 +595,7 @@ def _check_minimal_data5(tag: str, data5: Sequence[int]) -> None:
raise BOLT11DecodeException("Unexpected 'n' tag")
pubkeybytes = bytes(_convertbits_tag(tag, tagdata, 5, 8, False, length_range=(53, 53)))
addr.pubkey = pubkeybytes
+ addr.tags.append(('n', pubkeybytes))
elif tag == 'c':
# MUST use the minimum data_length possible
_check_minimal_data5(tag, tagdata)
### electrum/gui/qml/qeinvoice.py
@@ -275,7 +275,7 @@ def set_lnprops(self):
return
lnaddr = self._effectiveInvoice._lnaddr
- ln_routing_info = lnaddr.get_routing_info('r')
+ ln_routing_info = lnaddr.get_routing_info()
self._logger.debug(str(ln_routing_info))
self._lnprops = {
### electrum/gui/qt/main_window.py
@@ -1718,7 +1718,7 @@ def show_lightning_invoice(self, invoice: Invoice):
invoice_e.setText(invoice.lightning_invoice)
grid.addWidget(QLabel(_('Text') + ':'), 8, 0)
grid.addWidget(invoice_e, 8, 1)
- r_tags = lnaddr.get_routing_info('r')
+ r_tags = lnaddr.get_routing_info()
r_tags = '\n'.join(repr(r) for r in BOLT11Addr.format_bolt11_routing_info_as_human_readable(r_tags))
routing_e = QTextEdit(str(r_tags))
routing_e.setReadOnly(True)
### electrum/lnworker.py
@@ -1969,7 +1969,7 @@ async def pay_invoice(
payment_secret = lnaddr.payment_secret
invoice_pubkey = lnaddr.pubkey.serialize()
invoice_features = lnaddr.get_features()
- r_tags = lnaddr.get_routing_info('r')
+ r_tags = lnaddr.get_routing_info()
amount_to_pay = lnaddr.get_amount_msat()
status = self.get_invoice_status(invoice)
if status == PR_PAID:
### tests/lnhelpers.py
@@ -285,7 +285,7 @@ async def create_routes_from_invoice(self, amount_msat: int, decoded_invoice: BO
payment_secret=decoded_invoice.payment_secret,
initial_trampoline_fee_level=0,
invoice_features=decoded_invoice.get_features(),
- r_tags=decoded_invoice.get_routing_info('r'),
+ r_tags=decoded_invoice.get_routing_info(),
min_final_cltv_delta=decoded_invoice.get_min_final_cltv_delta(),
amount_to_pay=amount_msat,
invoice_pubkey=decoded_invoice.pubkey.serialize(),
### tests/test_bolt11.py
@@ -109,8 +109,17 @@ def test_roundtrip(self):
invoice_str2 = encode_bolt11_invoice(lnaddr1, PRIVKEY)
self.assertEqual(invoice_str1, invoice_str2)
lnaddr2 = decode_bolt11_invoice(invoice_str2, net=lnaddr1.net)
+ self.assertEqual(invoice_str1, encode_bolt11_invoice(lnaddr2, PRIVKEY))
self.compare(lnaddr1, lnaddr2)
+ def test_int_to_data5_padding(self):
+ # if bit_len is given, the result is left-padded with zeroes to exactly bit_len//5 values
+ self.assertEqual([0, 0, 0, 0, 0, 1, 8], list(int_to_data5(40, bit_len=35)))
+ self.assertEqual([1, 16, 5, 2, 1, 3, 2], list(int_to_data5(1615922274, bit_len=35)))
+ # ... so the fixed-width timestamp field stays 7 values wide and a small date roundtrips
+ lnaddr = BOLT11Addr(date=1000, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, tags=[('d', '')])
+ self.assertEqual(1000, decode_bolt11_invoice(encode_bolt11_invoice(lnaddr, PRIVKEY)).date)
+
def test_n_decoding(self):
# We flip the signature recovery bit, which would normally give a different
# pubkey.
@@ -129,6 +138,14 @@ def test_n_decoding(self):
lnaddr = decode_bolt11_invoice(bech32_encode(segwit_addr.Encoding.BECH32, hrp, data), verbose=True)
self.assertEqual(lnaddr.pubkey.serialize(), PUBKEY)
+ # the 'n' field is kept as a tag, so that re-encoding does not silently drop it
+ invoice = encode_bolt11_invoice(
+ BOLT11Addr(date=1615922274, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24,
+ tags=[('d', ''), ('n', PUBKEY), ('9', 33282)]), PRIVKEY)
+ lnaddr = decode_bolt11_invoice(invoice)
+ self.assertEqual(PUBKEY, lnaddr.get_tag('n'))
+ self.assertEqual(invoice, encode_bolt11_invoice(lnaddr, PRIVKEY))
+
@staticmethod
def _encode_invoice_with_raw_tags(tags5, *, net=None, date=1615922274, amountstr='') -> str:
"""Builds a correctly signed invoice containing exactly the given (tag, data5) fields."""
@@ -229,9 +246,7 @@ def test_tag_padding_errors(self):
('s', [0] * 51 + [1]),
('n', [0] * 52 + [1]), # data_length 53, non-zero padding bit
('r', [1]),
- ('r', [0, 1]),
- ('t', [1]),
- ('t', [0, 1])):
+ ('r', [0, 1])):
with self.subTest(tag=tag, tagdata5=tagdata5):
with self.assertRaises(BOLT11DecodeException):
decode_bolt11_invoice(self._encode_invoice_with_raw_tag(tag, tagdata5))
@@ -242,8 +257,7 @@ def test_tag_padding_errors(self):
('p', [0] * 51 + [16]),
('s', [0] * 51 + [16]),
('n', list(convertbits(PUBKEY, 8, 5))),
- ('r', [0] * 8),
- ('t', [0] * 8)):
+ ('r', [0] * 8)):
with self.subTest(tag=tag):
decode_bolt11_invoice(self._encode_invoice_with_raw_tag(tag, tagdata5))
@@ -255,20 +269,15 @@ def test_tag_padding_errors(self):
decode_bolt11_invoice(
self._encode_invoice_with_raw_tag(tag, [0] * wrong_length))
- # 'r' and 't': an empty payload converts to b'' instead of failing, so it is skipped
- for tag in ('r', 't'):
- 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)
+ # 'r': an empty payload converts to b'' instead of failing, so it is skipped
+ lnaddr = decode_bolt11_invoice(self._encode_invoice_with_raw_tag('r', []))
+ self.assertIsNone(lnaddr.get_tag('r'))
+ self.assertEqual([], lnaddr.unknown_tags)
# 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)))
+ invoice = self._encode_invoice_with_raw_tag('r', list(convertbits(r_hop, 8, 5)))
+ self.assertEqual(1, len(decode_bolt11_invoice(invoice).get_routing_info()))
def test_invalid_signature(self):
# The trailing 65 bytes of an invoice are attacker-controlled: every way the ecc lib
@@ -450,9 +459,15 @@ def test_invalid_amount(self):
self.assertEqual( # control
Decimal('0.0025'),
decode_bolt11_invoice(self._encode_invoice_with_raw_tags(tags5, amountstr='2500u')).amount)
+ self.assertEqual( # control: 'p' is allowed as long as the amount is a whole msat
+ Decimal('0.00000000001'),
+ decode_bolt11_invoice(self._encode_invoice_with_raw_tags(tags5, amountstr='10p')).amount)
for amountstr in ('21000001', # more than the total coin supply
'1p', # sub-millisatoshi precision
'25y', # invalid multiplier
+ '0', # must be positive; an absent amount means "any amount"
+ '0u',
+ '025u', # no leading zeroes
'-1',
'nan',
'1e3'):
@@ -467,6 +482,7 @@ def test_amount_validation(self):
("bytes", b'1'),
("NaN", Decimal('nan')),
("negative", Decimal(-1)),
+ ("zero", Decimal(0)),
("more than the coin supply", Decimal(21_000_001)),
("sub-millisatoshi", Decimal('0.0000000000001'))):
with self.subTest(label):
### tests/test_invoices.py
@@ -258,34 +258,28 @@ async def test_arg_validation(self):
invoice.exp = "asd"
async def test_malformed_route_tag_is_rejected(self):
- # A bolt11 invoice with a malformed 'r'/'t' tag used to decode fine (the tag was silently
+ # A bolt11 invoice with a malformed 'r' tag used to decode fine (the tag was silently
# skipped). It is now rejected, both when it arrives from outside and when it comes off
# disk: the attrs validator decodes strictly. What keeps that from making an old wallet
# file unloadable is db conversion 73, which purges such invoices; see
# TestStorageUpgrade.test_upgrade_removes_invoice_with_malformed_route_tag.
- # Both strings below are correctly signed testnet invoices whose 'r'/'t' payload has
+ # The string below is a correctly signed testnet invoice whose 'r' payload has
# non-zero padding bits; see TestBolt11._encode_invoice_with_raw_tag.
- for tag, invoice_str in (
- ('r', 'lntb1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq8w3jhxaqrqzq'
- 'pxhlj48td8uen6qqvke0kwsx0uf3g9pqfg3sdetumr2lla597ahcjcqcn5v7yycysc39ua9r2l8qx527'
- 'uthxfdgmhp47exeh98pv7facqmjed87'),
- ('t', 'lntb1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq8w3jhxaqtqzq'
- 'pg3tvdu05w4rd9ccwjq80f5ujz89c5ltq5fhp8dqxg7aan38gs24z0pgx8xj4vvzt2su5fqpr35tz692'
- 'czrwt6e56twh3v8l0t8hfkxsq5xtyfu'),
- ):
- with self.subTest(tag=tag):
- with self.assertRaises(BOLT11DecodeException):
- Invoice(
- amount_msat=None,
- message="mymsg",
- time=1615922274,
- exp=LN_EXPIRY_NEVER,
- outputs=None,
- height=0,
- lightning_invoice=invoice_str,
- )
- with self.assertRaises(InvoiceError):
- Invoice.from_bech32(invoice_str)
+ invoice_str = ('lntb1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq8w3jhxaqrqzq'
+ 'pxhlj48td8uen6qqvke0kwsx0uf3g9pqfg3sdetumr2lla597ahcjcqcn5v7yycysc39ua9r2l8qx527'
+ 'uthxfdgmhp47exeh98pv7facqmjed87')
+ with self.assertRaisesRegex(BOLT11DecodeException, "Failed to decode tag 'r'"):
+ Invoice(
+ amount_msat=None,
+ message="mymsg",
+ time=1615922274,
+ exp=LN_EXPIRY_NEVER,
+ outputs=None,
+ height=0,
+ lightning_invoice=invoice_str,
+ )
+ with self.assertRaisesRegex(InvoiceError, "Failed to decode tag 'r'"):
+ Invoice.from_bech32(invoice_str)
class TestOutgoingInvoicesPaidCache(ElectrumTestCase):
### tests/test_lnwallet.py
@@ -160,7 +160,7 @@ async def test_trampoline_invoice_features_and_routing_hints(self):
)
lnaddr, _ = wallet.get_bolt11_invoice(payment_info=pi, message='test', fallback_address=None)
- hint_node_ids = {route[0][0] for route in lnaddr.get_routing_info('r')}
+ hint_node_ids = {route[0][0] for route in lnaddr.get_routing_info()}
self.assertEqual(hint_node_ids, {trampoline_pubkey, regular_pubkey})
# trampoline feature should not be set if we use trampoline but one peer is not a trampoline
@@ -176,7 +176,7 @@ async def test_trampoline_invoice_features_and_routing_hints(self):
wallet.clear_invoices_cache()
lnaddr, _ = wallet.get_bolt11_invoice(payment_info=pi, message='test', fallback_address=None)
- hint_node_ids = {route[0][0] for route in lnaddr.get_routing_info('r')}
+ hint_node_ids = {route[0][0] for route in lnaddr.get_routing_info()}
self.assertEqual(hint_node_ids, {trampoline_pubkey, regular_pubkey})
wallet.uses_trampoline = old_check
@@ -198,14 +198,14 @@ async def test_trampoline_invoice_features_and_routing_hints(self):
wallet.clear_invoices_cache()
lnaddr2, _ = wallet.get_bolt11_invoice(payment_info=pi2, message='test', fallback_address=None)
- hint_node_ids2 = {route[0][0] for route in lnaddr2.get_routing_info('r')}
+ hint_node_ids2 = {route[0][0] for route in lnaddr2.get_routing_info()}
self.assertEqual(hint_node_ids2, {trampoline_pubkey, regular_pubkey})
# assert only trampoline peers are included in r_tags if the invoice_features signal trampoline
del electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS['regular_peer']
wallet.clear_invoices_cache()
lnaddr3, _ = wallet.get_bolt11_invoice(payment_info=pi2, message='test', fallback_address=None)
- hint_node_ids3 = {route[0][0] for route in lnaddr3.get_routing_info('r')}
+ hint_node_ids3 = {route[0][0] for route in lnaddr3.get_routing_info()}
self.assertEqual(hint_node_ids3, {trampoline_pubkey})
async def test_open_channel_just_in_time_success(self):
### tests/test_storage_upgrade.py
@@ -344,19 +344,16 @@ async def test_upgrade_from_client_4_8_1_9dk_with_ln_chan_backups(self):
@as_testnet
async def test_upgrade_removes_invoice_with_malformed_route_tag(self):
# 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
+ # Older versions decoded a malformed 'r' 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
+ # The malformed invoice below is correctly signed, but its 'r' payload has
# non-zero padding bits; see TestBolt11._encode_invoice_with_raw_tag.
bad_r = ('lntb1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq8w3jhxaqrqzq'
'pxhlj48td8uen6qqvke0kwsx0uf3g9pqfg3sdetumr2lla597ahcjcqcn5v7yycysc39ua9r2l8qx527'
'uthxfdgmhp47exeh98pv7facqmjed87')
- bad_t = ('lntb1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq8w3jhxaqtqzq'
- 'pg3tvdu05w4rd9ccwjq80f5ujz89c5ltq5fhp8dqxg7aan38gs24z0pgx8xj4vvzt2su5fqpr35tz692'
- 'czrwt6e56twh3v8l0t8hfkxsq5xtyfu')
good = ('lntb15u1p0m6lzupp5zqjthgvaad9mewmdjuehwddyze9d8zyxcc43zhaddeegt37sndgsdq4xysyymr0vd'
'4kzcmrd9hx7cqp7xqrrss9qy9qsqsp5vlhcs24hwm747w8f3uau2tlrdkvjaglffnsstwyamj84cxuhrn2'
's8tut3jqumepu42azyyjpgqa4w9w03204zp9h4clk499y2umstl6s29hqyj8vv4as6zt5567ux7l3f66m8'
@@ -372,7 +369,6 @@ def invoice_json(lightning_invoice):
'wallet_type': 'imported',
'addresses': {'tb1qmjzmg8nd4z56ar4fpngzsr6euktrhnjg9td385': {}},
'invoices': {'bad_r': invoice_json(bad_r),
- 'bad_t': invoice_json(bad_t),
'good': invoice_json(good)},
}
db = self._load_db_from_json_string(wallet_json=json.dumps(data), upgrade=True)
@@ -382,13 +378,13 @@ def invoice_json(lightning_invoice):
# sanity: without the conversion (i.e. already at seed_version 73) the same file
# would not load at all
data['seed_version'] = 73
- with self.assertRaises(BOLT11DecodeException):
+ with self.assertRaisesRegex(BOLT11DecodeException, "Failed to decode tag 'r'"):
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))}
+ for key, invoice_str in (('bad_r', bad_r), ('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()))Why this scored 60/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.