Bundle sapling params in APK instead of downloading from remote servers

- Add sapling-spend.params and sapling-output.params to sdk-lib assets
- Rewrite SaplingParamTool to copy params from bundled assets instead of HTTP download
- Remove OkHttp dependency from SaplingParamTool
- Initialize SaplingParamTool with app context from Initializer
This commit is contained in:
2026-03-22 09:13:50 -05:00
parent 36dcb2af32
commit e8a2d3ebc9
4 changed files with 32 additions and 56 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk
import android.content.Context
import cash.z.ecc.android.sdk.exception.InitializerException
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.internal.SaplingParamTool
import cash.z.ecc.android.sdk.internal.SdkDispatchers
import cash.z.ecc.android.sdk.internal.ext.getCacheDirSuspend
import cash.z.ecc.android.sdk.internal.ext.getDatabasePathSuspend
@@ -319,6 +320,7 @@ class Initializer private constructor(
config: Config
): Initializer {
config.validate()
SaplingParamTool.init(context)
val loadedCheckpoint = run {
val height = config.birthdayHeight

View File

@@ -1,21 +1,28 @@
package cash.z.ecc.android.sdk.internal
import android.content.Context
import cash.z.ecc.android.sdk.exception.TransactionEncoderException
import cash.z.ecc.android.sdk.ext.ZcashSdk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import okio.buffer
import okio.sink
import java.io.File
class SaplingParamTool {
companion object {
private var appContext: Context? = null
/**
* Checks the given directory for the output and spending params and calls [fetchParams] if
* they're missing.
* Initialize with application context so params can be copied from bundled assets.
*/
fun init(context: Context) {
appContext = context.applicationContext
}
/**
* Checks the given directory for the output and spending params and copies them from
* bundled assets if they're missing.
*
* @param destinationDir the directory where the params should be stored.
*/
@@ -32,64 +39,44 @@ class SaplingParamTool {
}
if (hadError) {
try {
Bush.trunk.twigTask("attempting to download missing params") {
fetchParams(destinationDir)
Bush.trunk.twigTask("copying bundled sapling params") {
copyBundledParams(destinationDir)
}
} catch (e: Throwable) {
twig("failed to fetch params due to: $e")
twig("failed to copy bundled params due to: $e")
throw TransactionEncoderException.MissingParamsException
}
}
}
/**
* Download and store the params into the given directory.
* Copy the sapling params from bundled assets into the given directory.
*
* @param destinationDir the directory where the params will be stored. It's assumed that we
* have write access to this directory. Typically, this should be the app's cache directory
* because it is not harmful if these files are cleared by the user since they are downloaded
* on-demand.
* @param destinationDir the directory where the params will be stored.
*/
suspend fun fetchParams(destinationDir: String) {
val client = createHttpClient()
var failureMessage = ""
private suspend fun copyBundledParams(destinationDir: String) {
val context = appContext
?: throw IllegalStateException("SaplingParamTool not initialized. Call init(context) first.")
arrayOf(
ZcashSdk.SPEND_PARAM_FILE_NAME,
ZcashSdk.OUTPUT_PARAM_FILE_NAME
).forEach { paramFileName ->
val url = "${ZcashSdk.CLOUD_PARAM_DIR_URL.random()}/$paramFileName"
twig("Downloading Sapling params from ${url}...")
val request = Request.Builder().url(url).build()
val response = withContext(Dispatchers.IO) { client.newCall(request).execute() }
if (response.isSuccessful) {
twig("fetch succeeded", -1)
val file = File(destinationDir, paramFileName)
if (file.parentFile?.existsSuspend() == true) {
twig("directory exists!", -1)
} else {
twig("directory did not exist attempting to make it")
file.parentFile?.mkdirsSuspend()
val destFile = File(destinationDir, paramFileName)
if (!destFile.existsSuspend()) {
if (destFile.parentFile?.existsSuspend() != true) {
destFile.parentFile?.mkdirsSuspend()
}
withContext(Dispatchers.IO) {
response.body?.let { body ->
body.source().use { source ->
file.sink().buffer().use { sink ->
twig("writing to $file")
sink.writeAll(source)
}
context.assets.open("params/$paramFileName").use { input ->
destFile.outputStream().use { output ->
twig("copying bundled $paramFileName to $destFile")
input.copyTo(output)
}
}
}
} else {
failureMessage += "Error while fetching $paramFileName : $response\n"
twig(failureMessage)
}
twig("fetch succeeded, done writing $paramFileName")
}
if (failureMessage.isNotEmpty()) throw TransactionEncoderException.FetchParamsException(
failureMessage
)
}
suspend fun clear(destinationDir: String) {
@@ -119,19 +106,6 @@ class SaplingParamTool {
}
}
//
// Helpers
//
/**
* Http client is only used for downloading sapling spend and output params data, which are
* necessary for the wallet to scan blocks.
*
* @return an http client suitable for downloading params data.
*/
private fun createHttpClient(): OkHttpClient {
// TODO: add logging and timeouts
return OkHttpClient()
}
}
}