lnworker: rewrite payment_bundles: lower cpu-time-complexity
What changed, and why it matters
This commit is a performance refactor, not a security fix. It replaces a slow list-based data structure for tracking bundled Lightning payments with two dictionaries that allow faster lookups. There is no indication it addresses a vulnerability or changes security behavior.
No security action needed; treat as a normal performance refactor. If reviewing for correctness, verify that the new lock usage covers all readers/writers and that the immutability assumption for bundles is enforced elsewhere.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change rewrites payment_bundles in electrum/lnworker.py from a list of payment-key lists to a pair of dicts: _payment_bundles_pkey_to_canon maps each payment key to a canonical bundle key, and _payment_bundles_canon_to_pkeylist maps that canonical key back to the bundle. This lowers CPU-time complexity for get_payment_bundle and delete_payment_bundle from O(n) list scans to O(1) dict lookups. The commit adds assertions that payment keys are not already in a bundle and wraps updates in self.lock. The test mock is updated to match the new structure. No security bug is described or visibly patched.
Changed components
electrum/lnworker.pytests/test_lnpeer.pyInspect captured patch +34 / −12
diff --git a/electrum/lnworker.py b/electrum/lnworker.py
index 78e6c03..0261e3f 100644
--- a/electrum/lnworker.py
+++ b/electrum/lnworker.py
@@ -910,7 +910,8 @@ class LNWallet(LNWorker):
# payment_hash -> callback:
self.hold_invoice_callbacks = {} # type: Dict[bytes, Callable[[bytes], Awaitable[None]]]
- self.payment_bundles = [] # lists of hashes. todo:persist
+ self._payment_bundles_pkey_to_canon = {} # type: Dict[bytes, bytes] # TODO: persist
+ self._payment_bundles_canon_to_pkeylist = {} # type: Dict[bytes, Sequence[bytes]] # TODO: persist
self.nostr_keypair = generate_keypair(BIP32Node.from_xkey(xprv), LnKeyFamily.NOSTR_KEY)
self.swap_manager = SwapManager(wallet=self.wallet, lnworker=self)
@@ -2308,22 +2309,42 @@ class LNWallet(LNWorker):
self.wallet.save_db()
return payment_hash
- def bundle_payments(self, hash_list):
+ def bundle_payments(self, hash_list: Sequence[bytes]) -> None:
+ """Bundle together a list of payment_hashes, for atomicity, so that either
+ - all gets fulfilled, or
+ - none of them gets fulfilled.
+ (we are the recipient of this payment)
+ """
payment_keys = [self._get_payment_key(x) for x in hash_list]
- self.payment_bundles.append(payment_keys)
+ with self.lock:
+ # We maintain two maps.
+ # map1: payment_key -> bundle_key=canon_pkey (canonically smallest among pkeys)
+ # map2: bundle_key -> list of pkeys in bundle
+ # assumption: bundles are immutable, so no adding extra pkeys after-the-fact
+ canon_pkey = min(payment_keys)
+ for pkey in payment_keys:
+ assert pkey not in self._payment_bundles_pkey_to_canon
+ for pkey in payment_keys:
+ self._payment_bundles_pkey_to_canon[pkey] = canon_pkey
+ self._payment_bundles_canon_to_pkeylist[canon_pkey] = tuple(payment_keys)
def get_payment_bundle(self, payment_key: bytes) -> Sequence[bytes]:
- for key_list in self.payment_bundles:
- if payment_key in key_list:
- return key_list
- return []
+ with self.lock:
+ canon_pkey = self._payment_bundles_pkey_to_canon.get(payment_key)
+ if canon_pkey is None:
+ return []
+ return self._payment_bundles_canon_to_pkeylist[canon_pkey]
def delete_payment_bundle(self, payment_hash: bytes) -> None:
payment_key = self._get_payment_key(payment_hash)
- for key_list in self.payment_bundles:
- if payment_key in key_list:
- self.payment_bundles.remove(key_list)
+ with self.lock:
+ canon_pkey = self._payment_bundles_pkey_to_canon.get(payment_key)
+ if canon_pkey is None: # is it ok for bundle to be missing??
return
+ pkey_list = self._payment_bundles_canon_to_pkeylist[canon_pkey]
+ for pkey in pkey_list:
+ del self._payment_bundles_pkey_to_canon[pkey]
+ del self._payment_bundles_canon_to_pkeylist[canon_pkey]
def save_preimage(self, payment_hash: bytes, preimage: bytes, *, write_to_disk: bool = True):
if sha256(preimage) != payment_hash:
diff --git a/tests/test_lnpeer.py b/tests/test_lnpeer.py
index 9ffbaf6..01b3074 100644
--- a/tests/test_lnpeer.py
+++ b/tests/test_lnpeer.py
@@ -10,7 +10,7 @@ import logging
import concurrent
from concurrent import futures
from unittest import mock
-from typing import Iterable, NamedTuple, Tuple, List, Dict
+from typing import Iterable, NamedTuple, Tuple, List, Dict, Sequence
from aiorpcx import timeout_after, TaskTimeout
from electrum_ecc import ECPrivkey
@@ -215,7 +215,8 @@ class MockLNWallet(Logger, EventListener, NetworkRetryManager[LNPeerAddr]):
self.downstream_to_upstream_htlc = {}
self.dont_settle_htlcs = {}
self.hold_invoice_callbacks = {}
- self.payment_bundles = [] # lists of hashes. todo:persist
+ self._payment_bundles_pkey_to_canon = {} # type: Dict[bytes, bytes]
+ self._payment_bundles_canon_to_pkeylist = {} # type: Dict[bytes, Sequence[bytes]]
self.config.INITIAL_TRAMPOLINE_FEE_LEVEL = 0
self.logger.info(f"created LNWallet[{name}] with nodeID={local_keypair.pubkey.hex()}")
Why this scored 12/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.