2020-09-11 00:33:25 -07:00
|
|
|
package cash.z.ecc.android.sdk.tool
|
|
|
|
|
|
|
|
import android.content.Context
|
2021-09-11 07:38:52 -07:00
|
|
|
import androidx.annotation.VisibleForTesting
|
2020-09-11 00:33:25 -07:00
|
|
|
import cash.z.ecc.android.sdk.exception.BirthdayException
|
2022-06-13 06:16:51 -07:00
|
|
|
import cash.z.ecc.android.sdk.internal.from
|
2022-07-12 05:40:09 -07:00
|
|
|
import cash.z.ecc.android.sdk.internal.model.Checkpoint
|
2021-10-13 07:20:13 -07:00
|
|
|
import cash.z.ecc.android.sdk.internal.twig
|
2022-07-12 05:40:09 -07:00
|
|
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
2021-04-09 18:43:07 -07:00
|
|
|
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
2021-10-21 13:05:02 -07:00
|
|
|
import kotlinx.coroutines.Dispatchers
|
|
|
|
import kotlinx.coroutines.withContext
|
2022-06-13 06:16:51 -07:00
|
|
|
import java.io.BufferedReader
|
2021-09-11 07:38:52 -07:00
|
|
|
import java.io.IOException
|
2022-06-13 06:16:51 -07:00
|
|
|
import java.util.*
|
2020-09-11 00:33:25 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Tool for loading checkpoints for the wallet, based on the height at which the wallet was born.
|
|
|
|
*/
|
2022-07-12 05:40:09 -07:00
|
|
|
internal object CheckpointTool {
|
2021-10-21 13:05:02 -07:00
|
|
|
|
|
|
|
// Behavior change implemented as a fix for issue #270. Temporarily adding a boolean
|
|
|
|
// that allows the change to be rolled back quickly if needed, although long-term
|
|
|
|
// this flag should be removed.
|
|
|
|
@VisibleForTesting
|
|
|
|
internal val IS_FALLBACK_ON_FAILURE = true
|
2020-09-11 00:33:25 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Load the nearest checkpoint to the given birthday height. If null is given, then this
|
|
|
|
* will load the most recent checkpoint available.
|
|
|
|
*/
|
2021-10-21 13:05:02 -07:00
|
|
|
suspend fun loadNearest(
|
|
|
|
context: Context,
|
|
|
|
network: ZcashNetwork,
|
2022-07-12 05:40:09 -07:00
|
|
|
birthdayHeight: BlockHeight?
|
|
|
|
): Checkpoint {
|
2021-10-21 13:05:02 -07:00
|
|
|
// TODO: potentially pull from shared preferences first
|
2022-07-12 05:40:09 -07:00
|
|
|
return loadCheckpointFromAssets(context, network, birthdayHeight)
|
2020-09-11 00:33:25 -07:00
|
|
|
}
|
|
|
|
|
2021-10-21 13:05:02 -07:00
|
|
|
/**
|
|
|
|
* Useful for when an exact checkpoint is needed, like for SAPLING_ACTIVATION_HEIGHT. In
|
|
|
|
* most cases, loading the nearest checkpoint is preferred for privacy reasons.
|
|
|
|
*/
|
2022-07-12 05:40:09 -07:00
|
|
|
suspend fun loadExact(context: Context, network: ZcashNetwork, birthday: BlockHeight) =
|
|
|
|
loadNearest(context, network, birthday).also {
|
|
|
|
if (it.height != birthday) {
|
2021-10-21 13:05:02 -07:00
|
|
|
throw BirthdayException.ExactBirthdayNotFoundException(
|
2022-07-12 05:40:09 -07:00
|
|
|
birthday,
|
|
|
|
it
|
2021-10-21 13:05:02 -07:00
|
|
|
)
|
2022-06-23 05:31:02 -07:00
|
|
|
}
|
2020-09-11 00:33:25 -07:00
|
|
|
}
|
|
|
|
|
2021-10-21 13:05:02 -07:00
|
|
|
// Converting this to suspending will then propagate
|
|
|
|
@Throws(IOException::class)
|
2022-07-12 05:40:09 -07:00
|
|
|
internal suspend fun listCheckpointDirectoryContents(context: Context, directory: String) =
|
2021-10-21 13:05:02 -07:00
|
|
|
withContext(Dispatchers.IO) {
|
2021-09-11 07:38:52 -07:00
|
|
|
context.assets.list(directory)
|
2021-09-11 08:21:38 -07:00
|
|
|
}
|
|
|
|
|
2021-10-21 13:05:02 -07:00
|
|
|
/**
|
|
|
|
* Returns the directory within the assets folder where birthday data
|
|
|
|
* (i.e. sapling trees for a given height) can be found.
|
|
|
|
*/
|
|
|
|
@VisibleForTesting
|
2022-07-12 05:40:09 -07:00
|
|
|
internal fun checkpointDirectory(network: ZcashNetwork) =
|
|
|
|
"co.electriccoin.zcash/checkpoint/${
|
|
|
|
(network.networkName as java.lang.String).toLowerCase(
|
|
|
|
Locale.ROOT
|
|
|
|
)
|
|
|
|
}"
|
2021-09-11 08:21:38 -07:00
|
|
|
|
2022-07-12 05:40:09 -07:00
|
|
|
internal fun checkpointHeightFromFilename(zcashNetwork: ZcashNetwork, fileName: String) =
|
|
|
|
BlockHeight.new(zcashNetwork, fileName.split('.').first().toLong())
|
2021-09-11 08:21:38 -07:00
|
|
|
|
2022-07-12 05:40:09 -07:00
|
|
|
private fun Array<String>.sortDescending(zcashNetwork: ZcashNetwork) =
|
|
|
|
apply { sortByDescending { checkpointHeightFromFilename(zcashNetwork, it).value } }
|
2021-10-21 13:05:02 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Load the given birthday file from the assets of the given context. When no height is
|
|
|
|
* specified, we default to the file with the greatest name.
|
|
|
|
*
|
|
|
|
* @param context the context from which to load assets.
|
2022-07-12 05:40:09 -07:00
|
|
|
* @param birthday the height file to look for among the file names.
|
2021-10-21 13:05:02 -07:00
|
|
|
*
|
|
|
|
* @return a WalletBirthday that reflects the contents of the file or an exception when
|
|
|
|
* parsing fails.
|
|
|
|
*/
|
2022-07-12 05:40:09 -07:00
|
|
|
private suspend fun loadCheckpointFromAssets(
|
2021-10-21 13:05:02 -07:00
|
|
|
context: Context,
|
|
|
|
network: ZcashNetwork,
|
2022-07-12 05:40:09 -07:00
|
|
|
birthday: BlockHeight?
|
|
|
|
): Checkpoint {
|
|
|
|
twig("loading checkpoint from assets: $birthday")
|
|
|
|
val directory = checkpointDirectory(network)
|
|
|
|
val treeFiles = getFilteredFileNames(context, network, directory, birthday)
|
2021-10-21 13:05:02 -07:00
|
|
|
|
|
|
|
twig("found ${treeFiles.size} sapling tree checkpoints: $treeFiles")
|
|
|
|
|
2022-07-12 05:40:09 -07:00
|
|
|
return getFirstValidWalletBirthday(context, network, directory, treeFiles)
|
2021-10-21 13:05:02 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
private suspend fun getFilteredFileNames(
|
|
|
|
context: Context,
|
2022-07-12 05:40:09 -07:00
|
|
|
network: ZcashNetwork,
|
2021-10-21 13:05:02 -07:00
|
|
|
directory: String,
|
2022-07-12 05:40:09 -07:00
|
|
|
birthday: BlockHeight? = null
|
2021-10-21 13:05:02 -07:00
|
|
|
): List<String> {
|
2022-07-12 05:40:09 -07:00
|
|
|
val unfilteredTreeFiles = listCheckpointDirectoryContents(context, directory)
|
2021-10-21 13:05:02 -07:00
|
|
|
if (unfilteredTreeFiles.isNullOrEmpty()) {
|
|
|
|
throw BirthdayException.MissingBirthdayFilesException(directory)
|
|
|
|
}
|
|
|
|
|
|
|
|
val filteredTreeFiles = unfilteredTreeFiles
|
2022-07-12 05:40:09 -07:00
|
|
|
.sortDescending(network)
|
2021-10-21 13:05:02 -07:00
|
|
|
.filter { filename ->
|
2022-07-12 05:40:09 -07:00
|
|
|
birthday?.let { checkpointHeightFromFilename(network, filename) <= it } ?: true
|
2020-09-11 00:33:25 -07:00
|
|
|
}
|
2021-09-11 08:21:38 -07:00
|
|
|
|
2021-10-21 13:05:02 -07:00
|
|
|
if (filteredTreeFiles.isEmpty()) {
|
|
|
|
throw BirthdayException.BirthdayFileNotFoundException(
|
|
|
|
directory,
|
2022-07-12 05:40:09 -07:00
|
|
|
birthday
|
2021-10-21 13:05:02 -07:00
|
|
|
)
|
2021-09-11 08:21:38 -07:00
|
|
|
}
|
|
|
|
|
2021-10-21 13:05:02 -07:00
|
|
|
return filteredTreeFiles
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param treeFiles A list of files, sorted in descending order based on `int` value of the first part of the filename.
|
|
|
|
*/
|
|
|
|
@VisibleForTesting
|
|
|
|
internal suspend fun getFirstValidWalletBirthday(
|
|
|
|
context: Context,
|
2022-07-12 05:40:09 -07:00
|
|
|
network: ZcashNetwork,
|
2021-10-21 13:05:02 -07:00
|
|
|
directory: String,
|
|
|
|
treeFiles: List<String>
|
2022-07-12 05:40:09 -07:00
|
|
|
): Checkpoint {
|
2021-10-21 13:05:02 -07:00
|
|
|
var lastException: Exception? = null
|
|
|
|
treeFiles.forEach { treefile ->
|
|
|
|
try {
|
2022-06-13 06:16:51 -07:00
|
|
|
val jsonString = withContext(Dispatchers.IO) {
|
2021-09-11 08:21:38 -07:00
|
|
|
context.assets.open("$directory/$treefile").use { inputStream ->
|
2022-06-13 06:16:51 -07:00
|
|
|
inputStream.reader().use { inputStreamReader ->
|
|
|
|
BufferedReader(inputStreamReader).use { bufferedReader ->
|
|
|
|
bufferedReader.readText()
|
2021-09-11 08:21:38 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-06-13 06:16:51 -07:00
|
|
|
|
2022-07-12 05:40:09 -07:00
|
|
|
return Checkpoint.from(network, jsonString)
|
2021-10-21 13:05:02 -07:00
|
|
|
} catch (t: Throwable) {
|
|
|
|
val exception = BirthdayException.MalformattedBirthdayFilesException(
|
|
|
|
directory,
|
2022-06-13 06:16:51 -07:00
|
|
|
treefile,
|
|
|
|
t
|
2021-10-21 13:05:02 -07:00
|
|
|
)
|
|
|
|
lastException = exception
|
2021-09-11 08:21:38 -07:00
|
|
|
|
2021-10-21 13:05:02 -07:00
|
|
|
if (IS_FALLBACK_ON_FAILURE) {
|
|
|
|
// TODO: If we ever add crash analytics hooks, this would be something to report
|
|
|
|
twig("Malformed birthday file $t")
|
|
|
|
} else {
|
|
|
|
throw exception
|
|
|
|
}
|
|
|
|
}
|
2020-09-11 00:33:25 -07:00
|
|
|
}
|
2021-10-21 13:05:02 -07:00
|
|
|
|
|
|
|
throw lastException!!
|
2020-09-11 00:33:25 -07:00
|
|
|
}
|
2021-03-10 10:10:03 -08:00
|
|
|
}
|