only check address validation once for old addresses
What changed, and why it matters
This commit changes how Cake Wallet verifies whether stored Bitcoin and Bitcoin Cash addresses belong to the 'hidden' (change) side of a wallet. Previously, the app re-checked every address on every wallet open, which could flip address labels incorrectly if a stored address didn't match either the normal or hidden chain. Now it checks each old address only once, marks it as checked, and only flips the label if the address actually re-derives from the opposite chain. New addresses created by the wallet are marked correct-by-construction and skipped. The change is a correctness/performance fix rather than a clear security patch, but a mislabeled change address could in theory cause a user to share or reuse an address unexpectedly.
Review whether any persisted `BitcoinAddressRecord` objects from older app versions could have had `isHidden` flipped incorrectly before this fix; consider a one-time migration or audit log for affected wallets. Otherwise treat as a routine correctness/performance improvement.
Security signals we found
Address label (hidden/visible) correctness affects which addresses users believe are receive vs change addresses
Repeated re-derivation on every startup removed, reducing side-channel/performance exposure
Logic change prevents arbitrary flipping of `isHidden` for addresses that do not match either derivation path
No explicit security advisory, CVE, or bug bounty attribution in commit or references
Evidence from the diff
The patch adds an isHiddenChecked boolean to BitcoinAddressRecord, serializes it, and sets it to true for addresses generated internally. In _validateAddress, it now returns early if already checked, then derives only the opposite-chain HD path and toggles isHidden only when the stored address matches that opposite-chain derivation. The previous code derived both chains and could toggle isHidden when the address matched neither (e.g., imported or legacy records), leading to label drift. Bitcoin Cash wallet address conversion also preserves the flag.
Changed components
cw_bitcoin/lib/bitcoin_address_record.dartcw_bitcoin/lib/electrum_wallet_addresses.dartcw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dartInspect captured patch +30 / −25
### cw_bitcoin/lib/bitcoin_address_record.dart
@@ -67,6 +67,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
required super.index,
super.accountIndex = 0,
super.isHidden = false,
+ this.isHiddenChecked = false,
super.isLegacyDerivation = false,
super.txCount = 0,
super.balance = 0,
@@ -110,6 +111,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
index: decoded['index'] as int,
accountIndex: decoded['accountIndex'] as int? ?? 0,
isHidden: decoded['isHidden'] as bool? ?? false,
+ isHiddenChecked: decoded['isHiddenChecked'] as bool? ?? false,
isLegacyDerivation: parsedIsLegacy,
isUsed: decoded['isUsed'] as bool? ?? false,
txCount: decoded['txCount'] as int? ?? 0,
@@ -121,6 +123,10 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
);
}
+ /// Whether [isHidden] has been checked against the wallet's keys. Records created from the
+ /// wallet's own keys are correct by construction; older ones are checked once on wallet open.
+ bool isHiddenChecked;
+
String? scriptHash;
static int _purposeForType(BitcoinAddressType type) {
@@ -179,6 +185,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
'index': index,
'accountIndex': accountIndex,
'isHidden': isHidden,
+ 'isHiddenChecked': isHiddenChecked,
'isLegacyDerivation': isLegacyDerivation,
'isUsed': isUsed,
'txCount': txCount,
### cw_bitcoin/lib/electrum_wallet_addresses.dart
@@ -571,6 +571,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
index: newAddressIndex,
accountIndex: accountIndex,
isHidden: false,
+ isHiddenChecked: true,
isLegacyDerivation: false,
name: label,
type: addressPageType,
@@ -957,6 +958,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
await getAddressAsync(index: i, hd: hd, addressType: addrType),
index: i,
isHidden: isHidden,
+ isHiddenChecked: true,
isLegacyDerivation: isLegacyDerivation,
type: addrType,
network: network,
@@ -1023,37 +1025,31 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
}
Future<void> _validateAddress(BitcoinAddressRecord element) async {
+ if (element.isHiddenChecked) return;
+
if (element.type == SegwitAddresType.mweb) {
// this would add a ton of startup lag for mweb addresses since we have 1000 of them
return;
}
- try {
- final mainHd = _hdForAddressGeneration(
- isHidden: false,
- type: element.type,
- isLegacyDerivation: element.isLegacyDerivation,
- accountIndex: element.accountIndex);
- final sideHd = _hdForAddressGeneration(
- isHidden: true,
- type: element.type,
- isLegacyDerivation: element.isLegacyDerivation,
- accountIndex: element.accountIndex);
- if (!element.isHidden &&
- element.address !=
- await getAddressAsync(
- index: element.index, hd: mainHd, addressType: element.type)) {
- element.isHidden = true;
- } else if (element.isHidden &&
- element.address !=
- await getAddressAsync(
- index: element.index, hd: sideHd, addressType: element.type)) {
- element.isHidden = false;
- }
- } on UnsupportedAddressTypeForAccountException catch (e) {
- printV("_validateAddresses: skipping ${element.address}: $e");
+ try {
+ // Relabel only when the address re-derives from the other chain. A record from a path these
+ // keys don't produce matches neither chain, so it keeps its label instead of flipping.
+ final otherChainHd = _hdForAddressGeneration(
+ isHidden: !element.isHidden,
+ type: element.type,
+ isLegacyDerivation: element.isLegacyDerivation,
+ accountIndex: element.accountIndex,
+ );
+ final otherChainAddress =
+ await getAddressAsync(index: element.index, hd: otherChainHd, addressType: element.type);
+ if (element.address == otherChainAddress) {
+ element.isHidden = !element.isHidden;
}
- });
+ element.isHiddenChecked = true;
+ } on UnsupportedAddressTypeForAccountException catch (e) {
+ printV("_validateAddresses: skipping ${element.address}: $e");
+ }
}
@override
### cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart
@@ -150,6 +150,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
addr.address,
index: addr.index,
isHidden: addr.isHidden,
+ isHiddenChecked: addr.isHiddenChecked,
name: addr.name,
type: P2pkhAddressType.p2pkh,
network: BitcoinCashNetwork.mainnet,
@@ -159,6 +160,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
AddressUtils.getCashAddrFormat(addr.address),
index: addr.index,
isHidden: addr.isHidden,
+ isHiddenChecked: addr.isHiddenChecked,
name: addr.name,
type: P2pkhAddressType.p2pkh,
network: BitcoinCashNetwork.mainnet,Why this scored 29/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.