What changed, and why it matters
This commit changes how the Stack Wallet app finds a user's Firo masternodes. Previously, the wallet guessed masternode identities by scanning local transaction records and 1000-FIRO UTXOs, which could miss masternodes or include wrong transactions. The new code fetches full transaction details from the ElectrumX server and matches each ProRegTx transaction to its exact collateral transaction ID. There is no direct evidence in the commit that this fixes an exploitable security vulnerability; it appears to be a correctness/reliability fix for the masternode list display and management.
Treat this as a functional/reliability fix rather than a critical security patch. Reviewers should verify that getBatchTransactions() responses are validated correctly, that null/ malformed fields cannot crash the wallet, and that a malicious or compromised ElectrumX server cannot spoof masternode ownership by returning crafted proReg.collateralHash values. Consider adding tests for malformed server responses and confirming the fallback collateral-txid logic does not mislead the user.
Security signals we found
Change in masternode identity resolution logic
New network call to ElectrumX server for batch transaction data
Parsing of server-controlled fields (version, type, proReg.collateralHash) used to decide wallet state
Removal of local-only ProRegTx detection heuristic
Possible functional bug in old code: fallback could include non-ProRegTx txids that downstream code had to filter
Evidence from the diff
The patch rewrites getMyMasternodeProTxHashes() in lib/wallets/wallet/impl/firo_wallet.dart. The old implementation identified ProRegTx transactions by checking the local transactionV2s table for version == 65539 and by adding any 1000-FIRO UTXO txid as a fallback. The new implementation collects all 1000-FIRO UTXO collateral txids, then queries the ElectrumX server via getBatchTransactions() for every wallet transaction, parses each returned transaction’s version, type, and proReg.collateralHash, and only adds a txid to the result when it is a type-1 version-3 ProRegTx whose collateralHash matches a known 1000-FIRO UTXO. A catch block logs failures, and any unmatched collateral txids are still appended as a fallback. The change improves accuracy of masternode identification but introduces a network dependency and additional parsing of server-provided transaction data.
Changed components
lib/wallets/wallet/impl/firo_wallet.dartFiro masternode management / getMyMasternodeProTxHashes()ElectrumX client integrationInspect captured patch +52 / −18
diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart
index 593f618..72253b2 100644
--- a/lib/wallets/wallet/impl/firo_wallet.dart
+++ b/lib/wallets/wallet/impl/firo_wallet.dart
@@ -1311,23 +1311,8 @@ class FiroWallet<T extends ElectrumXCurrencyInterface> extends Bip39HDWallet<T>
Future<List<String>> getMyMasternodeProTxHashes() async {
final List<String> r = [];
+ final Set<String> collateralTxids = {};
- // Look for ProRegTx transactions (nVersion=3, nType=1 → version field
- // = 3 + (1 << 16) = 65539) that this wallet has broadcast.
- final allTxs =
- await mainDB.isar.transactionV2s
- .where()
- .walletIdEqualTo(walletId)
- .findAll();
- for (final tx in allTxs) {
- if (tx.version == 3 + (1 << 16) && !r.contains(tx.txid)) {
- r.add(tx.txid);
- }
- }
-
- // Fallback: also check 1000 FIRO UTXOs (works for legacy internal
- // collateral where the protx txid == collateral txid). Will harmlessly
- // produce non-protx txids that getMyMasternodes filters out.
final utxos = await mainDB.getUTXOs(walletId).sortByBlockHeight().findAll();
final rawMasterNodeAmount = Amount.fromDecimal(
kMasterNodeValue,
@@ -1335,8 +1320,57 @@ class FiroWallet<T extends ElectrumXCurrencyInterface> extends Bip39HDWallet<T>
).raw.toInt();
for (final utxo in utxos) {
- if (utxo.value == rawMasterNodeAmount && !r.contains(utxo.txid)) {
- r.add(utxo.txid);
+ if (utxo.value == rawMasterNodeAmount) {
+ collateralTxids.add(utxo.txid);
+ }
+ }
+
+ if (collateralTxids.isNotEmpty) {
+ try {
+ final walletTxids =
+ await mainDB.isar.transactionV2s
+ .where()
+ .walletIdEqualTo(walletId)
+ .txidProperty()
+ .findAll();
+
+ if (walletTxids.isNotEmpty) {
+ final txs = await electrumXCachedClient.getBatchTransactions(
+ txHashes: walletTxids.toSet().toList(growable: false),
+ cryptoCurrency: cryptoCurrency,
+ );
+
+ for (final tx in txs) {
+ final txid = tx["txid"]?.toString();
+ final version = tx["version"];
+ final type = tx["type"];
+ final proReg = tx["proReg"];
+ if (txid == null ||
+ version != 3 ||
+ type != 1 ||
+ proReg is! Map) {
+ continue;
+ }
+
+ final proRegMap = Map<String, dynamic>.from(proReg);
+ final collateralHash = proRegMap["collateralHash"]?.toString();
+ if (collateralHash != null &&
+ collateralTxids.contains(collateralHash) &&
+ !r.contains(txid)) {
+ r.add(txid);
+ }
+ }
+ }
+ } catch (e) {
+ Logging.instance.i(
+ "Failed to resolve proTx hashes from wallet tx history: $e",
+ );
+ }
+ }
+
+ for (final txid in collateralTxids) {
+ if (!r.contains(txid)) {
+ r.add(txid);
}
}
Why this scored 21/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.