diff --git a/CHANGELOG.md b/CHANGELOG.md index af815009..1e9f24a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Change Log - `FirstClassByteArray` - `UnifiedSpendingKey` - `cash.z.ecc.android.sdk.tool`: + - `DerivationTool.deriveUnifiedSpendingKey` + - `DerivationTool.deriveUnifiedFullViewingKey` - `DerivationTool.deriveTransparentAccountPrivateKey` - `DerivationTool.deriveTransparentAddressFromAccountPrivateKey` - `DerivationTool.deriveUnifiedAddress` @@ -36,8 +38,11 @@ Change Log - `Synchronizer.Companion.new` now takes a `seed` argument. A non-null value should be provided if `Synchronizer.Companion.new` throws an error that a database migration requires the wallet seed. - - `Synchronizer.shieldFunds` now takes a transparent account private key (representing - all transparent secret keys within an account) instead of a transparent secret key. + - `Synchronizer.sendToAddress` now takes a `UnifiedSpendingKey` instead of an encoded + Sapling extended spending key, and the `fromAccountIndex` argument is now implicit in + the `UnifiedSpendingKey`. + - `Synchronizer.shieldFunds` now takes a `UnifiedSpendingKey` instead of separately + encoded Sapling and transparent keys. ### Removed - `cash.z.ecc.android.sdk`: @@ -49,6 +54,10 @@ Change Log public key, and not the extended public key as intended. This made it incompatible with ZIP 316. - `cash.z.ecc.android.sdk.tool`: + - `DerivationTool.deriveSpendingKeys` (use + `DerivationTool.deriveUnifiedSpendingKey` instead). + - `DerivationTool.deriveViewingKey` (use + - `DerivationTool.deriveUnifiedFullViewingKey` instead). - `DerivationTool.deriveTransparentAddressFromPrivateKey` (use `DerivationTool.deriveTransparentAddressFromAccountPrivateKey` instead). - `DerivationTool.deriveTransparentSecretKey` (use diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt index 8746da1b..4f0fd9b7 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt @@ -43,14 +43,14 @@ class GetPrivateKeyFragment : BaseDemoFragment() { // demonstrate deriving spending keys for five accounts but only take the first one lifecycleScope.launchWhenStarted { @Suppress("MagicNumber") - val spendingKey = DerivationTool.deriveSpendingKeys( + val spendingKey = DerivationTool.deriveUnifiedSpendingKey( seed, ZcashNetwork.fromResources(requireApplicationContext()), 5 - ).first() + ) // derive the key that allows you to view but not spend transactions - val viewingKey = DerivationTool.deriveViewingKey( + val viewingKey = DerivationTool.deriveUnifiedFullViewingKey( spendingKey, ZcashNetwork.fromResources(requireApplicationContext()) ) diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt index b386c3ac..110f98e7 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt @@ -30,6 +30,7 @@ import cash.z.ecc.android.sdk.ext.toZecString import cash.z.ecc.android.sdk.internal.Twig import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.model.LightWalletEndpoint +import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.WalletBalance import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.model.defaultForNetwork @@ -53,7 +54,7 @@ class SendFragment : BaseDemoFragment() { // in a normal app, this would be stored securely with the trusted execution environment (TEE) // but since this is a demo, we'll derive it on the fly - private lateinit var spendingKey: String + private lateinit var spendingKey: UnifiedSpendingKey /** * Initialize the required values that would normally live outside the demo but are repeated @@ -76,7 +77,7 @@ class SendFragment : BaseDemoFragment() { birthday = null ) spendingKey = runBlocking { - DerivationTool.deriveSpendingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first() + DerivationTool.deriveUnifiedSpendingKey(seed, ZcashNetwork.fromResources(requireApplicationContext())) } } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt index 39287c92..74a4f760 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt @@ -661,17 +661,16 @@ class SdkSynchronizer internal constructor( processor.getTransparentAddress(accountId) override fun sendToAddress( - spendingKey: String, + usk: UnifiedSpendingKey, amount: Zatoshi, toAddress: String, memo: String, - fromAccountIndex: Int ): Flow = flow { twig("Initializing pending transaction") // Emit the placeholder transaction, then switch to monitoring the database - txManager.initSpend(amount, toAddress, memo, fromAccountIndex).let { placeHolderTx -> + txManager.initSpend(amount, toAddress, memo, usk.account).let { placeHolderTx -> emit(placeHolderTx) - txManager.encode(spendingKey, placeHolderTx).let { encodedTx -> + txManager.encode(usk, placeHolderTx).let { encodedTx -> // only submit if it wasn't cancelled. Otherwise cleanup, immediately for best UX. if (encodedTx.isCancelled()) { twig("[cleanup] this tx has been cancelled so we will cleanup instead of submitting") @@ -691,20 +690,20 @@ class SdkSynchronizer internal constructor( }.distinctUntilChanged() override fun shieldFunds( - spendingKey: String, - transparentAccountPrivateKey: String, + usk: UnifiedSpendingKey, memo: String ): Flow = flow { twig("Initializing shielding transaction") - val tAddr = - DerivationTool.deriveTransparentAddressFromAccountPrivateKey(transparentAccountPrivateKey, network) + // TODO(str4d): This only shields funds from the current UA's transparent receiver. Fix this once we start + // rolling UAs. + val tAddr = processor.getTransparentAddress(usk.account) val tBalance = processor.getUtxoCacheBalance(tAddr) - val zAddr = getCurrentAddress(0) + val zAddr = getCurrentAddress(usk.account) // Emit the placeholder transaction, then switch to monitoring the database - txManager.initSpend(tBalance.available, zAddr, memo, 0).let { placeHolderTx -> + txManager.initSpend(tBalance.available, zAddr, memo, usk.account).let { placeHolderTx -> emit(placeHolderTx) - txManager.encode(spendingKey, transparentAccountPrivateKey, placeHolderTx).let { encodedTx -> + txManager.encode("", usk, placeHolderTx).let { encodedTx -> // only submit if it wasn't cancelled. Otherwise cleanup, immediately for best UX. if (encodedTx.isCancelled()) { twig("[cleanup] this shielding tx has been cancelled so we will cleanup instead of submitting") diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt index dec91b58..a4dd8bd6 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt @@ -225,11 +225,10 @@ interface Synchronizer { /** * Sends zatoshi. * - * @param spendingKey the key associated with the notes that will be spent. + * @param usk the unified spending key associated with the notes that will be spent. * @param zatoshi the amount of zatoshi to send. * @param toAddress the recipient's address. * @param memo the optional memo to include as part of the transaction. - * @param fromAccountIndex the optional account id to use. By default, the first account is used. * * @return a flow of PendingTransaction objects representing changes to the state of the * transaction. Any time the state changes a new instance will be emitted by this flow. This is @@ -237,16 +236,14 @@ interface Synchronizer { * for any wallet that wants to ignore this return value. */ fun sendToAddress( - spendingKey: String, + usk: UnifiedSpendingKey, amount: Zatoshi, toAddress: String, memo: String = "", - fromAccountIndex: Int = 0 ): Flow fun shieldFunds( - spendingKey: String, - transparentAccountPrivateKey: String, + usk: UnifiedSpendingKey, memo: String = ZcashSdk.DEFAULT_SHIELD_FUNDS_MEMO_PREFIX ): Flow diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PersistentTransactionManager.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PersistentTransactionManager.kt index 3b993885..bfb825c9 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PersistentTransactionManager.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PersistentTransactionManager.kt @@ -13,6 +13,7 @@ import cash.z.ecc.android.sdk.internal.db.PendingTransactionDb import cash.z.ecc.android.sdk.internal.service.LightWalletService import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.model.BlockHeight +import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.Zatoshi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers.IO @@ -108,21 +109,23 @@ class PersistentTransactionManager( } override suspend fun encode( - spendingKey: String, + usk: UnifiedSpendingKey, pendingTx: PendingTransaction ): PendingTransaction = withContext(Dispatchers.IO) { twig("managing the creation of a transaction") var tx = pendingTx as PendingTransactionEntity + if (tx.accountIndex != usk.account) { + throw java.lang.IllegalArgumentException("usk is not for the same account as pendingTx") + } @Suppress("TooGenericExceptionCaught") try { twig("beginning to encode transaction with : $encoder") val encodedTx = encoder.createTransaction( - spendingKey, + usk, tx.valueZatoshi, tx.toAddress, tx.memo, - tx.accountIndex ) twig("successfully encoded transaction!") safeUpdate("updating transaction encoding", -1) { @@ -149,7 +152,7 @@ class PersistentTransactionManager( // spendingKey is removed. Figure out where these methods need to be renamed, and do so. override suspend fun encode( spendingKey: String, // TODO(str4d): Remove this argument. - transparentAccountPrivateKey: String, + usk: UnifiedSpendingKey, pendingTx: PendingTransaction ): PendingTransaction { twig("managing the creation of a shielding transaction") @@ -158,7 +161,7 @@ class PersistentTransactionManager( try { twig("beginning to encode shielding transaction with : $encoder") val encodedTx = encoder.createShieldingTransaction( - transparentAccountPrivateKey, + usk, tx.memo ) twig("successfully encoded shielding transaction!") diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionEncoder.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionEncoder.kt index d6574026..45a864e6 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionEncoder.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionEncoder.kt @@ -1,41 +1,37 @@ package cash.z.ecc.android.sdk.internal.transaction import cash.z.ecc.android.sdk.db.entity.EncodedTransaction +import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.Zatoshi interface TransactionEncoder { - // TODO(str4d): Migrate to binary USK format. /** * Creates a transaction, throwing an exception whenever things are missing. When the provided * wallet implementation doesn't throw an exception, we wrap the issue into a descriptive * exception ourselves (rather than using double-bangs for things). * - * @param spendingKey the key associated with the notes that will be spent. + * @param usk the unified spending key associated with the notes that will be spent. * @param amount the amount of zatoshi to send. * @param toAddress the recipient's address. * @param memo the optional memo to include as part of the transaction. - * @param fromAccountIndex the optional account id to use. By default, the 1st account is used. * * @return the successfully encoded transaction or an exception */ suspend fun createTransaction( - spendingKey: String, + usk: UnifiedSpendingKey, amount: Zatoshi, toAddress: String, memo: ByteArray? = byteArrayOf(), - fromAccountIndex: Int = 0 ): EncodedTransaction - // TODO(str4d): Migrate to binary USK format. - // TODO(str4d): Enable this to shield funds for other accounts. /** - * Creates a transaction that shields any transparent funds sent to account 0. + * Creates a transaction that shields any transparent funds sent to the given usk's account. * - * @param transparentAccountPrivateKey the transparent account private key for account 0. + * @param usk the unified spending key associated with the transparent funds that will be shielded. * @param memo the optional memo to include as part of the transaction. */ suspend fun createShieldingTransaction( - transparentAccountPrivateKey: String, + usk: UnifiedSpendingKey, memo: ByteArray? = byteArrayOf() ): EncodedTransaction diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionManager.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionManager.kt index c51ca51e..4c04276f 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionManager.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionManager.kt @@ -2,6 +2,7 @@ package cash.z.ecc.android.sdk.internal.transaction import cash.z.ecc.android.sdk.db.entity.PendingTransaction import cash.z.ecc.android.sdk.model.BlockHeight +import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.Zatoshi import kotlinx.coroutines.flow.Flow @@ -34,17 +35,17 @@ interface OutboundTransactionManager { * Encode the pending transaction using the given spending key. This is a local operation that * produces a raw transaction to submit to lightwalletd. * - * @param spendingKey the spendingKey to use for constructing the transaction. + * @param usk the unified spending key to use for constructing the transaction. * @param pendingTx the transaction information created by [initSpend] that will be used to * construct a transaction. * * @return the resulting pending transaction whose ID can be used to monitor for changes. */ - suspend fun encode(spendingKey: String, pendingTx: PendingTransaction): PendingTransaction + suspend fun encode(usk: UnifiedSpendingKey, pendingTx: PendingTransaction): PendingTransaction suspend fun encode( spendingKey: String, - transparentAccountPrivateKey: String, + usk: UnifiedSpendingKey, pendingTx: PendingTransaction ): PendingTransaction diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/WalletTransactionEncoder.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/WalletTransactionEncoder.kt index 4ef6cd5c..6b0a2557 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/WalletTransactionEncoder.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/WalletTransactionEncoder.kt @@ -8,6 +8,7 @@ import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twigTask import cash.z.ecc.android.sdk.jni.RustBackend import cash.z.ecc.android.sdk.jni.RustBackendWelding +import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.Zatoshi /** @@ -29,31 +30,29 @@ internal class WalletTransactionEncoder( * wallet implementation doesn't throw an exception, we wrap the issue into a descriptive * exception ourselves (rather than using double-bangs for things). * - * @param spendingKey the key associated with the notes that will be spent. + * @param usk the unified spending key associated with the notes that will be spent. * @param amount the amount of zatoshi to send. * @param toAddress the recipient's address. * @param memo the optional memo to include as part of the transaction. - * @param fromAccountIndex the optional account id to use. By default, the 1st account is used. * * @return the successfully encoded transaction or an exception */ override suspend fun createTransaction( - spendingKey: String, + usk: UnifiedSpendingKey, amount: Zatoshi, toAddress: String, memo: ByteArray?, - fromAccountIndex: Int ): EncodedTransaction { - val transactionId = createSpend(spendingKey, amount, toAddress, memo) + val transactionId = createSpend(usk, amount, toAddress, memo) return repository.findEncodedTransactionById(transactionId) ?: throw TransactionEncoderException.TransactionNotFoundException(transactionId) } override suspend fun createShieldingTransaction( - transparentAccountPrivateKey: String, + usk: UnifiedSpendingKey, memo: ByteArray? ): EncodedTransaction { - val transactionId = createShieldingSpend(transparentAccountPrivateKey, memo) + val transactionId = createShieldingSpend(usk, memo) return repository.findEncodedTransactionById(transactionId) ?: throw TransactionEncoderException.TransactionNotFoundException(transactionId) } @@ -103,21 +102,19 @@ internal class WalletTransactionEncoder( * Does the proofs and processing required to create a transaction to spend funds and inserts * the result in the database. On average, this call takes over 10 seconds. * - * @param spendingKey the key associated with the notes that will be spent. + * @param usk the unified spending key associated with the notes that will be spent. * @param amount the amount of zatoshi to send. * @param toAddress the recipient's address. * @param memo the optional memo to include as part of the transaction. - * @param fromAccountIndex the optional account id to use. By default, the 1st account is used. * * @return the row id in the transactions table that contains the spend transaction or -1 if it * failed. */ private suspend fun createSpend( - spendingKey: String, + usk: UnifiedSpendingKey, amount: Zatoshi, toAddress: String, memo: ByteArray? = byteArrayOf(), - fromAccountIndex: Int = 0 ): Long { return twigTask( "creating transaction to spend $amount zatoshi to" + @@ -128,8 +125,7 @@ internal class WalletTransactionEncoder( SaplingParamTool.ensureParams((rustBackend as RustBackend).pathParamsDir) twig("params exist! attempting to send...") rustBackend.createToAddress( - fromAccountIndex, - spendingKey, + usk, toAddress, amount.value, memo @@ -144,7 +140,7 @@ internal class WalletTransactionEncoder( } private suspend fun createShieldingSpend( - transparentAccountPrivateKey: String, + usk: UnifiedSpendingKey, memo: ByteArray? = byteArrayOf() ): Long { return twigTask("creating transaction to shield all UTXOs") { @@ -153,7 +149,7 @@ internal class WalletTransactionEncoder( SaplingParamTool.ensureParams((rustBackend as RustBackend).pathParamsDir) twig("params exist! attempting to shield...") rustBackend.shieldToAddress( - transparentAccountPrivateKey, + usk, memo ) } catch (t: Throwable) { diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt index d75ffdce..c71cd8b4 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt @@ -225,16 +225,15 @@ internal class RustBackend private constructor( } override suspend fun createToAddress( - account: Int, - extsk: String, + usk: UnifiedSpendingKey, to: String, value: Long, memo: ByteArray? ): Long = withContext(SdkDispatchers.DATABASE_IO) { createToAddress( dataDbFile.absolutePath, - account, - extsk, + usk.account, + usk.bytes.byteArray, to, value, memo ?: ByteArray(0), @@ -245,15 +244,15 @@ internal class RustBackend private constructor( } override suspend fun shieldToAddress( - xprv: String, + usk: UnifiedSpendingKey, memo: ByteArray? ): Long { twig("TMP: shieldToAddress with db path: $dataDbFile, ${memo?.size}") return withContext(SdkDispatchers.DATABASE_IO) { shieldToAddress( dataDbFile.absolutePath, - 0, - xprv, + usk.account, + usk.bytes.byteArray, memo ?: ByteArray(0), "$pathParamsDir/$SPEND_PARAM_FILE_NAME", "$pathParamsDir/$OUTPUT_PARAM_FILE_NAME", @@ -429,6 +428,12 @@ internal class RustBackend private constructor( @JvmStatic private external fun getSaplingReceiverForUnifiedAddress(ua: String): String? + internal fun validateUnifiedSpendingKey(bytes: ByteArray) = + isValidSpendingKey(bytes) + + @JvmStatic + private external fun isValidSpendingKey(bytes: ByteArray): Boolean + @JvmStatic private external fun isValidShieldedAddress(addr: String, networkId: Int): Boolean @@ -510,7 +515,7 @@ internal class RustBackend private constructor( private external fun createToAddress( dbDataPath: String, account: Int, - extsk: String, + usk: ByteArray, to: String, value: Long, memo: ByteArray, @@ -524,7 +529,7 @@ internal class RustBackend private constructor( private external fun shieldToAddress( dbDataPath: String, account: Int, - xprv: String, + usk: ByteArray, memo: ByteArray, spendParamsPath: String, outputParamsPath: String, diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt index ea35744f..45c78648 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt @@ -21,15 +21,14 @@ internal interface RustBackendWelding { @Suppress("LongParameterList") suspend fun createToAddress( - account: Int, - extsk: String, + usk: UnifiedSpendingKey, to: String, value: Long, memo: ByteArray? = byteArrayOf() ): Long suspend fun shieldToAddress( - xprv: String, + usk: UnifiedSpendingKey, memo: ByteArray? = byteArrayOf() ): Long @@ -110,11 +109,11 @@ internal interface RustBackendWelding { accountIndex: Int = 0 ): String - suspend fun deriveSpendingKeys( + suspend fun deriveUnifiedSpendingKey( seed: ByteArray, network: ZcashNetwork, - numberOfAccounts: Int = 1 - ): Array + account: Int = 0 + ): UnifiedSpendingKey suspend fun deriveTransparentAddress( seed: ByteArray, @@ -140,10 +139,10 @@ internal interface RustBackendWelding { account: Int = 0 ): String - suspend fun deriveViewingKey( - spendingKey: String, + suspend fun deriveUnifiedFullViewingKey( + usk: UnifiedSpendingKey, network: ZcashNetwork - ): String + ): UnifiedFullViewingKey suspend fun deriveUnifiedFullViewingKeys( seed: ByteArray, diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/UnifiedSpendingKey.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/UnifiedSpendingKey.kt index d2a37b43..7abd3b20 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/UnifiedSpendingKey.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/UnifiedSpendingKey.kt @@ -1,5 +1,7 @@ package cash.z.ecc.android.sdk.model +import cash.z.ecc.android.sdk.jni.RustBackend + /** * A [ZIP 316](https://zips.z.cash/zip-0316) Unified Spending Key. * @@ -29,4 +31,17 @@ data class UnifiedSpendingKey internal constructor( override fun toString() = "UnifiedSpendingKey(account=$account)" fun copyBytes() = bytes.byteArray.copyOf() + + companion object { + suspend fun new(account: Int, bytes: ByteArray): Result { + val bytesCopy = bytes.copyOf() + RustBackend.rustLibraryLoader.load() + return Result.runCatching { + // We can ignore the Boolean returned from this, because if an error + // occurs the Rust side will throw. + RustBackend.validateUnifiedSpendingKey(bytesCopy) + return success(UnifiedSpendingKey(account, FirstClassByteArray(bytesCopy))) + } + } + } } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/DerivationTool.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/DerivationTool.kt index 39f9d2f4..6b8ba2cd 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/DerivationTool.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/DerivationTool.kt @@ -2,6 +2,7 @@ package cash.z.ecc.android.sdk.tool import cash.z.ecc.android.sdk.jni.RustBackend import cash.z.ecc.android.sdk.jni.RustBackendWelding +import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey @@ -32,34 +33,39 @@ class DerivationTool { } /** - * Given a spending key, return the associated viewing key. + * Given a unified spending key, return the associated unified full viewing key. * - * @param spendingKey the key from which to derive the viewing key. + * @param usk the key from which to derive the viewing key. * - * @return the viewing key that corresponds to the spending key. + * @return a unified full viewing key. */ - override suspend fun deriveViewingKey( - spendingKey: String, + override suspend fun deriveUnifiedFullViewingKey( + usk: UnifiedSpendingKey, network: ZcashNetwork - ): String = withRustBackendLoaded { - deriveExtendedFullViewingKey(spendingKey, networkId = network.id) + ): UnifiedFullViewingKey = withRustBackendLoaded { + UnifiedFullViewingKey( + deriveUnifiedFullViewingKey(usk.bytes.byteArray, networkId = network.id) + ) } /** - * Given a seed and a number of accounts, return the associated spending keys. + * Derives and returns a unified spending key from the given seed for the given account ID. + * + * Returns the newly created [ZIP 316] account identifier, along with the binary encoding + * of the [`UnifiedSpendingKey`] for the newly created account. The caller should store + * the returned spending key in a secure fashion. * * @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. + * @param account the account to derive. * - * @return the spending keys that correspond to the seed, formatted as Strings. + * @return the unified spending key for the account. */ - override suspend fun deriveSpendingKeys( + override suspend fun deriveUnifiedSpendingKey( seed: ByteArray, network: ZcashNetwork, - numberOfAccounts: Int - ): Array = withRustBackendLoaded { - deriveExtendedSpendingKeys(seed, numberOfAccounts, networkId = network.id) + account: Int + ): UnifiedSpendingKey = withRustBackendLoaded { + deriveSpendingKey(seed, account, networkId = network.id) } /** @@ -146,11 +152,11 @@ class DerivationTool { // @JvmStatic - private external fun deriveExtendedSpendingKeys( + private external fun deriveSpendingKey( seed: ByteArray, - numberOfAccounts: Int, + account: Int, networkId: Int - ): Array + ): UnifiedSpendingKey @JvmStatic private external fun deriveUnifiedFullViewingKeysFromSeed( @@ -160,7 +166,7 @@ class DerivationTool { ): Array @JvmStatic - private external fun deriveExtendedFullViewingKey(spendingKey: String, networkId: Int): String + private external fun deriveUnifiedFullViewingKey(usk: ByteArray, networkId: Int): String @JvmStatic private external fun deriveUnifiedAddressFromSeed( diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/type/WalletTypes.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/type/WalletTypes.kt index 198dfed7..f7a205a4 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/type/WalletTypes.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/type/WalletTypes.kt @@ -1,5 +1,7 @@ package cash.z.ecc.android.sdk.type +import cash.z.ecc.android.sdk.model.FirstClassByteArray + /** * A [ZIP 316] Unified Full Viewing Key, corresponding to a single wallet account. * diff --git a/sdk-lib/src/main/rust/lib.rs b/sdk-lib/src/main/rust/lib.rs index c9c7c6a8..c7a77ff0 100644 --- a/sdk-lib/src/main/rust/lib.rs +++ b/sdk-lib/src/main/rust/lib.rs @@ -20,9 +20,9 @@ use jni::{ use log::Level; use schemer::MigratorError; use secp256k1::PublicKey; -use secrecy::SecretVec; +use secrecy::{ExposeSecret, SecretVec}; use zcash_address::{ToAddress, ZcashAddress}; -use zcash_client_backend::keys::UnifiedSpendingKey; +use zcash_client_backend::keys::{DecodingError, UnifiedSpendingKey}; use zcash_client_backend::{ address::{RecipientAddress, UnifiedAddress}, data_api::{ @@ -33,11 +33,8 @@ use zcash_client_backend::{ }, WalletRead, WalletReadTransparent, WalletWrite, WalletWriteTransparent, }, - encoding::{ - decode_extended_spending_key, encode_extended_full_viewing_key, - encode_extended_spending_key, AddressCodec, - }, - keys::{sapling, Era, UnifiedFullViewingKey}, + encoding::AddressCodec, + keys::{Era, UnifiedFullViewingKey}, wallet::{OvkPolicy, WalletTransparentOutput}, }; use zcash_client_sqlite::wallet::init::WalletMigrationError; @@ -60,7 +57,7 @@ use zcash_primitives::{ components::{Amount, OutPoint, TxOut}, Transaction, }, - zip32::{AccountId, DiversifierIndex, ExtendedFullViewingKey}, + zip32::{AccountId, DiversifierIndex}, }; use zcash_proofs::prover::LocalTxProver; @@ -147,6 +144,40 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initDataDb( unwrap_exc_or(&env, res, -1) } +fn encode_usk( + env: &JNIEnv<'_>, + account: AccountId, + usk: UnifiedSpendingKey, +) -> Result { + let encoded = SecretVec::new(usk.to_bytes(Era::Orchard)); + let output = env.new_object( + "cash/z/ecc/android/sdk/model/UnifiedSpendingKey", + "(I[B)V", + &[ + JValue::Int(u32::from(account) as i32), + JValue::Object(env.byte_array_from_slice(encoded.expose_secret())?.into()), + ], + )?; + Ok(output.into_inner()) +} + +fn decode_usk(env: &JNIEnv<'_>, usk: jbyteArray) -> Result { + let usk_bytes = SecretVec::new(env.convert_byte_array(usk).unwrap()); + + // The remainder of the function is safe. + UnifiedSpendingKey::from_bytes(Era::Orchard, usk_bytes.expose_secret()).map_err(|e| match e { + DecodingError::EraMismatch(era) => format_err!( + "Spending key was from era {:?}, but {:?} was expected.", + era, + Era::Orchard + ), + e => format_err!( + "An error occurred decoding the provided unified spending key: {:?}", + e + ), + }) +} + /// Adds the next available account-level spend authority, given the current set of /// [ZIP 316] account identifiers known, to the wallet database. /// @@ -181,16 +212,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_createAccou .create_account(&seed) .map_err(|e| format_err!("Error while initializing accounts: {}", e))?; - let encoded = usk.to_bytes(Era::Orchard); - let output = env.new_object( - "cash/z/ecc/android/sdk/model/UnifiedSpendingKey", - "(I[B)V", - &[ - JValue::Int(u32::from(account) as i32), - JValue::Object(env.byte_array_from_slice(&encoded)?.into()), - ], - )?; - Ok(output.into_inner()) + encode_usk(&env, account, usk) }); unwrap_exc_or(&env, res, ptr::null_mut()) } @@ -241,41 +263,32 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initAccount unwrap_exc_or(&env, res, JNI_FALSE) } +/// Derives and returns a unified spending key from the given seed for the given account ID. +/// +/// Returns the newly created [ZIP 316] account identifier, along with the binary encoding +/// of the [`UnifiedSpendingKey`] for the newly created account. The caller should store +/// the returned spending key in a secure fashion. #[no_mangle] -pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveExtendedSpendingKeys( +pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveSpendingKey( env: JNIEnv<'_>, _: JClass<'_>, seed: jbyteArray, - accounts: jint, + account: jint, network_id: jint, -) -> jobjectArray { +) -> jobject { let res = panic::catch_unwind(|| { let network = parse_network(network_id as u32)?; - let seed = env.convert_byte_array(seed).unwrap(); - let accounts = if accounts > 0 { - accounts as u32 + let seed = SecretVec::new(env.convert_byte_array(seed).unwrap()); + let account = if account >= 0 { + AccountId::from(account as u32) } else { return Err(format_err!("accounts argument must be greater than zero")); }; - let extsks: Vec<_> = (0..accounts) - .map(|account| { - sapling::spending_key(&seed, network.coin_type(), AccountId::from(account)) - }) - .collect(); + let usk = UnifiedSpendingKey::from_seed(&network, seed.expose_secret(), account) + .map_err(|e| format_err!("error generating unified spending key from seed: {:?}", e))?; - Ok(utils::rust_vec_to_java( - &env, - extsks, - "java/lang/String", - |env, extsk| { - env.new_string(encode_extended_spending_key( - network.hrp_sapling_extended_spending_key(), - &extsk, - )) - }, - |env| env.new_string(""), - )) + encode_usk(&env, account, usk) }); unwrap_exc_or(&env, res, ptr::null_mut()) } @@ -386,33 +399,20 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveU } #[no_mangle] -pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveExtendedFullViewingKey( +pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveUnifiedFullViewingKey( env: JNIEnv<'_>, _: JClass<'_>, - extsk_string: JString<'_>, + usk: jbyteArray, network_id: jint, -) -> jobjectArray { +) -> jstring { let res = panic::catch_unwind(|| { + let usk = decode_usk(&env, usk)?; let network = parse_network(network_id as u32)?; - let extsk_string = utils::java_string_to_rust(&env, extsk_string); - let extfvk = match decode_extended_spending_key( - network.hrp_sapling_extended_spending_key(), - &extsk_string, - ) { - Ok(extsk) => ExtendedFullViewingKey::from(&extsk), - Err(e) => { - return Err(format_err!( - "Error while deriving viewing key from spending key: {}", - e - )); - } - }; + + let ufvk = usk.to_unified_full_viewing_key(); let output = env - .new_string(encode_extended_full_viewing_key( - network.hrp_sapling_extended_full_viewing_key(), - &extfvk, - )) + .new_string(ufvk.encode(&network)) .expect("Couldn't create Java string!"); Ok(output.into_inner()) @@ -576,6 +576,19 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_getSaplingR unwrap_exc_or(&env, res, ptr::null_mut()) } +#[no_mangle] +pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_isValidSpendingKey( + env: JNIEnv<'_>, + _: JClass<'_>, + usk: jbyteArray, +) -> jboolean { + let res = panic::catch_unwind(|| { + let _usk = decode_usk(&env, usk)?; + Ok(JNI_TRUE) + }); + unwrap_exc_or(&env, res, JNI_FALSE) +} + #[no_mangle] pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_isValidShieldedAddress( env: JNIEnv<'_>, @@ -1222,7 +1235,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_createToAdd _: JClass<'_>, db_data: JString<'_>, account: jint, - extsk: JString<'_>, + usk: jbyteArray, to: JString<'_>, value: jlong, memo: jbyteArray, @@ -1239,7 +1252,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_createToAdd } else { return Err(format_err!("account argument must be nonnegative")); }; - let extsk = utils::java_string_to_rust(&env, extsk); + let usk = decode_usk(&env, usk)?; let to = utils::java_string_to_rust(&env, to); let value = Amount::from_i64(value).map_err(|()| format_err!("Invalid amount, out of range"))?; @@ -1250,15 +1263,6 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_createToAdd let spend_params = utils::java_string_to_rust(&env, spend_params); let output_params = utils::java_string_to_rust(&env, output_params); - let extsk = - match decode_extended_spending_key(network.hrp_sapling_extended_spending_key(), &extsk) - { - Ok(extsk) => extsk, - Err(e) => { - return Err(format_err!("Invalid ExtendedSpendingKey: {}", e)); - } - }; - let to = match RecipientAddress::decode(&network, &to) { Some(to) => to, None => { @@ -1284,7 +1288,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_createToAdd &network, prover, AccountId::from(account), - &extsk, + usk.sapling(), &to, value, memo, @@ -1302,7 +1306,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_shieldToAdd _: JClass<'_>, db_data: JString<'_>, account: jint, - xprv: JString<'_>, + usk: jbyteArray, memo: jbyteArray, spend_params: JString<'_>, output_params: JString<'_>, @@ -1312,25 +1316,16 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_shieldToAdd let network = parse_network(network_id as u32)?; let db_data = wallet_db(&env, network, db_data)?; let mut db_data = db_data.get_update_ops()?; - let account = if account == 0 { + let account = if account >= 0 { account as u32 } else { - return Err(format_err!( - "account argument {} must be nonnegative", - account - )); + return Err(format_err!("account argument must be nonnegative")); }; - let xprv_str = utils::java_string_to_rust(&env, xprv); + let usk = decode_usk(&env, usk)?; let memo_bytes = env.convert_byte_array(memo).unwrap(); let spend_params = utils::java_string_to_rust(&env, spend_params); let output_params = utils::java_string_to_rust(&env, output_params); - let xprv = match hdwallet_bitcoin::PrivKey::deserialize(xprv_str) { - Ok(xprv) => xprv, - Err(e) => return Err(format_err!("Invalid transparent extended privkey: {:?}", e)), - }; - let sk = AccountPrivKey::from_extended_privkey(xprv.extended_key); - let memo = Memo::from_bytes(&memo_bytes).unwrap(); let prover = LocalTxProver::new(Path::new(&spend_params), Path::new(&output_params)); @@ -1339,7 +1334,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_shieldToAdd &mut db_data, &network, prover, - &sk, + usk.transparent(), AccountId::from(account), &MemoBytes::from(&memo), 0,