undf: assign 920-922; stamp wasabi patches

This commit is contained in:
russell@unturf.com 2026-03-31 09:13:59 -04:00
parent 737891b5a1
commit b62fd27e28
7 changed files with 302 additions and 4 deletions

View file

@ -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"
}

View file

@ -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<void> _injectUserEthTokensIntoCurrencyLists() async {
final tokens = await TokenUtilities.loadEvmTokensForSwap();
final toAddReceive = <CryptoCurrency>[];
final toAddDeposit = <CryptoCurrency>[];
+ // Build sets of existing contract addresses for O(1) lookup instead of
+ // scanning receiveCurrencies/depositCurrencies with .any() per token.
+ final receiveAddrs = receiveCurrencies.whereType<Erc20Token>()
+ .map((t) => t.contractAddress.toLowerCase()).toSet();
+ final depositAddrs = depositCurrencies.whereType<Erc20Token>()
+ .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<void> _injectUserSplTokensIntoCurrencyLists() async {
final tokens = await TokenUtilities.loadSolTokensForSwap();
final toAddReceive = <CryptoCurrency>[];
final toAddDeposit = <CryptoCurrency>[];
+ // Build sets of existing mint addresses for O(1) lookup.
+ final receiveMints = receiveCurrencies.whereType<SPLToken>()
+ .map((t) => t.mintAddress.toLowerCase()).toSet();
+ final depositMints = depositCurrencies.whereType<SPLToken>()
+ .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<void> _injectUserTronTokensIntoCurrencyLists() async {
final tokens = await TokenUtilities.loadTronTokensForSwap();
final toAddReceive = <CryptoCurrency>[];
final toAddDeposit = <CryptoCurrency>[];
+ // Build sets of existing contract addresses for O(1) lookup.
+ final receiveAddrs = receiveCurrencies.whereType<TronToken>()
+ .map((t) => t.contractAddress.toLowerCase()).toSet();
+ final depositAddrs = depositCurrencies.whereType<TronToken>()
+ .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);

View file

@ -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<FakeToken> list, FakeToken token) {
return list.any((item) {
opsDefect++;
return item.contractAddress.toLowerCase() == token.contractAddress.toLowerCase();
});
}
/// Fix: set-based lookup
bool setContainsToken(Set<String> 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 = <FakeToken>[];
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 = <FakeToken>[];
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');
}

View file

@ -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<ExchangePair> supportedPairs(List<CryptoCurrency> 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<TradePair<CryptoCurrency, FiatCurrency>> supportedCryptoToFiatPairs({
required List<CryptoCurrency> notSupportedCrypto,
required List<FiatCurrency> 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<TradePair<FiatCurrency, CryptoCurrency>> supportedFiatToCryptoPairs({
required List<FiatCurrency> notSupportedFiat,
required List<CryptoCurrency> 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

View file

@ -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

View file

@ -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:

View file

@ -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()