Created birthday tool and derivation tool.

By extracting behavior from the Initializer into a more genralized class that can be used statically.
This commit is contained in:
Kevin Gorham 2020-09-11 03:33:25 -04:00
parent 11431f5e5a
commit a08beef93b
No known key found for this signature in database
GPG Key ID: CCA55602DF49FC38
7 changed files with 303 additions and 52 deletions

View File

@ -1,7 +1,5 @@
package cash.z.ecc.android.sdk.exception
import java.lang.RuntimeException
/**
* Marker for all custom exceptions from the SDK. Making it an interface would result in more typing
@ -82,6 +80,14 @@ sealed class BirthdayException(message: String, cause: Throwable? = null) : SdkE
"Failed to initialize wallet with alias=$alias because its birthday could not be found." +
" Verify the alias or perhaps a new wallet should be created, instead."
)
class ExactBirthdayNotFoundException(height: Int, nearestMatch: Int? = null): BirthdayException(
"Unable to find birthday that exactly matches $height.${
if (nearestMatch != null)
" An exact match was request but the nearest match found was $nearestMatch."
else ""
}"
)
class BirthdayFileNotFoundException(directory: String, height: Int?) : BirthdayException(
"Unable to find birthday file for $height verify that $directory/$height.json exists."
)

View File

@ -7,3 +7,13 @@ internal inline fun <R> tryNull(block: () -> R): R? {
null
}
}
internal inline fun <R> tryWarn(message: String, block: () -> R): R? {
return try {
block()
} catch (t: Throwable) {
twig("$message due to: $t")
return null
}
}

View File

@ -25,6 +25,7 @@ class RustBackend : RustBackendWelding {
internal var birthdayHeight: Int = -1
get() = if (field != -1) field else throw BirthdayException.UninitializedBirthdayException
private set
/**
* Loads the library and initializes path variables. Although it is best to only call this
@ -33,23 +34,27 @@ class RustBackend : RustBackendWelding {
fun init(
cacheDbPath: String,
dataDbPath: String,
paramsPath: String
paramsPath: String,
birthdayHeight: Int? = null
): RustBackend {
twig("Creating RustBackend") {
pathCacheDb = cacheDbPath
pathDataDb = dataDbPath
pathParamsDir = paramsPath
if (birthdayHeight != null) {
this.birthdayHeight = birthdayHeight
}
}
return this
}
fun clear(clearCacheDb: Boolean = true, clearDataDb: Boolean = true) {
if (clearCacheDb) {
twig("Deleting cache database!")
twig("Deleting the cache database!")
File(pathCacheDb).delete()
}
if (clearDataDb) {
twig("Deleting data database!")
twig("Deleting the data database!")
File(pathDataDb).delete()
}
}
@ -75,7 +80,6 @@ class RustBackend : RustBackendWelding {
time: Long,
saplingTree: String
): Boolean {
birthdayHeight = height
return initBlocksTable(pathDataDb, height, hash, time, saplingTree)
}
@ -123,22 +127,6 @@ class RustBackend : RustBackendWelding {
"${pathParamsDir}/$OUTPUT_PARAM_FILE_NAME"
)
override fun deriveSpendingKeys(seed: ByteArray, numberOfAccounts: Int) =
deriveExtendedSpendingKeys(seed, numberOfAccounts)
override fun deriveTAddress(seed: ByteArray): String = deriveTransparentAddress(seed)
override fun deriveViewingKeys(seed: ByteArray, numberOfAccounts: Int) =
deriveExtendedFullViewingKeys(seed, numberOfAccounts)
override fun deriveViewingKey(spendingKey: String) = deriveExtendedFullViewingKey(spendingKey)
override fun deriveAddress(seed: ByteArray, accountIndex: Int) =
deriveAddressFromSeed(seed, accountIndex)
override fun deriveAddress(viewingKey: String) = deriveAddressFromViewingKey(viewingKey)
override fun isValidShieldedAddr(addr: String) = isValidShieldedAddress(addr)
override fun isValidTransparentAddr(addr: String) = isValidTransparentAddress(addr)
@ -258,20 +246,8 @@ class RustBackend : RustBackendWelding {
@JvmStatic private external fun initLogs()
@JvmStatic private external fun deriveExtendedSpendingKeys(seed: ByteArray, numberOfAccounts: Int): Array<String>
@JvmStatic private external fun deriveExtendedFullViewingKeys(seed: ByteArray, numberOfAccounts: Int): Array<String>
@JvmStatic private external fun deriveExtendedFullViewingKey(spendingKey: String): String
@JvmStatic private external fun deriveAddressFromSeed(seed: ByteArray, accountIndex: Int): String
@JvmStatic private external fun deriveAddressFromViewingKey(key: String): String
@JvmStatic private external fun branchIdForHeight(height: Int): Long
@JvmStatic private external fun parseTransactionDataList(serializedList: ByteArray): ByteArray
@JvmStatic private external fun deriveTransparentAddress(seed: ByteArray): String
}
}

View File

@ -19,18 +19,6 @@ interface RustBackendWelding {
memo: ByteArray? = byteArrayOf()
): Long
fun deriveAddress(viewingKey: String): String
fun deriveAddress(seed: ByteArray, accountIndex: Int = 0): String
fun deriveSpendingKeys(seed: ByteArray, numberOfAccounts: Int = 1): Array<String>
fun deriveTAddress(seed: ByteArray): String
fun deriveViewingKey(spendingKey: String): String
fun deriveViewingKeys(seed: ByteArray, numberOfAccounts: Int = 1): Array<String>
fun decryptAndStoreTransaction(tx: ByteArray)
fun initAccountsTable(seed: ByteArray, numberOfAccounts: Int): Array<String>
@ -65,4 +53,18 @@ interface RustBackendWelding {
fun validateCombinedChain(): Int
// Implemented by `DerivationTool`
interface Derivation {
fun deriveShieldedAddress(viewingKey: String): String
fun deriveShieldedAddress(seed: ByteArray, accountIndex: Int = 0): String
fun deriveSpendingKeys(seed: ByteArray, numberOfAccounts: Int = 1): Array<String>
fun deriveTransparentAddress(seed: ByteArray): String
fun deriveViewingKey(spendingKey: String): String
fun deriveViewingKeys(seed: ByteArray, numberOfAccounts: Int = 1): Array<String>
}
}

View File

@ -0,0 +1,129 @@
package cash.z.ecc.android.sdk.tool
import cash.z.ecc.android.sdk.jni.RustBackend
import cash.z.ecc.android.sdk.jni.RustBackendWelding
class DerivationTool {
companion object : RustBackendWelding.Derivation {
/**
* Given a seed and a number of accounts, return the associated viewing keys.
*
* @param seed the seed from which to derive viewing keys.
* @param numberOfAccounts the number of accounts to use. Multiple accounts are not fully
* supported so the default value of 1 is recommended.
*
* @return the viewing keys that correspond to the seed, formatted as Strings.
*/
override fun deriveViewingKeys(seed: ByteArray, numberOfAccounts: Int): Array<String> =
withRustBackendLoaded {
deriveExtendedFullViewingKeys(seed, numberOfAccounts)
}
/**
* Given a spending key, return the associated viewing key.
*
* @param spendingKey the key from which to derive the viewing key.
*
* @return the viewing key that corresponds to the spending key.
*/
override fun deriveViewingKey(spendingKey: String): String = withRustBackendLoaded {
deriveExtendedFullViewingKey(spendingKey)
}
/**
* Given a seed and a number of accounts, return the associated spending keys.
*
* @param seed the seed from which to derive spending keys.
* @param numberOfAccounts the number of accounts to use. Multiple accounts are not fully
* supported so the default value of 1 is recommended.
*
* @return the spending keys that correspond to the seed, formatted as Strings.
*/
override fun deriveSpendingKeys(seed: ByteArray, numberOfAccounts: Int): Array<String> =
withRustBackendLoaded {
deriveExtendedSpendingKeys(seed, numberOfAccounts)
}
/**
* Given a seed and account index, return the associated address.
*
* @param seed the seed from which to derive the address.
* @param accountIndex the index of the account to use for deriving the address. Multiple
* accounts are not fully supported so the default value of 1 is recommended.
*
* @return the address that corresponds to the seed and account index.
*/
override fun deriveShieldedAddress(seed: ByteArray, accountIndex: Int): String =
withRustBackendLoaded {
deriveShieldedAddressFromSeed(seed, accountIndex)
}
/**
* Given a viewing key string, return the associated address.
*
* @param viewingKey the viewing key to use for deriving the address. The viewing key is tied to
* a specific account so no account index is required.
*
* @return the address that corresponds to the viewing key.
*/
override fun deriveShieldedAddress(viewingKey: String): String = withRustBackendLoaded {
deriveShieldedAddressFromViewingKey(viewingKey)
}
// WIP probably shouldn't be used just yet. Why?
// - because we need the private key associated with this seed and this function doesn't return it.
// - the underlying implementation needs to be split out into a few lower-level calls
override fun deriveTransparentAddress(seed: ByteArray): String = withRustBackendLoaded {
deriveTransparentAddressFromSeed(seed)
}
fun validateViewingKey(viewingKey: String) {
// TODO
}
/**
* A helper function to ensure that the Rust libraries are loaded before any code in this
* class attempts to interact with it, indirectly, by invoking JNI functions. It would be
* nice to have an annotation like @UsesSystemLibrary for this
*/
private fun <T> withRustBackendLoaded(block: () -> T): T {
RustBackend.load()
return block()
}
//
// JNI functions
//
@JvmStatic
private external fun deriveExtendedSpendingKeys(
seed: ByteArray,
numberOfAccounts: Int
): Array<String>
@JvmStatic
private external fun deriveExtendedFullViewingKeys(
seed: ByteArray,
numberOfAccounts: Int
): Array<String>
@JvmStatic
private external fun deriveExtendedFullViewingKey(spendingKey: String): String
@JvmStatic
private external fun deriveShieldedAddressFromSeed(
seed: ByteArray,
accountIndex: Int
): String
@JvmStatic
private external fun deriveShieldedAddressFromViewingKey(key: String): String
@JvmStatic
private external fun deriveTransparentAddressFromSeed(seed: ByteArray): String
}
}

