wallet_db: convert PaymentInfo amounts from 0 to None
What changed, and why it matters
This commit fixes a wallet-crash bug, not a security vulnerability. Some older Electrum wallets created 'zero-amount' Lightning payment requests in 2023 that were stored with an amount of 0 instead of the special 'no amount' marker (None). A later code change added a rule that received payment amounts cannot be 0, causing those older wallets to crash or fail to open. The patch converts the stored 0 values back to None during wallet database upgrade and prevents new 0 values from being created.
No security action required; this is a bug-fix/data-migration patch. Users with affected wallets should upgrade Electrum to a version containing this commit so the database migration runs and restores consistency.
Security signals we found
Data-consistency hardening: converts legacy 0 amounts to None to satisfy new invariant
Defensive validation added: ValueError on creation of amount_msat==0 payment info
Database upgrade bump from seed version 68 to 69
Evidence from the diff
The change adds a database upgrade routine (_convert_version_69) that iterates over stored lightning_payments entries and replaces amount_msat==0 with None for RECEIVED direction payments. It also strengthens PaymentInfo.validate() to assert amount_msat != 0 for received payments, makes LNWallet.create_payment_info() raise ValueError on amount_msat==0, and updates tests accordingly. The root cause is an inconsistency introduced around PR #8659 where zero-amount payment requests were persisted as 0 rather than None, and a later assert (commit 286fc4b86e4d23cb9af15b9061b3d709e7592bcb) now fails for affected wallets.
Changed components
electrum/wallet_db.pyelectrum/lnworker.pyelectrum/wallet.pytests/test_lnwallet.pyInspect captured patch +37 / −4
diff --git a/electrum/lnworker.py b/electrum/lnworker.py
index 633bf2b..9727eca 100644
--- a/electrum/lnworker.py
+++ b/electrum/lnworker.py
@@ -143,8 +143,10 @@ class PaymentInfo:
def validate(self):
assert isinstance(self.payment_hash, bytes) and len(self.payment_hash) == 32
- assert self.amount_msat is None or isinstance(self.amount_msat, int)
assert isinstance(self.direction, int)
+ assert self.amount_msat is None or isinstance(self.amount_msat, int)
+ if self.direction == RECEIVED:
+ assert self.amount_msat != 0 # use amount_msat=None instead!
assert isinstance(self.status, int)
assert isinstance(self.min_final_cltv_delta, int)
assert isinstance(self.expiry_delay, int) and self.expiry_delay > 0, repr(self.expiry_delay)
@@ -2611,6 +2613,8 @@ class LNWallet(Logger):
exp_delay: int = LN_EXPIRY_NEVER,
write_to_disk=True
) -> bytes:
+ if amount_msat == 0:
+ raise ValueError("amount_msat must not be 0. Use None instead.")
payment_preimage = os.urandom(32)
payment_hash = sha256(payment_preimage)
min_final_cltv_delta = min_final_cltv_delta or MIN_FINAL_CLTV_DELTA_ACCEPTED
diff --git a/electrum/wallet.py b/electrum/wallet.py
index cb8c2db..ae116e9 100644
--- a/electrum/wallet.py
+++ b/electrum/wallet.py
@@ -3020,7 +3020,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
amount_msat = req.get_amount_msat() or None
assert (amount_msat is None or amount_msat > 0), amount_msat
info = self.lnworker.get_payment_info(payment_hash, direction=RECEIVED)
- assert info.amount_msat == amount_msat, f"{info.amount_msat=} != {amount_msat=}"
+ assert info.amount_msat == amount_msat, f"{info.amount_msat=} != {amount_msat=}" # info.amount_msat or None
lnaddr, invoice = self.lnworker.get_bolt11_invoice(
payment_info=info,
message=req.message,
diff --git a/electrum/wallet_db.py b/electrum/wallet_db.py
index 7f0c893..1ba3dba 100644
--- a/electrum/wallet_db.py
+++ b/electrum/wallet_db.py
@@ -69,7 +69,7 @@ class WalletUnfinished(WalletFileException):
# 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 = 68 # electrum >= 2.7 will set this to prevent
+FINAL_SEED_VERSION = 69 # electrum >= 2.7 will set this to prevent
# old versions from overwriting new format
@@ -243,6 +243,7 @@ class WalletDBUpgrader(Logger):
self._convert_version_66()
self._convert_version_67()
self._convert_version_68()
+ self._convert_version_69()
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
def _convert_wallet_type(self):
@@ -1365,6 +1366,25 @@ class WalletDBUpgrader(Logger):
self.data['lightning_preimages'] = new_preimages
self.data['seed_version'] = 68
+ def _convert_version_69(self):
+ """Convert PaymentInfo amounts from 0 to None"""
+ if not self._is_upgrade_method_needed(68, 68):
+ return
+ new_payment_infos = {}
+ old_payment_infos = self.data.get('lightning_payments', {})
+ for key, old_v in old_payment_infos.items():
+ #amount_msat, status, min_final_cltv_delta, expiry_delay, creation_ts, invoice_features = old_v
+ amount_msat = old_v[0]
+ rhash, direction = key.split(":") # key is "RHASH:direction"
+ direction = int(direction)
+ if direction == 1: # RECEIVED
+ if amount_msat == 0:
+ amount_msat = None
+ new_v = (amount_msat, *old_v[1:])
+ new_payment_infos[key] = new_v
+ self.data['lightning_payments'] = new_payment_infos
+ self.data['seed_version'] = 69
+
def _convert_imported(self):
if not self._is_upgrade_method_needed(0, 13):
return
diff --git a/tests/test_lnwallet.py b/tests/test_lnwallet.py
index 6a856df..f50d0d6 100644
--- a/tests/test_lnwallet.py
+++ b/tests/test_lnwallet.py
@@ -24,7 +24,6 @@ class TestLNWallet(ElectrumTestCase):
wallet = self.lnwallet_anchors
tests = (
(100_000, 200, 100),
- (0, 200, 100),
(None, 200, 100),
(None, None, LN_EXPIRY_NEVER),
(100_000, None, 0),
@@ -43,3 +42,13 @@ class TestLNWallet(ElectrumTestCase):
self.assertEqual(pi.db_key, f"{payment_hash.hex()}:{int(pi.direction)}")
self.assertEqual(pi.status, PR_UNPAID)
self.assertIsNone(wallet.get_payment_info(os.urandom(32), direction=RECEIVED))
+
+ def test_create_payment_info__amount_must_not_be_zero(self):
+ wallet = self.lnwallet_anchors
+ amount_msat, min_final_cltv_delta, exp_delay = (0, 200, 100)
+ with self.assertRaises(ValueError):
+ wallet.create_payment_info(
+ amount_msat=amount_msat,
+ min_final_cltv_delta=min_final_cltv_delta,
+ exp_delay=exp_delay,
+ )
Why this scored 22/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.