lnworker: enforce creation of PaymentInfo for b11
What changed, and why it matters
This commit tightens how Electrum creates Lightning invoices so the stored payment record and the generated invoice always match. Previously, callers passed invoice details separately, creating a risk that the invoice could differ from what Electrum later used to validate incoming payments. Now a single PaymentInfo object is required, and it stores extra fields (expiry and minimum locktime) so validation can rely on it. This is a defensive hardening change, not a fix for a known active exploit.
Review as a hardening improvement. Ensure the database migration is backward-compatible and that all new PaymentInfo fields are correctly populated by third-party plugins or scripts using the API. No urgent patch deployment is indicated, but downstream integrators should adapt to the new get_bolt11_invoice() signature.
Security signals we found
Defensive refactoring to eliminate parameter-drift between invoice generation and HTLC validation
Database schema upgrade for PaymentInfo with new expiry and cltv fields
Constant rename/removal of MIN_FINAL_CLTV_DELTA_FOR_INVOICE in favor of MIN_FINAL_CLTV_DELTA_ACCEPTED + buffer
Hold invoices and submarine swaps now create PaymentInfo before generating bolt11
No explicit vulnerability, CVE, or bug identifier mentioned in commit
Evidence from the diff
The patch refactors LNWallet.get_bolt11_invoice() to take a PaymentInfo dataclass instead of loose payment_hash/amount_msat/expiry/min_final_cltv_expiry_delta arguments. PaymentInfo is extended with min_final_cltv_delta, expiry_delay, and creation_ts, and a database upgrade (version 60→61) migrates existing records. All callers (commands, GUI, NWC plugin, submarine swaps, wallet) are updated to create or fetch PaymentInfo first. A buffer constant MIN_FINAL_CLTV_DELTA_BUFFER_INVOICE is added so the invoice’s min_final_cltv_delta is the stored accepted value plus 3 blocks. The change aims to prevent invoice parameters from diverging from the persisted PaymentInfo, which is used to validate incoming HTLCs.
Changed components
electrum/lnworker.pyelectrum/lnutil.pyelectrum/wallet_db.pyelectrum/wallet.pyelectrum/commands.pyelectrum/gui/qt/main_window.pyelectrum/plugins/nwc/nwcserver.pyelectrum/submarine_swaps.pytests/test_lnpeer.pyInspect captured patch +133 / −79
diff --git a/electrum/commands.py b/electrum/commands.py
index 09bc31a..c661698 100644
--- a/electrum/commands.py
+++ b/electrum/commands.py
@@ -68,7 +68,7 @@ from .wallet import (
)
from .address_synchronizer import TX_HEIGHT_LOCAL
from .mnemonic import Mnemonic
-from .lnutil import (channel_id_from_funding_tx, LnFeatures, SENT, MIN_FINAL_CLTV_DELTA_FOR_INVOICE,
+from .lnutil import (channel_id_from_funding_tx, LnFeatures, SENT, MIN_FINAL_CLTV_DELTA_ACCEPTED,
PaymentFeeBudget, NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE)
from .plugin import run_hook, DeviceMgr, Plugins
from .version import ELECTRUM_VERSION
@@ -1382,7 +1382,7 @@ class Commands(Logger):
amount: Optional[Decimal] = None,
memo: str = "",
expiry: int = 3600,
- min_final_cltv_expiry_delta: int = MIN_FINAL_CLTV_DELTA_FOR_INVOICE * 2,
+ min_final_cltv_expiry_delta: int = MIN_FINAL_CLTV_DELTA_ACCEPTED * 2,
wallet: Abstract_Wallet = None
) -> dict:
"""
@@ -1399,24 +1399,24 @@ class Commands(Logger):
assert payment_hash not in wallet.lnworker.payment_info, "Payment hash already used!"
assert payment_hash not in wallet.lnworker.dont_settle_htlcs, "Payment hash already used!"
assert wallet.lnworker.get_preimage(bfh(payment_hash)) is None, "Already got a preimage for this payment hash!"
- assert MIN_FINAL_CLTV_DELTA_FOR_INVOICE < min_final_cltv_expiry_delta < 576, "Use a sane min_final_cltv_expiry_delta value"
+ assert MIN_FINAL_CLTV_DELTA_ACCEPTED < min_final_cltv_expiry_delta < 576, "Use a sane min_final_cltv_expiry_delta value"
amount = amount if amount and satoshis(amount) > 0 else None # make amount either >0 or None
inbound_capacity = wallet.lnworker.num_sats_can_receive()
assert inbound_capacity > satoshis(amount or 0), \
f"Not enough inbound capacity [{inbound_capacity} sat] to receive this payment"
+ wallet.lnworker.add_payment_info_for_hold_invoice(
+ bfh(payment_hash),
+ lightning_amount_sat=satoshis(amount) if amount else None,
+ min_final_cltv_delta=min_final_cltv_expiry_delta,
+ exp_delay=expiry,
+ )
+ info = wallet.lnworker.get_payment_info(bfh(payment_hash))
lnaddr, invoice = wallet.lnworker.get_bolt11_invoice(
- payment_hash=bfh(payment_hash),
- amount_msat=satoshis(amount) * 1000 if amount else None,
+ payment_info=info,
message=memo,
- expiry=expiry,
- min_final_cltv_expiry_delta=min_final_cltv_expiry_delta,
fallback_address=None
)
- wallet.lnworker.add_payment_info_for_hold_invoice(
- bfh(payment_hash),
- satoshis(amount) if amount else None,
- )
wallet.lnworker.dont_settle_htlcs[payment_hash] = None
wallet.set_label(payment_hash, memo)
result = {
diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
index 9fbbcb5..31f0b84 100644
--- a/electrum/gui/qt/main_window.py
+++ b/electrum/gui/qt/main_window.py
@@ -2758,7 +2758,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
self.closing_warning_callbacks.append(warning_callback)
def _check_ongoing_force_closures(self) -> Optional[str]:
- from electrum.lnutil import MIN_FINAL_CLTV_DELTA_FOR_INVOICE
+ from electrum.lnutil import MIN_FINAL_CLTV_DELTA_ACCEPTED
if not self.wallet.has_lightning():
return None
if not self.network:
@@ -2767,7 +2767,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
if not force_closes:
return
# fixme: this is inaccurate, we need local_height - cltv_of_htlc
- cltv_delta = MIN_FINAL_CLTV_DELTA_FOR_INVOICE
+ cltv_delta = MIN_FINAL_CLTV_DELTA_ACCEPTED
msg = '\n\n'.join([
_("Pending channel force-close"),
messages.MSG_FORCE_CLOSE_WARNING.format(cltv_delta),
diff --git a/electrum/lnutil.py b/electrum/lnutil.py
index 025cc9a..c21d927 100644
--- a/electrum/lnutil.py
+++ b/electrum/lnutil.py
@@ -505,9 +505,10 @@ MIN_FUNDING_SAT = 200_000
# the minimum cltv_expiry accepted for newly received HTLCs
# note: when changing, consider Blockchain.is_tip_stale()
MIN_FINAL_CLTV_DELTA_ACCEPTED = 144
-# set it a tiny bit higher for invoices as blocks could get mined
-# during forward path of payment
-MIN_FINAL_CLTV_DELTA_FOR_INVOICE = MIN_FINAL_CLTV_DELTA_ACCEPTED + 3
+
+# buffer added to min_final_cltv_delta of created bolt11 invoices to make verifying the cltv delta
+# of incoming payment htlcs reliable even if some blocks have been mined during forwarding
+MIN_FINAL_CLTV_DELTA_BUFFER_INVOICE = 3
# the deadline for offered HTLCs:
# the deadline after which the channel has to be failed and timed out on-chain
diff --git a/electrum/lnworker.py b/electrum/lnworker.py
index a4f6678..39f2013 100644
--- a/electrum/lnworker.py
+++ b/electrum/lnworker.py
@@ -65,11 +65,11 @@ from .lnchannel import Channel, AbstractChannel, ChannelState, PeerState, HTLCWi
from .lnrater import LNRater
from .lnutil import (
get_compressed_pubkey_from_bech32, serialize_htlc_key, deserialize_htlc_key, PaymentFailure, generate_keypair,
- LnKeyFamily, LOCAL, REMOTE, MIN_FINAL_CLTV_DELTA_FOR_INVOICE, SENT, RECEIVED, HTLCOwner, UpdateAddHtlc, LnFeatures,
+ LnKeyFamily, LOCAL, REMOTE, MIN_FINAL_CLTV_DELTA_ACCEPTED, SENT, RECEIVED, HTLCOwner, UpdateAddHtlc, LnFeatures,
ShortChannelID, HtlcLog, NoPathFound, InvalidGossipMsg, FeeBudgetExceeded, ImportedChannelBackupStorage,
OnchainChannelBackupStorage, ln_compare_features, IncompatibleLightningFeatures, PaymentFeeBudget,
NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE, GossipForwardingMessage, MIN_FUNDING_SAT,
- RecvMPPResolution, ReceivedMPPStatus,
+ MIN_FINAL_CLTV_DELTA_BUFFER_INVOICE, ReceivedMPPStatus, RecvMPPResolution,
)
from .lnonion import (
decode_onion_error, OnionFailureCode, OnionRoutingFailure, OnionPacket,
@@ -119,12 +119,22 @@ class PaymentInfo:
amount_msat: Optional[int]
direction: int
status: int
+ min_final_cltv_delta: int
+ expiry_delay: int
+ creation_ts: int = dataclasses.field(default_factory=lambda: int(time.time()))
+
+ @property
+ def expiration_ts(self):
+ return self.creation_ts + self.expiry_delay
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 isinstance(self.status, int)
+ assert isinstance(self.min_final_cltv_delta, int)
+ assert isinstance(self.expiry_delay, int) and self.expiry_delay > 0
+ assert isinstance(self.creation_ts, int)
def __post_init__(self):
self.validate()
@@ -864,7 +874,8 @@ class LNWallet(LNWorker):
LNWorker.__init__(self, self.node_keypair, features, config=self.config)
self.lnwatcher = LNWatcher(self)
self.lnrater: LNRater = None
- self.payment_info = self.db.get_dict('lightning_payments') # RHASH -> amount, direction, is_paid
+ # lightning_payments: RHASH -> amount_msat, direction, status, min_final_cltv_delta, expiry_delay, creation_ts
+ self.payment_info = self.db.get_dict('lightning_payments') # type: dict[str, Tuple[Optional[int], int, int, int, int, int]]
self._preimages = self.db.get_dict('lightning_preimages') # RHASH -> preimage
self._bolt11_cache = {}
# note: this sweep_address is only used as fallback; as it might result in address-reuse
@@ -1567,6 +1578,8 @@ class LNWallet(LNWorker):
amount_msat=amount_to_pay,
direction=SENT,
status=PR_UNPAID,
+ min_final_cltv_delta=min_final_cltv_delta,
+ expiry_delay=LN_EXPIRY_NEVER,
)
self.save_payment_info(info)
self.wallet.set_label(key, lnaddr.get_description())
@@ -2238,17 +2251,13 @@ class LNWallet(LNWorker):
def get_bolt11_invoice(
self, *,
- payment_hash: bytes,
- amount_msat: Optional[int],
+ payment_info: PaymentInfo,
message: str,
- expiry: int, # expiration of invoice (in seconds, relative)
fallback_address: Optional[str],
channels: Optional[Sequence[Channel]] = None,
- min_final_cltv_expiry_delta: Optional[int] = None,
) -> Tuple[LnAddr, str]:
- assert isinstance(payment_hash, bytes), f"expected bytes, but got {type(payment_hash)}"
-
- pair = self._bolt11_cache.get(payment_hash)
+ amount_msat = payment_info.amount_msat
+ pair = self._bolt11_cache.get(payment_info.payment_hash)
if pair:
lnaddr, invoice = pair
assert lnaddr.get_amount_msat() == amount_msat
@@ -2265,19 +2274,16 @@ class LNWallet(LNWorker):
if needs_jit:
# jit only works with single htlcs, mpp will cause LSP to open channels for each htlc
invoice_features &= ~ LnFeatures.BASIC_MPP_OPT & ~ LnFeatures.BASIC_MPP_REQ
- payment_secret = self.get_payment_secret(payment_hash)
+ payment_secret = self.get_payment_secret(payment_info.payment_hash)
amount_btc = amount_msat/Decimal(COIN*1000) if amount_msat else None
- if expiry == 0:
- expiry = LN_EXPIRY_NEVER
- if min_final_cltv_expiry_delta is None:
- min_final_cltv_expiry_delta = MIN_FINAL_CLTV_DELTA_FOR_INVOICE
+ min_final_cltv_delta = payment_info.min_final_cltv_delta + MIN_FINAL_CLTV_DELTA_BUFFER_INVOICE
lnaddr = LnAddr(
- paymenthash=payment_hash,
+ paymenthash=payment_info.payment_hash,
amount=amount_btc,
tags=[
('d', message),
- ('c', min_final_cltv_expiry_delta),
- ('x', expiry),
+ ('c', min_final_cltv_delta),
+ ('x', payment_info.expiry_delay),
('9', invoice_features),
('f', fallback_address),
] + routing_hints,
@@ -2285,7 +2291,7 @@ class LNWallet(LNWorker):
payment_secret=payment_secret)
invoice = lnencode(lnaddr, self.node_keypair.privkey)
pair = lnaddr, invoice
- self._bolt11_cache[payment_hash] = pair
+ self._bolt11_cache[payment_info.payment_hash] = pair
return pair
def get_payment_secret(self, payment_hash):
@@ -2299,14 +2305,23 @@ class LNWallet(LNWorker):
payment_secret = self.get_payment_secret(payment_hash)
return payment_hash + payment_secret
- def create_payment_info(self, *, amount_msat: Optional[int], write_to_disk=True) -> bytes:
+ def create_payment_info(
+ self, *,
+ amount_msat: Optional[int],
+ min_final_cltv_delta: Optional[int] = None,
+ exp_delay: int = LN_EXPIRY_NEVER,
+ write_to_disk=True
+ ) -> bytes:
payment_preimage = os.urandom(32)
payment_hash = sha256(payment_preimage)
+ min_final_cltv_delta = min_final_cltv_delta or MIN_FINAL_CLTV_DELTA_ACCEPTED
info = PaymentInfo(
payment_hash=payment_hash,
amount_msat=amount_msat,
direction=RECEIVED,
status=PR_UNPAID,
+ min_final_cltv_delta=min_final_cltv_delta,
+ expiry_delay=exp_delay
)
self.save_preimage(payment_hash, payment_preimage, write_to_disk=False)
self.save_payment_info(info, write_to_disk=False)
@@ -2380,22 +2395,34 @@ class LNWallet(LNWorker):
key = payment_hash.hex()
with self.lock:
if key in self.payment_info:
- amount_msat, direction, status = self.payment_info[key]
+ stored_tuple = self.payment_info[key]
+ amount_msat, direction, status, min_final_cltv_delta, expiry_delay, creation_ts = stored_tuple
return PaymentInfo(
payment_hash=payment_hash,
amount_msat=amount_msat,
direction=direction,
status=status,
+ min_final_cltv_delta=min_final_cltv_delta,
+ expiry_delay=expiry_delay,
+ creation_ts=creation_ts,
)
return None
- def add_payment_info_for_hold_invoice(self, payment_hash: bytes, lightning_amount_sat: Optional[int]):
+ def add_payment_info_for_hold_invoice(
+ self,
+ payment_hash: bytes, *,
+ lightning_amount_sat: Optional[int],
+ min_final_cltv_delta: int,
+ exp_delay: int,
+ ):
amount = lightning_amount_sat * 1000 if lightning_amount_sat else None
info = PaymentInfo(
payment_hash=payment_hash,
amount_msat=amount,
direction=RECEIVED,
status=PR_UNPAID,
+ min_final_cltv_delta=min_final_cltv_delta,
+ expiry_delay=exp_delay,
)
self.save_payment_info(info, write_to_disk=False)
@@ -2411,9 +2438,12 @@ class LNWallet(LNWorker):
if old_info := self.get_payment_info(payment_hash=info.payment_hash):
if info == old_info:
return # already saved
+ if info.direction == SENT:
+ # allow saving of newer PaymentInfo if it is a sending attempt
+ old_info = dataclasses.replace(old_info, creation_ts=info.creation_ts)
if info != dataclasses.replace(old_info, status=info.status):
# differs more than in status. let's fail
- raise Exception("payment_hash already in use")
+ raise Exception(f"payment_hash already in use: {info=} != {old_info=}")
key = info.payment_hash.hex()
self.payment_info[key] = dataclasses.astuple(info)[1:] # drop the payment hash at index 0
if write_to_disk:
@@ -3031,12 +3061,14 @@ class LNWallet(LNWorker):
raise Exception('Rebalance requires two different channels')
if self.uses_trampoline() and chan1.node_id == chan2.node_id:
raise Exception('Rebalance requires channels from different trampolines')
- payment_hash = self.create_payment_info(amount_msat=amount_msat)
- lnaddr, invoice = self.get_bolt11_invoice(
- payment_hash=payment_hash,
+ payment_hash = self.create_payment_info(
amount_msat=amount_msat,
+ exp_delay=3600,
+ )
+ info = self.get_payment_info(payment_hash)
+ lnaddr, invoice = self.get_bolt11_invoice(
+ payment_info=info,
message='rebalance',
- expiry=3600,
fallback_address=None,
channels=[chan2],
)
diff --git a/electrum/plugins/nwc/nwcserver.py b/electrum/plugins/nwc/nwcserver.py
index 64b16a5..c2cd925 100644
--- a/electrum/plugins/nwc/nwcserver.py
+++ b/electrum/plugins/nwc/nwcserver.py
@@ -480,12 +480,11 @@ class NWCServer(Logger, EventListener):
address=None
)
req: Request = self.wallet.get_request(key)
+ info = self.wallet.lnworker.get_payment_info(req.payment_hash)
try:
lnaddr, b11 = self.wallet.lnworker.get_bolt11_invoice(
- payment_hash=req.payment_hash,
- amount_msat=amount_msat,
+ payment_info=info,
message=description,
- expiry=expiry,
fallback_address=None
)
except Exception:
@@ -538,11 +537,10 @@ class NWCServer(Logger, EventListener):
b11 = invoice.lightning_invoice
elif self.wallet.get_request(invoice.rhash):
direction = "incoming"
+ info = self.wallet.lnworker.get_payment_info(invoice.payment_hash)
_, b11 = self.wallet.lnworker.get_bolt11_invoice(
- payment_hash=bytes.fromhex(invoice.rhash),
- amount_msat=invoice.amount_msat,
+ payment_info=info,
message=invoice.message,
- expiry=invoice.exp,
fallback_address=None
)
@@ -749,11 +747,10 @@ class NWCServer(Logger, EventListener):
request: Optional[Request] = self.wallet.get_request(key)
if not request or not request.is_lightning() or not status == PR_PAID:
return
+ info = self.wallet.lnworker.get_payment_info(request.payment_hash)
_, b11 = self.wallet.lnworker.get_bolt11_invoice(
- payment_hash=request.payment_hash,
- amount_msat=request.get_amount_msat(),
+ payment_info=info,
message=request.message,
- expiry=request.exp,
fallback_address=None
)
diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py
index 99468eb..b0b6230 100644
--- a/electrum/submarine_swaps.py
+++ b/electrum/submarine_swaps.py
@@ -36,7 +36,8 @@ from .util import (
run_sync_function_on_asyncio_thread, trigger_callback, NoDynamicFeeEstimates, UserFacingException,
)
from . import lnutil
-from .lnutil import hex_to_bytes, REDEEM_AFTER_DOUBLE_SPENT_DELAY, Keypair
+from .lnutil import (hex_to_bytes, REDEEM_AFTER_DOUBLE_SPENT_DELAY, Keypair,
+ MIN_FINAL_CLTV_DELTA_ACCEPTED)
from .lnaddr import lndecode
from .json_db import StoredObject, stored_in
from . import constants
@@ -66,7 +67,6 @@ MAX_LOCKTIME_DELTA = 100
MIN_FINAL_CLTV_DELTA_FOR_CLIENT = 3 * 144 # note: put in invoice, but is not enforced by receiver in lnpeer.py
assert MIN_LOCKTIME_DELTA <= LOCKTIME_DELTA_REFUND <= MAX_LOCKTIME_DELTA
assert MAX_LOCKTIME_DELTA < lnutil.MIN_FINAL_CLTV_DELTA_ACCEPTED
-assert MAX_LOCKTIME_DELTA < lnutil.MIN_FINAL_CLTV_DELTA_FOR_INVOICE
assert MAX_LOCKTIME_DELTA < MIN_FINAL_CLTV_DELTA_FOR_CLIENT
@@ -645,33 +645,38 @@ class SwapManager(Logger):
else:
invoice_amount_sat = lightning_amount_sat
+ # add payment info to lnworker
+ self.lnworker.add_payment_info_for_hold_invoice(
+ payment_hash,
+ lightning_amount_sat=invoice_amount_sat,
+ min_final_cltv_delta=min_final_cltv_expiry_delta or MIN_FINAL_CLTV_DELTA_ACCEPTED,
+ exp_delay=300,
+ )
+ info = self.lnworker.get_payment_info(payment_hash)
lnaddr1, invoice = self.lnworker.get_bolt11_invoice(
- payment_hash=payment_hash,
- amount_msat=invoice_amount_sat * 1000,
+ payment_info=info,
message='Submarine swap',
- expiry=300,
fallback_address=None,
channels=channels,
- min_final_cltv_expiry_delta=min_final_cltv_expiry_delta,
)
margin_to_get_refund_tx_mined = MIN_LOCKTIME_DELTA
if not (locktime + margin_to_get_refund_tx_mined < self.network.get_local_height() + lnaddr1.get_min_final_cltv_delta()):
raise Exception(
f"onchain locktime ({locktime}+{margin_to_get_refund_tx_mined}) "
f"too close to LN-htlc-expiry ({self.network.get_local_height()+lnaddr1.get_min_final_cltv_delta()})")
- # add payment info to lnworker
- self.lnworker.add_payment_info_for_hold_invoice(payment_hash, invoice_amount_sat)
if prepay:
- prepay_hash = self.lnworker.create_payment_info(amount_msat=prepay_amount_sat*1000)
+ prepay_hash = self.lnworker.create_payment_info(
+ amount_msat=prepay_amount_sat*1000,
+ min_final_cltv_delta=min_final_cltv_expiry_delta or MIN_FINAL_CLTV_DELTA_ACCEPTED,
+ exp_delay=300,
+ )
+ info = self.lnworker.get_payment_info(prepay_hash)
lnaddr2, prepay_invoice = self.lnworker.get_bolt11_invoice(
- payment_hash=prepay_hash,
- amount_msat=prepay_amount_sat * 1000,
+ payment_info=info,
message='Submarine swap prepayment',
- expiry=300,
fallback_address=None,
channels=channels,
- min_final_cltv_expiry_delta=min_final_cltv_expiry_delta,
)
self.lnworker.bundle_payments([payment_hash, prepay_hash])
self._prepayments[prepay_hash] = payment_hash
diff --git a/electrum/wallet.py b/electrum/wallet.py
index 7bf807e..9bd1e9c 100644
--- a/electrum/wallet.py
+++ b/electrum/wallet.py
@@ -3011,11 +3011,11 @@ class Abstract_Wallet(ABC, Logger, EventListener):
return ''
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)
+ assert info.amount_msat == amount_msat, f"{info.amount_msat=} != {amount_msat=}"
lnaddr, invoice = self.lnworker.get_bolt11_invoice(
- payment_hash=payment_hash,
- amount_msat=amount_msat,
+ payment_info=info,
message=req.message,
- expiry=req.exp,
fallback_address=None)
return invoice
@@ -3031,7 +3031,11 @@ class Abstract_Wallet(ABC, Logger, EventListener):
timestamp = int(Request._get_cur_time())
if address is None:
assert self.has_lightning()
- payment_hash = self.lnworker.create_payment_info(amount_msat=amount_msat, write_to_disk=False)
+ payment_hash = self.lnworker.create_payment_info(
+ amount_msat=amount_msat,
+ exp_delay=exp_delay,
+ write_to_disk=False,
+ )
else:
payment_hash = None
outputs = [PartialTxOutput.from_address_and_value(address, amount_sat)] if address else []
diff --git a/electrum/wallet_db.py b/electrum/wallet_db.py
index f527d5d..95d038e 100644
--- a/electrum/wallet_db.py
+++ b/electrum/wallet_db.py
@@ -73,7 +73,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 = 60 # electrum >= 2.7 will set this to prevent
+FINAL_SEED_VERSION = 61 # electrum >= 2.7 will set this to prevent
# old versions from overwriting new format
@@ -236,6 +236,7 @@ class WalletDBUpgrader(Logger):
self._convert_version_58()
self._convert_version_59()
self._convert_version_60()
+ self._convert_version_61()
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
def _convert_wallet_type(self):
@@ -1157,6 +1158,18 @@ class WalletDBUpgrader(Logger):
cb['multisig_funding_privkey'] = None
self.data['seed_version'] = 60
+ def _convert_version_61(self):
+ if not self._is_upgrade_method_needed(60, 60):
+ return
+ # adding additional fields to PaymentInfo
+ lightning_payments = self.data.get('lightning_payments', {})
+ expiry_never = 100 * 365 * 24 * 60 * 60
+ migration_time = int(time.time())
+ for rhash, (amount_msat, direction, is_paid) in list(lightning_payments.items()):
+ new = (amount_msat, direction, is_paid, 147, expiry_never, migration_time)
+ lightning_payments[rhash] = new
+ self.data['seed_version'] = 61
+
def _convert_imported(self):
if not self._is_upgrade_method_needed(0, 13):
return
diff --git a/tests/test_lnpeer.py b/tests/test_lnpeer.py
index 85c2415..f7d8402 100644
--- a/tests/test_lnpeer.py
+++ b/tests/test_lnpeer.py
@@ -41,7 +41,7 @@ from electrum.lnworker import PaymentInfo, RECEIVED
from electrum.lnonion import OnionFailureCode, OnionRoutingFailure
from electrum.lnutil import UpdateAddHtlc
from electrum.lnutil import LOCAL, REMOTE
-from electrum.invoices import PR_PAID, PR_UNPAID, Invoice
+from electrum.invoices import PR_PAID, PR_UNPAID, Invoice, LN_EXPIRY_NEVER
from electrum.interface import GracefulDisconnect
from electrum.simple_config import SimpleConfig
from electrum.fee_policy import FeeTimeEstimates, FEE_ETA_TARGETS
@@ -563,15 +563,8 @@ class TestPeer(ElectrumTestCase):
payment_preimage = os.urandom(32)
if payment_hash is None:
payment_hash = sha256(payment_preimage)
- info = PaymentInfo(
- payment_hash=payment_hash,
- amount_msat=amount_msat,
- direction=RECEIVED,
- status=PR_UNPAID,
- )
if payment_preimage:
w2.save_preimage(payment_hash, payment_preimage)
- w2.save_payment_info(info)
if include_routing_hints:
routing_hints = w2.calc_routing_hints_for_invoice(amount_msat)
else:
@@ -584,7 +577,16 @@ class TestPeer(ElectrumTestCase):
else:
payment_secret = None
if min_final_cltv_delta is None:
- min_final_cltv_delta = lnutil.MIN_FINAL_CLTV_DELTA_FOR_INVOICE
+ min_final_cltv_delta = lnutil.MIN_FINAL_CLTV_DELTA_ACCEPTED
+ info = PaymentInfo(
+ payment_hash=payment_hash,
+ amount_msat=amount_msat,
+ direction=RECEIVED,
+ status=PR_UNPAID,
+ min_final_cltv_delta=min_final_cltv_delta,
+ expiry_delay=LN_EXPIRY_NEVER,
+ )
+ w2.save_payment_info(info)
lnaddr1 = LnAddr(
paymenthash=payment_hash,
amount=amount_btc,
Why this scored 46/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.