2021-10-04 04:18:37 -07:00
|
|
|
package cash.z.ecc.android.sdk.internal
|
2021-01-22 14:05:11 -08:00
|
|
|
|
2022-09-06 03:44:33 -07:00
|
|
|
import android.content.Context
|
2021-01-22 14:05:11 -08:00
|
|
|
import cash.z.ecc.android.sdk.exception.TransactionEncoderException
|
2022-08-12 08:05:00 -07:00
|
|
|
import cash.z.ecc.android.sdk.internal.ext.deleteRecursivelySuspend
|
2022-09-06 03:44:33 -07:00
|
|
|
import cash.z.ecc.android.sdk.internal.ext.deleteSuspend
|
2022-08-12 08:05:00 -07:00
|
|
|
import cash.z.ecc.android.sdk.internal.ext.existsSuspend
|
2022-09-06 03:44:33 -07:00
|
|
|
import cash.z.ecc.android.sdk.internal.ext.getCacheDirSuspend
|
|
|
|
import cash.z.ecc.android.sdk.internal.ext.getSha1Hash
|
2022-08-12 08:05:00 -07:00
|
|
|
import cash.z.ecc.android.sdk.internal.ext.mkdirsSuspend
|
2022-09-06 03:44:33 -07:00
|
|
|
import cash.z.ecc.android.sdk.internal.ext.renameToSuspend
|
2021-01-22 14:05:11 -08:00
|
|
|
import kotlinx.coroutines.Dispatchers
|
2022-09-26 22:03:36 -07:00
|
|
|
import kotlinx.coroutines.delay
|
2022-09-06 03:44:33 -07:00
|
|
|
import kotlinx.coroutines.sync.Mutex
|
|
|
|
import kotlinx.coroutines.sync.withLock
|
2021-01-22 14:05:11 -08:00
|
|
|
import kotlinx.coroutines.withContext
|
|
|
|
import java.io.File
|
2022-09-06 03:44:33 -07:00
|
|
|
import java.io.IOException
|
2022-08-25 05:36:57 -07:00
|
|
|
import java.net.URL
|
|
|
|
import java.nio.channels.Channels
|
2022-09-26 22:03:36 -07:00
|
|
|
import kotlin.time.Duration.Companion.milliseconds
|
2022-08-25 05:36:57 -07:00
|
|
|
|
2022-09-06 03:44:33 -07:00
|
|
|
internal class SaplingParamTool(val properties: SaplingParamToolProperties) {
|
2023-05-18 04:36:15 -07:00
|
|
|
|
|
|
|
val spendParamsFile: File
|
|
|
|
get() = File(properties.paramsDirectory, SPEND_PARAM_FILE_NAME)
|
|
|
|
|
|
|
|
val outputParamsFile: File
|
|
|
|
get() = File(properties.paramsDirectory, OUTPUT_PARAM_FILE_NAME)
|
|
|
|
|
2022-09-06 03:44:33 -07:00
|
|
|
companion object {
|
|
|
|
/**
|
|
|
|
* Maximum file size for the sapling spend params - 50MB
|
|
|
|
*/
|
|
|
|
internal const val SPEND_PARAM_FILE_MAX_BYTES_SIZE = 50L * 1024L * 1024L
|
2022-08-25 05:36:57 -07:00
|
|
|
|
2022-09-06 03:44:33 -07:00
|
|
|
/**
|
|
|
|
* Maximum file size for the sapling spend params - 5MB
|
|
|
|
*/
|
|
|
|
internal const val OUTPUT_PARAM_FILE_MAX_BYTES_SIZE = 5L * 1024L * 1024L
|
2022-08-25 05:36:57 -07:00
|
|
|
|
2022-09-06 03:44:33 -07:00
|
|
|
/**
|
|
|
|
* Subdirectory name, in which are the sapling params files stored.
|
|
|
|
*/
|
|
|
|
internal const val SAPLING_PARAMS_LEGACY_SUBDIRECTORY = "params"
|
2021-01-22 14:05:11 -08:00
|
|
|
|
2022-09-06 03:44:33 -07:00
|
|
|
/**
|
|
|
|
* File name for the sapling spend params
|
|
|
|
*/
|
|
|
|
internal const val SPEND_PARAM_FILE_NAME = "sapling-spend.params"
|
2021-01-22 14:05:11 -08:00
|
|
|
|
2022-09-06 03:44:33 -07:00
|
|
|
/**
|
|
|
|
* File name for the sapling output params
|
|
|
|
*/
|
|
|
|
internal const val OUTPUT_PARAM_FILE_NAME = "sapling-output.params"
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Temporary file prefix to fulfill atomicity requirement of file handling
|
|
|
|
*/
|
|
|
|
private const val TEMPORARY_FILE_NAME_PREFIX = "_"
|
|
|
|
|
|
|
|
/**
|
|
|
|
* File SHA1 hash for the sapling spend params
|
|
|
|
*/
|
|
|
|
internal const val SPEND_PARAM_FILE_SHA1_HASH = "a15ab54c2888880e53c823a3063820c728444126"
|
|
|
|
|
|
|
|
/**
|
|
|
|
* File SHA1 hash for the sapling output params
|
|
|
|
*/
|
|
|
|
internal const val OUTPUT_PARAM_FILE_SHA1_HASH = "0ebc5a1ef3653948e1c46cf7a16071eac4b7e352"
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The Url that is used by default in zcashd
|
|
|
|
*/
|
|
|
|
private const val CLOUD_PARAM_DIR_URL = "https://z.cash/downloads/"
|
|
|
|
|
|
|
|
private val checkFilesMutex = Mutex()
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initialization of needed properties. This is necessary entry point for other operations from {@code
|
|
|
|
* SaplingParamTool}. This type of implementation also simplifies its testing.
|
|
|
|
*
|
|
|
|
* @param context
|
|
|
|
*/
|
|
|
|
internal suspend fun new(context: Context): SaplingParamTool {
|
|
|
|
val paramsDirectory = Files.getZcashNoBackupSubdirectory(context)
|
|
|
|
val toolProperties = SaplingParamToolProperties(
|
|
|
|
paramsDirectory = paramsDirectory,
|
|
|
|
paramsLegacyDirectory = File(context.getCacheDirSuspend(), SAPLING_PARAMS_LEGACY_SUBDIRECTORY),
|
|
|
|
saplingParams = listOf(
|
|
|
|
SaplingParameters(
|
|
|
|
paramsDirectory,
|
|
|
|
SPEND_PARAM_FILE_NAME,
|
|
|
|
SPEND_PARAM_FILE_MAX_BYTES_SIZE,
|
|
|
|
SPEND_PARAM_FILE_SHA1_HASH
|
|
|
|
),
|
|
|
|
SaplingParameters(
|
|
|
|
paramsDirectory,
|
|
|
|
OUTPUT_PARAM_FILE_NAME,
|
|
|
|
OUTPUT_PARAM_FILE_MAX_BYTES_SIZE,
|
|
|
|
OUTPUT_PARAM_FILE_SHA1_HASH
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return SaplingParamTool(toolProperties)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns file object pointing to the parameters files parent directory. We need to check if the parameters
|
|
|
|
* files don't sit in the legacy folder first. If they do, then we move the files to the currently used
|
|
|
|
* directory and validate files hashes.
|
|
|
|
*
|
|
|
|
* @return params destination directory file
|
|
|
|
*/
|
|
|
|
internal suspend fun initAndGetParamsDestinationDir(toolProperties: SaplingParamToolProperties): File {
|
|
|
|
checkFilesMutex.withLock {
|
|
|
|
toolProperties.saplingParams.forEach {
|
|
|
|
val legacyFile = File(toolProperties.paramsLegacyDirectory, it.fileName)
|
|
|
|
val currentFile = File(toolProperties.paramsDirectory, it.fileName)
|
|
|
|
|
|
|
|
if (legacyFile.existsSuspend() && isFileHashValid(legacyFile, it.fileHash)) {
|
2023-02-06 14:36:28 -08:00
|
|
|
Twig.debug {
|
|
|
|
"Moving params file: ${it.fileName} from legacy folder to the currently used " +
|
|
|
|
"folder."
|
|
|
|
}
|
2022-09-06 03:44:33 -07:00
|
|
|
currentFile.parentFile?.mkdirsSuspend()
|
|
|
|
if (!renameParametersFile(legacyFile, currentFile)) {
|
2023-02-06 14:36:28 -08:00
|
|
|
Twig.debug {
|
|
|
|
"Failed while moving the params file: ${it.fileName} to the preferred " +
|
|
|
|
"location."
|
|
|
|
}
|
2022-09-06 03:44:33 -07:00
|
|
|
}
|
|
|
|
} else {
|
2023-02-06 14:36:28 -08:00
|
|
|
Twig.debug {
|
2022-09-06 03:44:33 -07:00
|
|
|
"Legacy file either does not exist or is not valid. Will be fetched to the preferred " +
|
|
|
|
"location."
|
2023-02-06 14:36:28 -08:00
|
|
|
}
|
2022-09-06 03:44:33 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// remove the params folder and its files - a new sapling files will be fetched to the preferred
|
|
|
|
// location
|
|
|
|
toolProperties.paramsLegacyDirectory.deleteRecursivelySuspend()
|
|
|
|
}
|
|
|
|
|
|
|
|
return toolProperties.paramsDirectory
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Compares the input file parameter SHA1 hash with the given input hash.
|
|
|
|
*
|
|
|
|
* @param parametersFile file of which SHA1 hash will be checked
|
|
|
|
* @param fileHash hash to compare with
|
|
|
|
*
|
|
|
|
* @return true in case of hashes are the same, false otherwise
|
|
|
|
*/
|
|
|
|
private suspend fun isFileHashValid(parametersFile: File, fileHash: String): Boolean {
|
|
|
|
return try {
|
|
|
|
fileHash == parametersFile.getSha1Hash()
|
|
|
|
} catch (e: IOException) {
|
2023-02-06 14:36:28 -08:00
|
|
|
Twig.debug { "Failed in comparing file's hashes with: ${e.message}, caused by: ${e.cause}." }
|
2022-09-06 03:44:33 -07:00
|
|
|
false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The purpose of this function is to rename parameters file from the old name (given by the {@code
|
|
|
|
* fromParamFile} parameter) to the new name (given by {@code toParamFile}). This operation covers also the file
|
|
|
|
* move, if it's in a different location.
|
|
|
|
*
|
|
|
|
* @param fromParamFile the previously used file name/location
|
|
|
|
* @param toParamFile the newly used file name/location
|
|
|
|
*/
|
|
|
|
private suspend fun renameParametersFile(
|
|
|
|
fromParamFile: File,
|
|
|
|
toParamFile: File
|
|
|
|
): Boolean {
|
|
|
|
return runCatching {
|
|
|
|
return@runCatching fromParamFile.renameToSuspend(toParamFile)
|
|
|
|
}.onFailure {
|
2023-02-06 14:36:28 -08:00
|
|
|
Twig.debug(it) { "Failed while renaming parameters file" }
|
2022-09-06 03:44:33 -07:00
|
|
|
}.getOrDefault(false)
|
|
|
|
}
|
|
|
|
}
|
2022-08-26 06:31:56 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks the given directory for the output and spending params and calls [fetchParams] for those, which are
|
|
|
|
* missing.
|
|
|
|
*
|
2022-09-06 03:44:33 -07:00
|
|
|
* Note: Don't forget to call the entry point function {@code initSaplingParamTool} first. Make sure you also
|
|
|
|
* called {@code initAndGetParamsDestinationDir} previously, as it's always better to check the
|
|
|
|
* legacy destination folder first.
|
|
|
|
*
|
2022-08-26 06:31:56 -07:00
|
|
|
* @param destinationDir the directory where the params should be stored.
|
|
|
|
*
|
|
|
|
* @throws TransactionEncoderException.MissingParamsException in case of failure while checking sapling params
|
2022-09-26 22:03:36 -07:00
|
|
|
* @throws TransactionEncoderException.FetchParamsException
|
|
|
|
* @throws TransactionEncoderException.ValidateParamsException
|
2022-08-26 06:31:56 -07:00
|
|
|
* files
|
|
|
|
*/
|
2022-09-26 22:03:36 -07:00
|
|
|
@Throws(
|
|
|
|
TransactionEncoderException.ValidateParamsException::class,
|
|
|
|
TransactionEncoderException.FetchParamsException::class,
|
|
|
|
TransactionEncoderException.MissingParamsException::class
|
|
|
|
)
|
2022-09-06 03:44:33 -07:00
|
|
|
internal suspend fun ensureParams(destinationDir: File) {
|
|
|
|
properties.saplingParams.filter {
|
|
|
|
!File(it.destinationDirectory, it.fileName).existsSuspend()
|
2022-08-26 06:31:56 -07:00
|
|
|
}.forEach {
|
|
|
|
try {
|
2023-02-06 14:36:28 -08:00
|
|
|
Twig.debug { "Attempting to download missing params: ${it.fileName}." }
|
2022-08-26 06:31:56 -07:00
|
|
|
fetchParams(it)
|
|
|
|
} catch (e: TransactionEncoderException.FetchParamsException) {
|
2023-02-06 14:36:28 -08:00
|
|
|
Twig.debug {
|
2022-09-26 22:03:36 -07:00
|
|
|
"Failed to fetch param file ${it.fileName} due to: $e. The second attempt is starting with a " +
|
|
|
|
"little delay."
|
2023-02-06 14:36:28 -08:00
|
|
|
}
|
2022-09-26 22:03:36 -07:00
|
|
|
// Re-run the fetch with a little delay, if it failed previously (as it can be caused by network
|
|
|
|
// conditions). We do it only once, the next failure is delivered to the caller of this method.
|
|
|
|
delay(200.milliseconds)
|
|
|
|
fetchParams(it)
|
|
|
|
} catch (e: TransactionEncoderException.ValidateParamsException) {
|
2023-02-06 14:36:28 -08:00
|
|
|
Twig.debug {
|
2022-09-26 22:03:36 -07:00
|
|
|
"Failed to validate fetched param file ${it.fileName} due to: $e. The second attempt is starting" +
|
|
|
|
" now."
|
2023-02-06 14:36:28 -08:00
|
|
|
}
|
2022-09-26 22:03:36 -07:00
|
|
|
// Re-run the fetch for invalid param file immediately, if it failed previously. We do it again only
|
|
|
|
// once, the next failure is delivered to the caller of this method.
|
|
|
|
fetchParams(it)
|
2021-01-22 14:05:11 -08:00
|
|
|
}
|
2022-08-26 06:31:56 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!validate(destinationDir)) {
|
2023-02-06 14:36:28 -08:00
|
|
|
Twig.debug { "Fetching sapling params files failed." }
|
2022-08-26 06:31:56 -07:00
|
|
|
throw TransactionEncoderException.MissingParamsException
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Download and store the params file into the given directory. It also checks the file size to eliminate the
|
|
|
|
* risk of downloading potentially large file from a malicious server.
|
|
|
|
*
|
|
|
|
* @param paramsToFetch parameters wrapper class, which holds information about it
|
|
|
|
*
|
|
|
|
* @throws TransactionEncoderException.FetchParamsException if any error while downloading the params file occurs
|
2022-09-26 22:03:36 -07:00
|
|
|
* @throws TransactionEncoderException.ValidateParamsException if a failure in validation of fetched file occurs
|
2022-08-26 06:31:56 -07:00
|
|
|
*/
|
2022-09-26 22:03:36 -07:00
|
|
|
@Throws(
|
|
|
|
TransactionEncoderException.ValidateParamsException::class,
|
|
|
|
TransactionEncoderException.FetchParamsException::class
|
|
|
|
)
|
2022-09-06 03:44:33 -07:00
|
|
|
internal suspend fun fetchParams(paramsToFetch: SaplingParameters) {
|
|
|
|
val url = URL("$CLOUD_PARAM_DIR_URL/${paramsToFetch.fileName}")
|
|
|
|
val temporaryFile = File(
|
|
|
|
paramsToFetch.destinationDirectory,
|
|
|
|
"$TEMPORARY_FILE_NAME_PREFIX${paramsToFetch.fileName}"
|
|
|
|
)
|
2022-08-26 06:31:56 -07:00
|
|
|
|
|
|
|
withContext(Dispatchers.IO) {
|
|
|
|
runCatching {
|
|
|
|
Channels.newChannel(url.openStream()).use { readableByteChannel ->
|
2022-09-06 03:44:33 -07:00
|
|
|
temporaryFile.outputStream().use { fileOutputStream ->
|
2022-08-26 06:31:56 -07:00
|
|
|
fileOutputStream.channel.use { fileChannel ->
|
|
|
|
// Transfers bytes from stream to file from position 0 to end position or to max
|
|
|
|
// file size limit. This eliminates the risk of downloading potentially large files
|
|
|
|
// from a malicious server. We need to make a check of the file hash then.
|
|
|
|
fileChannel.transferFrom(readableByteChannel, 0, paramsToFetch.fileMaxSizeBytes)
|
|
|
|
}
|
2021-01-22 14:05:11 -08:00
|
|
|
}
|
|
|
|
}
|
2022-08-26 06:31:56 -07:00
|
|
|
}.onFailure { exception ->
|
|
|
|
// IllegalArgumentException - If the preconditions on the parameters do not hold
|
|
|
|
// NonReadableChannelException - If the source channel was not opened for reading
|
|
|
|
// NonWritableChannelException - If this channel was not opened for writing
|
|
|
|
// ClosedChannelException - If either this channel or the source channel is closed
|
|
|
|
// AsynchronousCloseException - If another thread closes either channel while the transfer is
|
|
|
|
// in progress
|
|
|
|
// ClosedByInterruptException - If another thread interrupts the current thread while the
|
|
|
|
// transfer is in progress, thereby closing both channels and setting the current thread's
|
|
|
|
// interrupt status
|
|
|
|
// IOException - If some other I/O error occurs
|
2022-09-26 22:03:36 -07:00
|
|
|
finalizeAndReportError(
|
|
|
|
temporaryFile,
|
|
|
|
exception = TransactionEncoderException.FetchParamsException(
|
|
|
|
paramsToFetch,
|
|
|
|
"Error while fetching ${paramsToFetch.fileName}, caused by $exception."
|
|
|
|
)
|
|
|
|
)
|
2022-08-26 06:31:56 -07:00
|
|
|
}.onSuccess {
|
2023-02-06 14:36:28 -08:00
|
|
|
Twig.debug {
|
2022-09-06 03:44:33 -07:00
|
|
|
"Fetch and write of the temporary ${temporaryFile.name} succeeded. Validating and moving it to " +
|
2022-09-26 22:03:36 -07:00
|
|
|
"the final destination."
|
2023-02-06 14:36:28 -08:00
|
|
|
}
|
2022-09-06 03:44:33 -07:00
|
|
|
if (!isFileHashValid(temporaryFile, paramsToFetch.fileHash)) {
|
|
|
|
finalizeAndReportError(
|
|
|
|
temporaryFile,
|
2022-09-26 22:03:36 -07:00
|
|
|
exception = TransactionEncoderException.ValidateParamsException(
|
|
|
|
paramsToFetch,
|
|
|
|
"Failed while validating fetched param file: ${paramsToFetch.fileName}."
|
|
|
|
)
|
2022-09-06 03:44:33 -07:00
|
|
|
)
|
|
|
|
}
|
|
|
|
val resultFile = File(paramsToFetch.destinationDirectory, paramsToFetch.fileName)
|
|
|
|
if (!renameParametersFile(temporaryFile, resultFile)) {
|
|
|
|
finalizeAndReportError(
|
|
|
|
temporaryFile,
|
|
|
|
resultFile,
|
2022-09-26 22:03:36 -07:00
|
|
|
exception = TransactionEncoderException.ValidateParamsException(
|
|
|
|
paramsToFetch,
|
|
|
|
"Failed while renaming result param file: ${paramsToFetch.fileName}."
|
|
|
|
)
|
2022-09-06 03:44:33 -07:00
|
|
|
)
|
|
|
|
}
|
2021-01-22 14:05:11 -08:00
|
|
|
}
|
|
|
|
}
|
2022-08-26 06:31:56 -07:00
|
|
|
}
|
2021-01-22 14:05:11 -08:00
|
|
|
|
2022-09-06 03:44:33 -07:00
|
|
|
@Throws(TransactionEncoderException.FetchParamsException::class)
|
2022-09-26 22:03:36 -07:00
|
|
|
private suspend fun finalizeAndReportError(vararg files: File, exception: TransactionEncoderException) {
|
2022-09-06 03:44:33 -07:00
|
|
|
files.forEach {
|
|
|
|
it.deleteSuspend()
|
|
|
|
}
|
2022-09-26 22:03:36 -07:00
|
|
|
exception.also {
|
2023-02-06 14:36:28 -08:00
|
|
|
Twig.debug(it) { "Error while fetching sapling params files." }
|
2022-09-26 22:03:36 -07:00
|
|
|
throw it
|
2021-01-22 14:05:11 -08:00
|
|
|
}
|
2022-08-26 06:31:56 -07:00
|
|
|
}
|
2021-01-22 14:05:11 -08:00
|
|
|
|
2022-09-06 03:44:33 -07:00
|
|
|
internal suspend fun validate(destinationDir: File): Boolean {
|
2022-08-26 06:31:56 -07:00
|
|
|
return arrayOf(
|
2022-09-06 03:44:33 -07:00
|
|
|
SPEND_PARAM_FILE_NAME,
|
|
|
|
OUTPUT_PARAM_FILE_NAME
|
2022-08-26 06:31:56 -07:00
|
|
|
).all { paramFileName ->
|
|
|
|
File(destinationDir, paramFileName).existsSuspend()
|
|
|
|
}.also {
|
2023-02-06 14:36:28 -08:00
|
|
|
Twig.debug { "Param files ${if (!it) "did not" else ""} both exist!" }
|
2021-01-22 14:05:11 -08:00
|
|
|
}
|
|
|
|
}
|
2021-03-10 10:10:03 -08:00
|
|
|
}
|
2022-08-26 06:31:56 -07:00
|
|
|
|
|
|
|
/**
|
2022-09-06 03:44:33 -07:00
|
|
|
* Sapling file parameter class to hold each sapling file attributes.
|
|
|
|
*/
|
|
|
|
internal data class SaplingParameters(
|
|
|
|
val destinationDirectory: File,
|
2022-09-26 22:03:36 -07:00
|
|
|
val fileName: String,
|
2022-09-06 03:44:33 -07:00
|
|
|
val fileMaxSizeBytes: Long,
|
2022-09-26 22:03:36 -07:00
|
|
|
val fileHash: String
|
2022-09-06 03:44:33 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sapling param tool helper properties. The goal of this implementation is to ease its testing.
|
2022-08-26 06:31:56 -07:00
|
|
|
*/
|
2022-09-06 03:44:33 -07:00
|
|
|
internal data class SaplingParamToolProperties(
|
|
|
|
val saplingParams: List<SaplingParameters>,
|
|
|
|
val paramsDirectory: File,
|
|
|
|
val paramsLegacyDirectory: File
|
2022-08-26 06:31:56 -07:00
|
|
|
)
|