diff --git a/src/main/java/cash/z/ecc/android/sdk/exception/Exceptions.kt b/src/main/java/cash/z/ecc/android/sdk/exception/Exceptions.kt index bbbc4408..c944efac 100644 --- a/src/main/java/cash/z/ecc/android/sdk/exception/Exceptions.kt +++ b/src/main/java/cash/z/ecc/android/sdk/exception/Exceptions.kt @@ -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." ) diff --git a/src/main/java/cash/z/ecc/android/sdk/ext/Ext.kt b/src/main/java/cash/z/ecc/android/sdk/ext/Ext.kt index 4328df31..9b53e0a8 100644 --- a/src/main/java/cash/z/ecc/android/sdk/ext/Ext.kt +++ b/src/main/java/cash/z/ecc/android/sdk/ext/Ext.kt @@ -7,3 +7,13 @@ internal inline fun tryNull(block: () -> R): R? { null } } + +internal inline fun tryWarn(message: String, block: () -> R): R? { + return try { + block() + } catch (t: Throwable) { + twig("$message due to: $t") + return null + } +} + diff --git a/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt b/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt index 1160286a..125e5f15 100644 --- a/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt +++ b/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt @@ -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 - - @JvmStatic private external fun deriveExtendedFullViewingKeys(seed: ByteArray, numberOfAccounts: Int): Array - - @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 } } diff --git a/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt b/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt index 2bfc7f8e..3224b0f1 100644 --- a/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt +++ b/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt @@ -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 - - fun deriveTAddress(seed: ByteArray): String - - fun deriveViewingKey(spendingKey: String): String - - fun deriveViewingKeys(seed: ByteArray, numberOfAccounts: Int = 1): Array - fun decryptAndStoreTransaction(tx: ByteArray) fun initAccountsTable(seed: ByteArray, numberOfAccounts: Int): Array @@ -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 + + fun deriveTransparentAddress(seed: ByteArray): String + + fun deriveViewingKey(spendingKey: String): String + + fun deriveViewingKeys(seed: ByteArray, numberOfAccounts: Int = 1): Array + } } diff --git a/src/main/java/cash/z/ecc/android/sdk/tool/DerivationTool.kt b/src/main/java/cash/z/ecc/android/sdk/tool/DerivationTool.kt new file mode 100644 index 00000000..85533e17 --- /dev/null +++ b/src/main/java/cash/z/ecc/android/sdk/tool/DerivationTool.kt @@ -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 = + 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 = + 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 withRustBackendLoaded(block: () -> T): T { + RustBackend.load() + return block() + } + + + // + // JNI functions + // + + @JvmStatic + private external fun deriveExtendedSpendingKeys( + seed: ByteArray, + numberOfAccounts: Int + ): Array + + @JvmStatic + private external fun deriveExtendedFullViewingKeys( + seed: ByteArray, + numberOfAccounts: Int + ): Array + + @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 + } +} \ No newline at end of file diff --git a/src/main/java/cash/z/ecc/android/sdk/tool/WalletBirthdayTool.kt b/src/main/java/cash/z/ecc/android/sdk/tool/WalletBirthdayTool.kt new file mode 100644 index 00000000..b2dc7cd2 --- /dev/null +++ b/src/main/java/cash/z/ecc/android/sdk/tool/WalletBirthdayTool.kt @@ -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] + ) + } + } + } +} \ No newline at end of file diff --git a/src/main/rust/lib.rs b/src/main/rust/lib.rs index 08eae78d..8877139c 100644 --- a/src/main/rust/lib.rs +++ b/src/main/rust/lib.rs @@ -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,