View File

@ -0,0 +1,126 @@
package cash.z.ecc.android.sdk.tool
import android.content.Context
import cash.z.ecc.android.sdk.exception.BirthdayException
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.twig
import com.google.gson.Gson
import com.google.gson.stream.JsonReader
import java.io.InputStreamReader
import java.util.*
/**
* Tool for loading checkpoints for the wallet, based on the height at which the wallet was born.
*
* @param appContext needed for loading checkpoints from the app's assets directory.
*/
class WalletBirthdayTool(appContext: Context) {
val context = appContext.applicationContext
/**
* Load the nearest checkpoint to the given birthday height. If null is given, then this
* will load the most recent checkpoint available.
*/
fun loadNearest(birthdayHeight: Int? = null): WalletBirthday {
return loadBirthdayFromAssets(context, birthdayHeight)
}
/**
* Model object for holding a wallet birthday.
*
* @param height the height at the time the wallet was born.
* @param hash the hash of the block at the height.
* @param time the block time at the height. Represented as seconds since the Unix epoch.
* @param tree the sapling tree corresponding to the height.
*/
data class WalletBirthday(
val height: Int = -1,
val hash: String = "",
val time: Long = -1,
val tree: String = ""
)
companion object {
/**
* Directory within the assets folder where birthday data
* (i.e. sapling trees for a given height) can be found.
*/
private const val BIRTHDAY_DIRECTORY = "zcash/saplingtree"
/**
* Load the nearest checkpoint to the given birthday height. If null is given, then this
* will load the most recent checkpoint available.
*/
fun loadNearest(context: Context, birthdayHeight: Int? = null): WalletBirthday {
// TODO: potentially pull from shared preferences first
return loadBirthdayFromAssets(context, birthdayHeight)
}
/**
* 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.
*/
fun loadExact(context: Context, birthdayHeight: Int) =
loadNearest(context, birthdayHeight).also {
if (it.height != birthdayHeight)
throw BirthdayException.ExactBirthdayNotFoundException(
birthdayHeight,
it.height
)
}
/**
* 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.
* @param birthdayHeight the height file to look for among the file names.
*
* @return a WalletBirthday that reflects the contents of the file or an exception when
* parsing fails.
*/
private fun loadBirthdayFromAssets(
context: Context,
birthdayHeight: Int? = null
): WalletBirthday {
twig("loading birthday from assets: $birthdayHeight")
val treeFiles =
context.assets.list(BIRTHDAY_DIRECTORY)?.apply { sortByDescending { fileName ->
try {
fileName.split('.').first().toInt()
} catch (t: Throwable) {
ZcashSdk.SAPLING_ACTIVATION_HEIGHT
}
} }
if (treeFiles.isNullOrEmpty()) throw BirthdayException.MissingBirthdayFilesException(
BIRTHDAY_DIRECTORY
)
twig("found ${treeFiles.size} sapling tree checkpoints: ${Arrays.toString(treeFiles)}")
val file: String
try {
file = if (birthdayHeight == null) treeFiles.first() else {
treeFiles.first {
it.split(".").first().toInt() <= birthdayHeight
}
}
} catch (t: Throwable) {
throw BirthdayException.BirthdayFileNotFoundException(
BIRTHDAY_DIRECTORY,
birthdayHeight
)
}
try {
val reader = JsonReader(
InputStreamReader(context.assets.open("${BIRTHDAY_DIRECTORY}/$file"))
)
return Gson().fromJson(reader, WalletBirthday::class.java)
} catch (t: Throwable) {
throw BirthdayException.MalformattedBirthdayFilesException(
BIRTHDAY_DIRECTORY,
treeFiles[0]
)
}
}
}
}

