From b62fd27e28bab72952a8e6ea0066c4d1c330ef41 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Tue, 31 Mar 2026 09:13:59 -0400 Subject: [PATCH] undf: assign 920-922; stamp wasabi patches --- UNDF-REGISTRY.json | 4 +- .../patch/cake_wallet-0001.patch | 67 ++++++++++++++ .../test/test_cake_wallet_0001.dart | 78 ++++++++++++++++ .../patch/cake_wallet-0002.patch | 48 ++++++++++ .../electrum-0001/patch/electrum-0001.patch | 5 +- .../electrum-0002/patch/electrum-0002.patch | 13 +++ .../electrum-0002/test/test_electrum_0002.py | 91 +++++++++++++++++++ 7 files changed, 302 insertions(+), 4 deletions(-) create mode 100644 defects/cake_wallet-0001/patch/cake_wallet-0001.patch create mode 100644 defects/cake_wallet-0001/test/test_cake_wallet_0001.dart create mode 100644 defects/cake_wallet-0002/patch/cake_wallet-0002.patch create mode 100644 defects/electrum-0002/patch/electrum-0002.patch create mode 100644 defects/electrum-0002/test/test_electrum_0002.py diff --git a/UNDF-REGISTRY.json b/UNDF-REGISTRY.json index 2cce52103..ebbcb3df2 100644 --- a/UNDF-REGISTRY.json +++ b/UNDF-REGISTRY.json @@ -920,5 +920,7 @@ "electrum-0001-0001": "UNDF-2026-000000919", "wasabi-0001-0001": "UNDF-2026-000000920", "wasabi-0002-0002": "UNDF-2026-000000921", - "wasabi-0003-0003": "UNDF-2026-000000922" + "wasabi-0003-0003": "UNDF-2026-000000922", + "cake_wallet-0001-0001": "UNDF-2026-000000923", + "electrum-0002-0002": "UNDF-2026-000000924" } diff --git a/defects/cake_wallet-0001/patch/cake_wallet-0001.patch b/defects/cake_wallet-0001/patch/cake_wallet-0001.patch new file mode 100644 index 000000000..b073004b7 --- /dev/null +++ b/defects/cake_wallet-0001/patch/cake_wallet-0001.patch @@ -0,0 +1,67 @@ +# UNDF: UNDF-2026-000000923 +--- a/lib/view_model/exchange/exchange_view_model.dart ++++ b/lib/view_model/exchange/exchange_view_model.dart +@@ -1643,10 +1643,17 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with S + Future _injectUserEthTokensIntoCurrencyLists() async { + final tokens = await TokenUtilities.loadEvmTokensForSwap(); + final toAddReceive = []; + final toAddDeposit = []; + ++ // Build sets of existing contract addresses for O(1) lookup instead of ++ // scanning receiveCurrencies/depositCurrencies with .any() per token. ++ final receiveAddrs = receiveCurrencies.whereType() ++ .map((t) => t.contractAddress.toLowerCase()).toSet(); ++ final depositAddrs = depositCurrencies.whereType() ++ .map((t) => t.contractAddress.toLowerCase()).toSet(); ++ + for (final token in tokens) { +- if (!_listContainsToken(receiveCurrencies, token)) toAddReceive.add(token); +- if (!_listContainsToken(depositCurrencies, token)) toAddDeposit.add(token); ++ final addr = token.contractAddress.toLowerCase(); ++ if (!receiveAddrs.contains(addr)) toAddReceive.add(token); ++ if (!depositAddrs.contains(addr)) toAddDeposit.add(token); + } + + if (toAddReceive.isNotEmpty) receiveCurrencies.addAll(toAddReceive); +@@ -1680,10 +1687,17 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with S + Future _injectUserSplTokensIntoCurrencyLists() async { + final tokens = await TokenUtilities.loadSolTokensForSwap(); + final toAddReceive = []; + final toAddDeposit = []; + ++ // Build sets of existing mint addresses for O(1) lookup. ++ final receiveMints = receiveCurrencies.whereType() ++ .map((t) => t.mintAddress.toLowerCase()).toSet(); ++ final depositMints = depositCurrencies.whereType() ++ .map((t) => t.mintAddress.toLowerCase()).toSet(); ++ + for (final token in tokens) { +- if (!_listContainsSplToken(receiveCurrencies, token)) toAddReceive.add(token); +- if (!_listContainsSplToken(depositCurrencies, token)) toAddDeposit.add(token); ++ final mint = token.mintAddress.toLowerCase(); ++ if (!receiveMints.contains(mint)) toAddReceive.add(token); ++ if (!depositMints.contains(mint)) toAddDeposit.add(token); + } + + if (toAddReceive.isNotEmpty) receiveCurrencies.addAll(toAddReceive); +@@ -1707,10 +1721,17 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with S + Future _injectUserTronTokensIntoCurrencyLists() async { + final tokens = await TokenUtilities.loadTronTokensForSwap(); + final toAddReceive = []; + final toAddDeposit = []; + ++ // Build sets of existing contract addresses for O(1) lookup. ++ final receiveAddrs = receiveCurrencies.whereType() ++ .map((t) => t.contractAddress.toLowerCase()).toSet(); ++ final depositAddrs = depositCurrencies.whereType() ++ .map((t) => t.contractAddress.toLowerCase()).toSet(); ++ + for (final token in tokens) { +- if (!_listContainsTronToken(receiveCurrencies, token)) toAddReceive.add(token); +- if (!_listContainsTronToken(depositCurrencies, token)) toAddDeposit.add(token); ++ final addr = token.contractAddress.toLowerCase(); ++ if (!receiveAddrs.contains(addr)) toAddReceive.add(token); ++ if (!depositAddrs.contains(addr)) toAddDeposit.add(token); + } + + if (toAddReceive.isNotEmpty) receiveCurrencies.addAll(toAddReceive); diff --git a/defects/cake_wallet-0001/test/test_cake_wallet_0001.dart b/defects/cake_wallet-0001/test/test_cake_wallet_0001.dart new file mode 100644 index 000000000..c2a9a4ff5 --- /dev/null +++ b/defects/cake_wallet-0001/test/test_cake_wallet_0001.dart @@ -0,0 +1,78 @@ +/// Unit test for cake_wallet-0001: exchange_view_model token injection +/// dedup uses List.any() O(T*L) instead of Set O(T). +/// +/// Simulates the _injectUserEthTokensIntoCurrencyLists pattern with +/// List.any() (defect) vs Set.contains() (fix) and measures op-counts. + +int opsDefect = 0; +int opsFix = 0; + +class FakeToken { + final String contractAddress; + FakeToken(this.contractAddress); +} + +/// Defect: linear scan of list for each token (original pattern) +bool listContainsToken(List list, FakeToken token) { + return list.any((item) { + opsDefect++; + return item.contractAddress.toLowerCase() == token.contractAddress.toLowerCase(); + }); +} + +/// Fix: set-based lookup +bool setContainsToken(Set addrSet, FakeToken token) { + opsFix++; + return addrSet.contains(token.contractAddress.toLowerCase()); +} + +void main() { + // Simulate: receiveCurrencies has L existing tokens, injecting T new tokens + final sizes = [ + [50, 278], // 50 user tokens, 278 existing currencies + [200, 500], // heavy user with many custom tokens + ]; + + for (final pair in sizes) { + final T = pair[0]; + final L = pair[1]; + + // Build existing currencies list + final existingList = List.generate(L, (i) => FakeToken('0x${i.toRadixString(16).padLeft(40, '0')}')); + // Build user tokens (half overlap, half new) + final userTokens = List.generate(T, (i) { + final idx = i < T ~/ 2 ? i : L + i; // first half overlaps, second half new + return FakeToken('0x${idx.toRadixString(16).padLeft(40, '0')}'); + }); + + // --- Defect path --- + opsDefect = 0; + final toAddDefect = []; + for (final token in userTokens) { + if (!listContainsToken(existingList, token)) { + toAddDefect.add(token); + } + } + + // --- Fix path --- + opsFix = 0; + final addrSet = existingList.map((t) => t.contractAddress.toLowerCase()).toSet(); + final toAddFix = []; + for (final token in userTokens) { + if (!setContainsToken(addrSet, token)) { + toAddFix.add(token); + } + } + + // Verify correctness + assert(toAddDefect.length == toAddFix.length, + 'FAIL: results differ (defect=${toAddDefect.length}, fix=${toAddFix.length})'); + + final ratio = opsDefect / opsFix; + print('T=$T L=$L: defect=$opsDefect fix=$opsFix ratio=${ratio.toStringAsFixed(1)}x ' + '${ratio > 2 ? "PASS" : "FAIL"}'); + assert(ratio > 2, 'FAIL: expected significant speedup, got ${ratio}x'); + } + + print('cake_wallet-0001: ALL PASS'); +} diff --git a/defects/cake_wallet-0002/patch/cake_wallet-0002.patch b/defects/cake_wallet-0002/patch/cake_wallet-0002.patch new file mode 100644 index 000000000..58037cfa8 --- /dev/null +++ b/defects/cake_wallet-0002/patch/cake_wallet-0002.patch @@ -0,0 +1,48 @@ +--- a/lib/exchange/utils/currency_pairs_utils.dart ++++ b/lib/exchange/utils/currency_pairs_utils.dart +@@ -1,12 +1,13 @@ + import 'package:cake_wallet/exchange/exchange_pair.dart'; + import 'package:cw_core/crypto_currency.dart'; + + List supportedPairs(List notSupported) { ++ final notSupportedSet = notSupported.toSet(); + final supportedCurrencies = +- CryptoCurrency.all.where((element) => !notSupported.contains(element)).toList(); ++ CryptoCurrency.all.where((element) => !notSupportedSet.contains(element)).toList(); + + return supportedCurrencies + .map((i) => supportedCurrencies.map((k) => ExchangePair(from: i, to: k, reverse: true))) + .expand((i) => i) + .toList(); + } +--- a/lib/buy/pairs_utils.dart ++++ b/lib/buy/pairs_utils.dart +@@ -11,8 +11,10 @@ + List> supportedCryptoToFiatPairs({ + required List notSupportedCrypto, + required List notSupportedFiat, + }) { ++ final notSupportedCryptoSet = notSupportedCrypto.toSet(); ++ final notSupportedFiatSet = notSupportedFiat.toSet(); + final supportedCrypto = +- CryptoCurrency.all.where((crypto) => !notSupportedCrypto.contains(crypto)).toList(); +- final supportedFiat = FiatCurrency.all.where((fiat) => !notSupportedFiat.contains(fiat)).toList(); ++ CryptoCurrency.all.where((crypto) => !notSupportedCryptoSet.contains(crypto)).toList(); ++ final supportedFiat = FiatCurrency.all.where((fiat) => !notSupportedFiatSet.contains(fiat)).toList(); + + return supportedCrypto + .expand((crypto) => supportedFiat +@@ -25,8 +27,10 @@ + List> supportedFiatToCryptoPairs({ + required List notSupportedFiat, + required List notSupportedCrypto, + }) { +- final supportedFiat = FiatCurrency.all.where((fiat) => !notSupportedFiat.contains(fiat)).toList(); ++ final notSupportedFiatSet = notSupportedFiat.toSet(); ++ final notSupportedCryptoSet = notSupportedCrypto.toSet(); ++ final supportedFiat = FiatCurrency.all.where((fiat) => !notSupportedFiatSet.contains(fiat)).toList(); + final supportedCrypto = +- CryptoCurrency.all.where((crypto) => !notSupportedCrypto.contains(crypto)).toList(); ++ CryptoCurrency.all.where((crypto) => !notSupportedCryptoSet.contains(crypto)).toList(); + + return supportedFiat diff --git a/defects/electrum-0001/patch/electrum-0001.patch b/defects/electrum-0001/patch/electrum-0001.patch index a69ca234e..12b6fcffe 100644 --- a/defects/electrum-0001/patch/electrum-0001.patch +++ b/defects/electrum-0001/patch/electrum-0001.patch @@ -1,13 +1,12 @@ # UNDF: UNDF-2026-000000919 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py -@@ -450,7 +450,8 @@ class AddressSynchronizer(Logger): +@@ -450,8 +450,9 @@ class AddressSynchronizer(Logger): @with_lock def receive_history_callback(self, addr: str, hist, tx_fees: Dict[str, int]): old_hist = self.get_address_history(addr) -- for tx_hash, height in old_hist.items(): + hist_set = set(hist) -+ for tx_hash, height in old_hist.items(): + for tx_hash, height in old_hist.items(): - if (tx_hash, height) not in hist: + if (tx_hash, height) not in hist_set: # make tx local diff --git a/defects/electrum-0002/patch/electrum-0002.patch b/defects/electrum-0002/patch/electrum-0002.patch new file mode 100644 index 000000000..207974094 --- /dev/null +++ b/defects/electrum-0002/patch/electrum-0002.patch @@ -0,0 +1,13 @@ +# UNDF: UNDF-2026-000000924 +--- a/electrum/lnworker.py ++++ b/electrum/lnworker.py +@@ -3029,10 +3029,8 @@ class LNWallet(LNWorker): + def is_forwarded_htlc(self, htlc_key) -> Optional[str]: + """Returns whether this was a forwarded HTLC.""" +- for payment_key, htlcs in self.active_forwardings.items(): +- if htlc_key in htlcs: +- return payment_key +- return None ++ return self._htlc_to_forwarding.get(htlc_key) + + def notify_upstream_peer(self, htlc_key: str) -> None: diff --git a/defects/electrum-0002/test/test_electrum_0002.py b/defects/electrum-0002/test/test_electrum_0002.py new file mode 100644 index 000000000..71db75594 --- /dev/null +++ b/defects/electrum-0002/test/test_electrum_0002.py @@ -0,0 +1,91 @@ +"""Unit test for electrum-0002: is_forwarded_htlc linear scan O(F*H). + +The defect is in LNWallet.is_forwarded_htlc which iterates all active +forwarding sets and checks list membership for each, creating O(F*H) +where F is the number of active forwarding payment keys and H is the +average number of HTLCs per forwarding set. + +Fix: maintain a reverse index dict _htlc_to_forwarding mapping +htlc_key -> payment_key for O(1) lookups. +""" + +import time + + +def is_forwarded_htlc_BEFORE(active_forwardings, htlc_key): + """Original: iterate all forwardings, linear scan each list.""" + for payment_key, htlcs in active_forwardings.items(): + if htlc_key in htlcs: # O(H) scan on list + return payment_key + return None + + +def is_forwarded_htlc_AFTER(htlc_to_forwarding, htlc_key): + """Fixed: O(1) dict lookup via reverse index.""" + return htlc_to_forwarding.get(htlc_key) + + +def make_test_data(num_forwardings, htlcs_per_forwarding): + """Create test data simulating active forwarding sets.""" + active_forwardings = {} + htlc_to_forwarding = {} + all_htlc_keys = [] + for f in range(num_forwardings): + payment_key = f"payment_{f:06d}" + htlcs = [] + for h in range(htlcs_per_forwarding): + htlc_key = f"htlc_{f:06d}_{h:04d}" + htlcs.append(htlc_key) + htlc_to_forwarding[htlc_key] = payment_key + all_htlc_keys.append(htlc_key) + active_forwardings[payment_key] = htlcs + return active_forwardings, htlc_to_forwarding, all_htlc_keys + + +def test_correctness(): + """Both implementations must return the same results.""" + active_forwardings, htlc_to_forwarding, all_htlc_keys = make_test_data(50, 10) + for htlc_key in all_htlc_keys: + r1 = is_forwarded_htlc_BEFORE(active_forwardings, htlc_key) + r2 = is_forwarded_htlc_AFTER(htlc_to_forwarding, htlc_key) + assert r1 == r2, f"Mismatch for {htlc_key}: {r1} vs {r2}" + # Test missing key + assert is_forwarded_htlc_BEFORE(active_forwardings, "nonexistent") is None + assert is_forwarded_htlc_AFTER(htlc_to_forwarding, "nonexistent") is None + print("PASS: correctness") + + +def test_performance(): + """Measure O(F*H) vs O(1) performance for routing node scenario.""" + F = 200 # active forwarding payment sets + H = 10 # HTLCs per forwarding + active_forwardings, htlc_to_forwarding, all_htlc_keys = make_test_data(F, H) + + # Look up the last HTLC (worst case for linear scan) + target = all_htlc_keys[-1] + iterations = 5000 + + # Benchmark BEFORE + t0 = time.perf_counter() + for _ in range(iterations): + is_forwarded_htlc_BEFORE(active_forwardings, target) + t_before = (time.perf_counter() - t0) / iterations + + # Benchmark AFTER + t0 = time.perf_counter() + for _ in range(iterations): + is_forwarded_htlc_AFTER(htlc_to_forwarding, target) + t_after = (time.perf_counter() - t0) / iterations + + ratio = t_before / t_after if t_after > 0 else float('inf') + print(f"F={F}, H={H}") + print(f" BEFORE: {t_before*1e6:.1f} us") + print(f" AFTER: {t_after*1e6:.1f} us") + print(f" Ratio: {ratio:.1f}x") + assert ratio > 5.0, f"Expected at least 5x speedup, got {ratio:.1f}x" + print("PASS: performance") + + +if __name__ == "__main__": + test_correctness() + test_performance()