fetch params from download.z.cash on demand to reduce app size (#2879)
What changed, and why it matters
This commit changes the Cake Wallet app so that it no longer ships with two large Zcash cryptographic files inside the app bundle. Instead, the app downloads those files from download.z.cash the first time it needs them, and saves them to the app's cache folder. The same commit also adds the wallet's restore height to the seed/keys screen for Zcash wallets. The change is a size optimization, but it introduces a new network download of security-critical parameters and stores them on disk, which could matter if the download or storage is tampered with.
Treat this as a security-sensitive change that needs hardening before release. Add cryptographic verification of the downloaded parameter files against known trusted hashes or signatures before passing them to `WarpApi.initProver()`. Guard `loadProver()` against concurrent execution and redundant downloads. Consider shipping a small, signed manifest or using a verified, tamper-evident delivery channel. Review whether `ProxyWrapper` provides sufficient transport security and whether the cache directory is protected from modification by other apps on the device. The restore-height UI change should be reviewed for privacy impact but appears lower risk.
Security signals we found
Security-critical Zcash proving parameters are now downloaded from the public internet instead of shipped inside the signed app bundle
Downloaded parameters are written to the application cache directory and reused without integrity verification (no hash/signature check visible in the diff)
The download uses `ProxyWrapper().get()` with a clearnet URI; the diff does not show certificate pinning, signature verification, or fallback to a trusted source
A static `isProverLoaded` boolean is introduced but the diff shows no guard preventing concurrent or redundant downloads/initializations
The catch block treats any read failure (including permission or disk errors) as a signal to download from the network, which could enable downgrade or substitution if local files are corrupted or replaced
The commit also exposes `restoreHeight` in wallet keys/seed screen, which is a privacy-relevant metadata leak to the UI but not an exploit path by itself
Evidence from the diff
The patch removes bundled sapling-spend.params and sapling-output.params loads via rootBundle.load() and replaces them with an on-demand loadProver() routine. loadProver() first tries to read the params from the app cache, and if missing/empty it fetches them over HTTPS from https://download.z.cash/downloads/ using ProxyWrapper().get(), writes them to the cache directory, then passes the bytes to WarpApi.initProver(). Calls to loadProver() are added before each WarpApi.prepareTx() invocation and during Zcash wallet initialization. A static isProverLoaded flag is tracked but not used to prevent redundant loads. Additionally, lastKnownRestoreHeight is persisted and exposed in getKeys(), and the seed screen is updated to show a height box for Zcash.
Changed components
cw_zcash/lib/src/zcash_wallet.dartcw_zcash/lib/src/zcash_taddress_rotation.dartlib/src/screens/wallet_keys/wallet_keys_page.dartlib/view_model/wallet_keys_view_model.dartZcash transaction creation / proving flowZcash wallet initializationWallet seed/keys screenInspect captured patch +52 / −6
diff --git a/cw_zcash/lib/src/zcash_taddress_rotation.dart b/cw_zcash/lib/src/zcash_taddress_rotation.dart
index 14bdc530..1c2ef1f2 100644
--- a/cw_zcash/lib/src/zcash_taddress_rotation.dart
+++ b/cw_zcash/lib/src/zcash_taddress_rotation.dart
@@ -313,6 +313,7 @@ class ZcashTaddressRotation {
final recipient = Recipient(recipientBuilder.toBytes());
final fee = FeeT(fee: 10000, minFee: 0, maxFee: 0, scheme: 0);
+ await ZcashWalletBase.loadProver();
final txPlan = await ZcashWalletService.runInDbMutex(
() => WarpApi.prepareTx(
coin,
diff --git a/cw_zcash/lib/src/zcash_wallet.dart b/cw_zcash/lib/src/zcash_wallet.dart
index 4599fa9b..bfce46d4 100644
--- a/cw_zcash/lib/src/zcash_wallet.dart
+++ b/cw_zcash/lib/src/zcash_wallet.dart
@@ -11,6 +11,7 @@ import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/utils/print_verbose.dart';
+import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/wallet_addresses.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_credentials.dart';
@@ -220,6 +221,7 @@ abstract class ZcashWalletBase
// pools parameter: bitmask for which pools to use for sending
// 1=Transparent, 2=Sapling, 4=Orchard, 7=All pools
+ await ZcashWalletBase.loadProver();
// Using 7 (all pools) allows spending from any pool type
final txPlan = await ZcashWalletService.runInDbMutex(
() => WarpApi.prepareTx(
@@ -333,6 +335,8 @@ abstract class ZcashWalletBase
"privateViewKey": backup.fvk,
"uvk": backup.uvk,
"tsk": backup.tsk,
+ if (lastKnownRestoreHeight != null)
+ "restoreHeight": lastKnownRestoreHeight.toString(),
};
}
@@ -354,12 +358,14 @@ abstract class ZcashWalletBase
bool get hasRescan => true;
static Future<void> storeZcashHeight(final int height) async {
+ lastKnownRestoreHeight = height;
final zcashDir = await pathForWalletTypeDir(type: WalletType.zcash);
final zcashInitialSync = File(p.join(zcashDir, ".initial-sync-marker"));
zcashInitialSync.writeAsBytesSync([0x00]);
zcashInitialSync.writeAsStringSync(height.toString(), mode: FileMode.writeOnlyAppend);
}
+ static int? lastKnownRestoreHeight = null;
static Future<int?> loadZcashHeight() async {
final zcashDir = await pathForWalletTypeDir(type: WalletType.zcash);
final zcashInitialSync = File(p.join(zcashDir, ".initial-sync-marker"));
@@ -371,7 +377,8 @@ abstract class ZcashWalletBase
return null;
}
final heightString = String.fromCharCodes(bytes.skip(1));
- return int.tryParse(heightString);
+ lastKnownRestoreHeight = int.tryParse(heightString);
+ return lastKnownRestoreHeight;
}
static int zashiAnnouncedBlockHeight = 2419420;
@@ -749,6 +756,7 @@ abstract class ZcashWalletBase
final recipient = Recipient(recipientBuilder.toBytes());
final fee = FeeT(fee: 10000, minFee: 0, maxFee: 0, scheme: 0);
+ await ZcashWalletBase.loadProver();
final txPlan = await ZcashWalletService.runInDbMutex(
() => WarpApi.prepareTx(
coin,
@@ -1039,13 +1047,45 @@ abstract class ZcashWalletBase
} catch (e) {
printV("zec init failed: $e");
} // do not fail on network exception
- final spend = await rootBundle.load('scripts/zcash_lib/assets/sapling-spend.params');
- final output = await rootBundle.load('scripts/zcash_lib/assets/sapling-output.params');
- WarpApi.initProver(spend.buffer.asUint8List(), output.buffer.asUint8List());
+ await loadZcashHeight();
+
+ unawaited(loadProver());
+
await ZcashTaddressRotation.init();
await ZcashTransactionInfo.init();
_initialized = true;
}
+
+ static bool isProverLoaded = false;
+ static Future<void> loadProver() async {
+ Uint8List? spend;
+ Uint8List? output;
+ final cacheDir = await getApplicationCacheDirectory();
+ try {
+ final spendBundle = await rootBundle.load('scripts/zcash_lib/assets/sapling-spend.params');
+ final outputBundle = await rootBundle.load('scripts/zcash_lib/assets/sapling-output.params');
+ spend = spendBundle.buffer.asUint8List();
+ output = outputBundle.buffer.asUint8List();
+ if (spend.length == 0 || output.length == 0) throw Exception("NUH UH");
+ spend = await File(cacheDir.path+"/sapling-spend.params").readAsBytesSync();
+ output = await File(cacheDir.path+"/sapling-output.params").readAsBytesSync();
+ if (spend.length == 0 || output.length == 0) throw Exception("NUH UH");
+ } catch (e) {
+ printV("$e. Fine, I'll download them.");
+ final spendResponse = await ProxyWrapper().get(
+ clearnetUri: Uri.parse("https://download.z.cash/downloads/sapling-spend.params"),
+ );
+ final outputResponse = await ProxyWrapper().get(
+ clearnetUri: Uri.parse("https://download.z.cash/downloads/sapling-output.params"),
+ );
+ spend = spendResponse.bodyBytes;
+ output = outputResponse.bodyBytes;
+ await File(cacheDir.path+"/sapling-spend.params").writeAsBytes(spend);
+ await File(cacheDir.path+"/sapling-output.params").writeAsBytes(output);
+ }
+ WarpApi.initProver(spend, output);
+ isProverLoaded = true;
+ }
static Future<int> getBlockHeightByTime(final DateTime time) async {
final genesisTime = DateTime.utc(2016, 10, 28);
diff --git a/lib/src/screens/wallet_keys/wallet_keys_page.dart b/lib/src/screens/wallet_keys/wallet_keys_page.dart
index 4048636f..1a250ef4 100644
--- a/lib/src/screens/wallet_keys/wallet_keys_page.dart
+++ b/lib/src/screens/wallet_keys/wallet_keys_page.dart
@@ -183,7 +183,7 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
Widget _buildSeedTab(BuildContext context, bool isLegacySeed) {
return Column(
children: [
- if (isLegacySeedOnly || isLegacySeed || widget.walletKeysViewModel.isBitcoin) ...[
+ if (isLegacySeedOnly || isLegacySeed ||widget.walletKeysViewModel.shouldShowHeightBox) ...[
_buildHeightBox(),
const SizedBox(height: 20),
],
diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart
index 83d14853..a1f68a4c 100644
--- a/lib/view_model/wallet_keys_view_model.dart
+++ b/lib/view_model/wallet_keys_view_model.dart
@@ -62,7 +62,9 @@ abstract class WalletKeysViewModelBase with Store {
}
bool get isBitcoin => _wallet.type == WalletType.bitcoin;
-
+
+ // this is incomplete, needs legacy seed toggle for XMR
+ bool get shouldShowHeightBox => [WalletType.bitcoin, WalletType.zcash].contains(_wallet.type);
final ObservableList<StandartListItem> items;
@observable
@@ -318,6 +320,9 @@ abstract class WalletKeysViewModelBase with Store {
if (_wallet.type == WalletType.wownero) {
return wownero!.getRestoreHeight(_wallet)?.toString();
}
+ if (_wallet.type == WalletType.zcash) {
+ return zcash!.getKeys(_wallet)["restoreHeight"]?.toString();
+ }
if (_restoreHeightByTransactions != 0)
return getRoundedRestoreHeight(_restoreHeightByTransactions);
if (_restoreHeight != 0) return _restoreHeight.toString();
Why this scored 52/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.