View File

@ -171,7 +171,6 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initAccount
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveExtendedSpendingKeys(
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initAccountsTableWithKeys(
env: JNIEnv<'_>,
_: JClass<'_>,
@ -195,6 +194,9 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initAccount
});
unwrap_exc_or(&env, res, JNI_FALSE)
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveExtendedSpendingKeys(
env: JNIEnv<'_>,
_: JClass<'_>,
seed: jbyteArray,
@ -229,7 +231,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initAccount
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveExtendedFullViewingKeys(
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveExtendedFullViewingKeys(
env: JNIEnv<'_>,
_: JClass<'_>,
seed: jbyteArray,
@ -264,7 +266,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveExten
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveAddressFromSeed(
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveShieldedAddressFromSeed(
env: JNIEnv<'_>,
_: JClass<'_>,
seed: jbyteArray,
@ -292,7 +294,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveAddre
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveAddressFromViewingKey(
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveShieldedAddressFromViewingKey(
env: JNIEnv<'_>,
_: JClass<'_>,
extfvk_string: JString<'_>,
@ -326,7 +328,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveAddre
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveExtendedFullViewingKey(
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveExtendedFullViewingKey(
env: JNIEnv<'_>,
_: JClass<'_>,
extsk_string: JString<'_>,
@ -683,7 +685,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_parseTransa
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_deriveTransparentAddress(
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveTransparentAddressFromSeed(
env: JNIEnv<'_>,
_: JClass<'_>,
seed: jbyteArray,