What changed, and why it matters
This commit changes how Stack Wallet caches and resets Firo Spark private-balance data. It removes a safety fallback that refetched the full anonymity set when the local cache size looked inconsistent, switches from 'insert-or-ignore' to plain inserts for Spark coins, and simplifies the 'clear Spark cache' button so it only wipes the shared cache and no longer resets per-wallet metadata. A separate change also fixes which address type is used when generating the next Spark receiving address. The overall effect is a reset/rework of private balance tracking, but the commit message and diff alone do not clearly state whether this fixes a security bug or is just a reliability refactor.
Treat this as a candidate for further review rather than a confirmed vulnerability. Verify whether the removed fallback and metadata reset were intentional simplifications or whether they reintroduce stale-cache or state-inconsistency issues. Test Spark wallet resync, cache reset, and address generation paths. If this commit is part of a security fix, request a vendor advisory or CVE assignment.
Security signals we found
Removal of cache-size mismatch fallback could allow stale or partial anonymity-set data to be treated as current
Switch from INSERT OR IGNORE to plain INSERT may raise duplicate-key errors if the same coin is re-fetched, affecting robustness
Use of lastInsertRowId assumes no concurrent inserts; mismatch could corrupt SparkSetCoins linkage
Clear-cache UI no longer resets per-wallet block-hash cache, potentially leaving inconsistent private-balance state across wallets
Address generation change could affect Spark address derivation correctness
Evidence from the diff
The patch modifies Firo Spark cache coordination and writing. In firo_cache_coordinator.dart it removes the effectivePrevSize fallback (used when prevSize > meta.size) and always computes numberOfCoinsToFetch = meta.size - prevSize, starting sector fetches from 0 instead of effectivePrevSize. In firo_cache_writer.dart it replaces INSERT OR IGNORE with plain INSERT and uses db.lastInsertRowId instead of querying the inserted row, and removes the duplicate-check before inserting into SparkSetCoins. The UI clear-cache button no longer clears per-wallet WalletInfoKeys.firoSparkCacheSetBlockHashCache entries. In spark_interface.dart, generateNextSparkAddress now reads getCurrentReceivingAddress() instead of getCurrentReceivingSparkAddress() to determine the next diversifier.
Changed components
lib/db/sqlite/firo_cache_coordinator.dartlib/db/sqlite/firo_cache_writer.dartlib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dartlib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dartInspect captured patch +14 / −76
diff --git a/lib/db/sqlite/firo_cache_coordinator.dart b/lib/db/sqlite/firo_cache_coordinator.dart
index 3fff06f..5ecd753 100644
--- a/lib/db/sqlite/firo_cache_coordinator.dart
+++ b/lib/db/sqlite/firo_cache_coordinator.dart
@@ -109,23 +109,7 @@ abstract class FiroCacheCoordinator {
return;
}
- final int effectivePrevSize;
- if (prevSize > meta.size) {
- Logging.instance.w(
- "Spark cache size mismatch for groupId=$groupId: "
- "prevSize=$prevSize > meta.size=${meta.size}. "
- "Falling back to full refetch for this set.",
- );
- effectivePrevSize = 0;
- } else {
- effectivePrevSize = prevSize;
- }
-
- final numberOfCoinsToFetch = meta.size - effectivePrevSize;
- if (numberOfCoinsToFetch <= 0) {
- // Already up to date for this block hash/set hash.
- return;
- }
+ final numberOfCoinsToFetch = meta.size - prevSize;
final fullSectorCount = numberOfCoinsToFetch ~/ sectorSize;
final remainder = numberOfCoinsToFetch % sectorSize;
@@ -133,14 +117,14 @@ abstract class FiroCacheCoordinator {
final List<dynamic> coins = [];
for (int i = 0; i < fullSectorCount; i++) {
- final start = effectivePrevSize + (i * sectorSize);
+ final start = (i * sectorSize);
final data = await client.getSparkAnonymitySetBySector(
coinGroupId: groupId,
latestBlock: meta.blockHash,
startIndex: start,
endIndex: start + sectorSize,
);
- progressUpdated?.call(((i + 1) * sectorSize), numberOfCoinsToFetch);
+ progressUpdated?.call(start + sectorSize, numberOfCoinsToFetch);
coins.addAll(data);
}
@@ -149,8 +133,8 @@ abstract class FiroCacheCoordinator {
final data = await client.getSparkAnonymitySetBySector(
coinGroupId: groupId,
latestBlock: meta.blockHash,
- startIndex: effectivePrevSize + numberOfCoinsToFetch - remainder,
- endIndex: effectivePrevSize + numberOfCoinsToFetch,
+ startIndex: numberOfCoinsToFetch - remainder,
+ endIndex: numberOfCoinsToFetch,
);
progressUpdated?.call(numberOfCoinsToFetch, numberOfCoinsToFetch);
diff --git a/lib/db/sqlite/firo_cache_writer.dart b/lib/db/sqlite/firo_cache_writer.dart
index 3bfa702..fadc3eb 100644
--- a/lib/db/sqlite/firo_cache_writer.dart
+++ b/lib/db/sqlite/firo_cache_writer.dart
@@ -90,46 +90,21 @@ FCResult _updateSparkAnonSetCoinsWith(
for (final coin in coins) {
db.execute(
"""
- INSERT OR IGNORE INTO SparkCoin (serialized, txHash, context, groupId)
+ INSERT INTO SparkCoin (serialized, txHash, context, groupId)
VALUES (?, ?, ?, ?);
""",
[coin.serialized, coin.txHash, coin.context, coin.groupId],
);
- final coinIdResult = db.select(
- """
- SELECT id
- FROM SparkCoin
- WHERE serialized = ? AND txHash = ? AND context = ? AND groupId = ?
- LIMIT 1;
- """,
- [coin.serialized, coin.txHash, coin.context, coin.groupId],
- );
- if (coinIdResult.isEmpty) {
- throw Exception(
- "Failed to resolve SparkCoin id after insert/ignore operation",
- );
- }
- final coinId = coinIdResult.first["id"] as int;
+ final coinId = db.lastInsertRowId;
// finally add the row id to the newly added set
- final hasSetCoin = db.select(
+ db.execute(
"""
- SELECT 1
- FROM SparkSetCoins
- WHERE setId = ? AND coinId = ?
- LIMIT 1;
+ INSERT INTO SparkSetCoins (setId, coinId)
+ VALUES (?, ?);
""",
[setId, coinId],
);
- if (hasSetCoin.isEmpty) {
- db.execute(
- """
- INSERT INTO SparkSetCoins (setId, coinId)
- VALUES (?, ?);
- """,
- [setId, coinId],
- );
- }
}
db.execute("COMMIT;");
diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart
index e70ae01..57c5a11 100644
--- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart
+++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart
@@ -23,7 +23,6 @@ import '../../../../../utilities/assets.dart';
import '../../../../../utilities/text_styles.dart';
import '../../../../../wallets/crypto_currency/crypto_currency.dart';
import '../../../../../wallets/isar/models/wallet_info.dart';
-import '../../../../../wallets/isar/providers/all_wallets_info_provider.dart';
import '../../../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart';
import '../../../../../widgets/custom_buttons/draggable_switch_button.dart';
@@ -379,7 +378,6 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
case WalletFeature.clearSparkCache:
return _MoreFeaturesClearSparkCacheItem(
- walletId: widget.walletId,
cryptoCurrency: wallet.cryptoCurrency,
);
@@ -656,23 +654,21 @@ class _MoreFeaturesItemBase extends StatelessWidget {
}
}
-class _MoreFeaturesClearSparkCacheItem extends ConsumerStatefulWidget {
+class _MoreFeaturesClearSparkCacheItem extends StatefulWidget {
const _MoreFeaturesClearSparkCacheItem({
super.key,
- required this.walletId,
required this.cryptoCurrency,
});
- final String walletId;
final CryptoCurrency cryptoCurrency;
@override
- ConsumerState<_MoreFeaturesClearSparkCacheItem> createState() =>
+ State<_MoreFeaturesClearSparkCacheItem> createState() =>
_MoreFeaturesClearSparkCacheItemState();
}
class _MoreFeaturesClearSparkCacheItemState
- extends ConsumerState<_MoreFeaturesClearSparkCacheItem> {
+ extends State<_MoreFeaturesClearSparkCacheItem> {
bool _onPressedLock = false;
static const label = "Reset Spark electrumx cache";
@@ -689,23 +685,6 @@ class _MoreFeaturesClearSparkCacheItemState
await FiroCacheCoordinator.clearSharedCache(
widget.cryptoCurrency.network,
);
- final isar = ref.read(mainDBProvider).isar;
- final sparkWalletInfos = ref
- .read(pAllWalletsInfo)
- .where(
- (info) =>
- info.coin.identifier == widget.cryptoCurrency.identifier,
- )
- .toList();
- for (final info in sparkWalletInfos) {
- await info.updateOtherData(
- newEntries: {
- WalletInfoKeys.firoSparkCacheSetBlockHashCache:
- <String, String>{},
- },
- isar: isar,
- );
- }
setState(() {
// trigger rebuild for cache size display
});
diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart
index 80e19de..0dec8aa 100644
--- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart
+++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart
@@ -394,7 +394,7 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
Future<Address> generateNextSparkAddress({required bool saveToDB}) async {
final currentDiversifier =
- (await getCurrentReceivingSparkAddress())?.derivationIndex;
+ (await getCurrentReceivingAddress())?.derivationIndex;
// if current is null, start at index 1
int diversifier = (currentDiversifier ?? 0) + 1;
if (diversifier == libSpark.sparkChange) {
Why this scored 41/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.