From 1d2c7b1a4fb63bb13849f42eab41f7a5434bead5 Mon Sep 17 00:00:00 2001 From: Kevin Gorham Date: Wed, 17 Feb 2021 16:07:57 -0500 Subject: [PATCH] New: Non-Rust changes to support auto-shielding. --- .../cash/z/ecc/android/sdk/SdkSynchronizer.kt | 50 +++++++- .../cash/z/ecc/android/sdk/Synchronizer.kt | 43 ++++++- .../sdk/block/CompactBlockProcessor.kt | 37 +++++- .../z/ecc/android/sdk/db/entity/UtxoEntity.kt | 59 ++++++++++ .../z/ecc/android/sdk/ext/ZcashSdkCommon.kt | 4 + .../cash/z/ecc/android/sdk/jni/RustBackend.kt | 110 ++++++++++++++---- .../ecc/android/sdk/jni/RustBackendWelding.kt | 31 ++++- .../sdk/service/LightWalletGrpcService.kt | 12 ++ .../android/sdk/service/LightWalletService.kt | 10 ++ .../z/ecc/android/sdk/tool/DerivationTool.kt | 21 +++- .../ecc/android/sdk/tool/SaplingParamTool.kt | 6 + .../PersistentTransactionManager.kt | 34 ++++++ .../sdk/transaction/TransactionEncoder.kt | 6 + .../sdk/transaction/TransactionManager.kt | 6 + .../transaction/WalletTransactionEncoder.kt | 36 ++++++ src/main/proto/service.proto | 11 +- 16 files changed, 436 insertions(+), 40 deletions(-) create mode 100644 src/main/java/cash/z/ecc/android/sdk/db/entity/UtxoEntity.kt diff --git a/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt b/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt index 46581d4f..55732c32 100644 --- a/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt +++ b/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt @@ -40,6 +40,7 @@ import cash.z.ecc.android.sdk.ext.twigTask import cash.z.ecc.android.sdk.jni.RustBackend import cash.z.ecc.android.sdk.service.LightWalletGrpcService import cash.z.ecc.android.sdk.service.LightWalletService +import cash.z.ecc.android.sdk.tool.DerivationTool import cash.z.ecc.android.sdk.transaction.OutboundTransactionManager import cash.z.ecc.android.sdk.transaction.PagedTransactionRepository import cash.z.ecc.android.sdk.transaction.PersistentTransactionManager @@ -500,7 +501,13 @@ class SdkSynchronizer internal constructor( override suspend fun cancelSpend(pendingId: Long) = txManager.cancel(pendingId) - override suspend fun getAddress(accountId: Int): String = processor.getAddress(accountId) + override suspend fun getAddress(accountId: Int): String = getShieldedAddress(accountId) + + override suspend fun getShieldedAddress(accountId: Int): String = processor.getShieldedAddress(accountId) + + override suspend fun getTransparentAddress(seed: ByteArray, accountId: Int, index: Int): String { + return DerivationTool.deriveTransparentAddress(seed, accountId, index) + } override fun sendToAddress( spendingKey: String, @@ -529,6 +536,45 @@ class SdkSynchronizer internal constructor( txManager.monitorById(it.id) }.distinctUntilChanged() + override fun shieldFunds( + spendingKey: String, + transparentSecretKey: String, + memo: String + ): Flow = flow { + twig("Initializing shielding transaction") + val tAddr = DerivationTool.deriveTransparentAddress(transparentSecretKey) + val tBalance = processor.getUtxoCacheBalance(tAddr) + val zAddr = getAddress(0) + + + // Emit the placeholder transaction, then switch to monitoring the database + txManager.initSpend(tBalance.availableZatoshi, zAddr, memo, 0).let { placeHolderTx -> + emit(placeHolderTx) + txManager.encode(spendingKey, transparentSecretKey, 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") + if (cleanupCancelledTx(encodedTx)) refreshBalance() + encodedTx + } else { + txManager.submit(encodedTx) + } + } + } + }.flatMapLatest { + twig("Monitoring shielding transaction (id: ${it.id}) for updates...") + txManager.monitorById(it.id) + }.distinctUntilChanged() + + override suspend fun refreshUtxos(address: String, sinceHeight: Int): Int { + // TODO: we need to think about how we restrict this to only our taddr + return processor.downloadUtxos(address, sinceHeight) + } + + override suspend fun getTransparentBalance(tAddr: String): WalletBalance { + return processor.getUtxoCacheBalance(tAddr) + } + override suspend fun isValidShieldedAddr(address: String) = txManager.isValidShieldedAddress(address) @@ -649,4 +695,4 @@ fun Synchronizer( txManager, processor ) -} +} \ No newline at end of file diff --git a/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt b/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt index 9f0b0789..b0d2b32e 100644 --- a/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt +++ b/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt @@ -120,14 +120,38 @@ interface Synchronizer { // /** - * Gets the address for the given account. + * Gets the shielded address for the given account. This is syntactic sugar for + * [getShieldedAddress] because we use z-addrs by default. * * @param accountId the optional accountId whose address is of interest. By default, the first * account is used. * - * @return the address for the given account. + * @return the shielded address for the given account. */ - suspend fun getAddress(accountId: Int = 0): String + suspend fun getAddress(accountId: Int = 0) = getShieldedAddress(accountId) + + /** + * Gets the shielded address for the given account. + * + * @param accountId the optional accountId whose address is of interest. By default, the first + * account is used. + * + * @return the shielded address for the given account. + */ + suspend fun getShieldedAddress(accountId: Int = 0): String + + + /** + * Gets the transparent address for the given account and index. + * + * @param accountId the optional accountId whose address is of interest. By default, the first + * account is used. + * @param index the optional index whose address is of interest. By default, the first index is + * used. + * + * @return the address for the given account and index. + */ + suspend fun getTransparentAddress(seed: ByteArray, accountId: Int = 0, index: Int = 0): String /** * Sends zatoshi. @@ -151,6 +175,12 @@ interface Synchronizer { fromAccountIndex: Int = 0 ): Flow + fun shieldFunds( + spendingKey: String, + transparentSecretKey: String, + memo: String = ZcashSdk.DEFAULT_SHIELD_FUNDS_MEMO_PREFIX + ): Flow + /** * Returns true when the given address is a valid z-addr. Invalid addresses will throw an * exception. Valid z-addresses have these characteristics: //TODO copy info from related ZIP @@ -230,6 +260,13 @@ interface Synchronizer { errorHandler: (Throwable) -> Unit = { throw it } ) + suspend fun refreshUtxos(tAddr: String, sinceHeight: Int): Int + + /** + * Returns the balance that the wallet knows about. This should be called after [refreshUtxos]. + */ + suspend fun getTransparentBalance(tAddr: String): WalletBalance + // // Error Handling // diff --git a/src/main/java/cash/z/ecc/android/sdk/block/CompactBlockProcessor.kt b/src/main/java/cash/z/ecc/android/sdk/block/CompactBlockProcessor.kt index 420f3bbb..fee1565f 100644 --- a/src/main/java/cash/z/ecc/android/sdk/block/CompactBlockProcessor.kt +++ b/src/main/java/cash/z/ecc/android/sdk/block/CompactBlockProcessor.kt @@ -34,6 +34,7 @@ import cash.z.ecc.android.sdk.jni.RustBackend import cash.z.ecc.android.sdk.jni.RustBackendWelding import cash.z.ecc.android.sdk.transaction.PagedTransactionRepository import cash.z.ecc.android.sdk.transaction.TransactionRepository +import cash.z.wallet.sdk.rpc.Service import io.grpc.StatusRuntimeException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers.IO @@ -313,6 +314,33 @@ class CompactBlockProcessor( if (!repository.isInitialized()) throw CompactBlockProcessorException.Uninitialized } + + internal suspend fun downloadUtxos(tAddress: String, startHeight: Int): Int = withContext(IO) { + var skipped = 0 + twig("Downloading utxos starting at height $startHeight") + downloader.lightWalletService.fetchUtxos(tAddress, startHeight).let { result -> + result.forEach { utxo: Service.GetAddressUtxosReply -> + twig("Found UTXO at height ${utxo.height.toInt()}") + try { + rustBackend.putUtxo( + tAddress, + utxo.txid.toByteArray(), + utxo.index, + utxo.script.toByteArray(), + utxo.valueZat, + utxo.height.toInt() + ) + } catch (t: Throwable) { + // TODO: more accurately track the utxos that were skipped (in theory, this could fail for other reasons) + skipped++ + twig("Warning: Ignoring transaction at height ${utxo.height} @ index ${utxo.index} because it already exists") + } + } + // return the number of UTXOs that were downloaded + result.size - skipped + } + } + /** * Request all blocks in the given range and persist them locally for processing, later. * @@ -546,8 +574,8 @@ class CompactBlockProcessor( * * @return the address of this wallet. */ - suspend fun getAddress(accountId: Int) = withContext(IO) { - rustBackend.getAddress(accountId) + suspend fun getShieldedAddress(accountId: Int) = withContext(IO) { + rustBackend.getShieldedAddress(accountId) } /** @@ -572,6 +600,11 @@ class CompactBlockProcessor( } } + suspend fun getUtxoCacheBalance(address: String): WalletBalance = withContext(IO) { + rustBackend.getDownloadedUtxoBalance(address) + } + + /** * Transmits the given state for this processor. */ diff --git a/src/main/java/cash/z/ecc/android/sdk/db/entity/UtxoEntity.kt b/src/main/java/cash/z/ecc/android/sdk/db/entity/UtxoEntity.kt new file mode 100644 index 00000000..f11d5569 --- /dev/null +++ b/src/main/java/cash/z/ecc/android/sdk/db/entity/UtxoEntity.kt @@ -0,0 +1,59 @@ +package cash.z.ecc.android.sdk.db.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "utxos") +data class UtxoEntity( + val address: String ="", + + @ColumnInfo(typeAffinity = ColumnInfo.BLOB) + val txid: ByteArray? = byteArrayOf(), + + @ColumnInfo(name = "tx_index") + val transactionIndex: Int? = -1, + + @ColumnInfo(typeAffinity = ColumnInfo.BLOB) + val script: ByteArray? = byteArrayOf(), + + val value: Long = 0L, + + val height: Int? = -1, +) { + @PrimaryKey(autoGenerate = true) + var id: Long = 0L + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is UtxoEntity) return false + + if (id != other.id) return false + if (address != other.address) return false + if (txid != null) { + if (other.txid == null) return false + if (!txid.contentEquals(other.txid)) return false + } else if (other.txid != null) return false + if (transactionIndex != other.transactionIndex) return false + if (script != null) { + if (other.script == null) return false + if (!script.contentEquals(other.script)) return false + } else if (other.script != null) return false + if (value != other.value) return false + if (height != other.height) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + address.hashCode() + result = 31 * result + (txid?.contentHashCode() ?: 0) + result = 31 * result + (transactionIndex ?: 0) + result = 31 * result + (script?.contentHashCode() ?: 0) + result = 31 * result + value.hashCode() + result = 31 * result + (height ?: 0) + return result + } + +} diff --git a/src/main/java/cash/z/ecc/android/sdk/ext/ZcashSdkCommon.kt b/src/main/java/cash/z/ecc/android/sdk/ext/ZcashSdkCommon.kt index 005805bb..8f7da180 100644 --- a/src/main/java/cash/z/ecc/android/sdk/ext/ZcashSdkCommon.kt +++ b/src/main/java/cash/z/ecc/android/sdk/ext/ZcashSdkCommon.kt @@ -110,4 +110,8 @@ open class ZcashSdkCommon { * this will do for now, since we're using a cloudfront URL that already redirects. */ val CLOUD_PARAM_DIR_URL = "https://z.cash/downloads/" + /** + * The default memo to use when shielding transparent funds. + */ + open val DEFAULT_SHIELD_FUNDS_MEMO_PREFIX = "shielding:" } 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 c16d3dda..84a67298 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 @@ -1,10 +1,10 @@ package cash.z.ecc.android.sdk.jni +import cash.z.ecc.android.sdk.block.CompactBlockProcessor import cash.z.ecc.android.sdk.exception.BirthdayException import cash.z.ecc.android.sdk.ext.ZcashSdk.OUTPUT_PARAM_FILE_NAME import cash.z.ecc.android.sdk.ext.ZcashSdk.SPEND_PARAM_FILE_NAME import cash.z.ecc.android.sdk.ext.twig -import cash.z.ecc.android.sdk.rpc.LocalRpcTypes import java.io.File /** @@ -64,7 +64,11 @@ class RustBackend private constructor() : RustBackendWelding { return initBlocksTable(pathDataDb, height, hash, time, saplingTree) } - override fun getAddress(account: Int) = getAddress(pathDataDb, account) + override fun getShieldedAddress(account: Int) = getShieldedAddress(pathDataDb, account) + + override fun getTransparentAddress(account: Int, index: Int): String { + throw NotImplementedError("TODO: implement this at the zcash_client_sqlite level. But for now, use DerivationTool, instead to derive addresses from seeds") + } override fun getBalance(account: Int) = getBalance(pathDataDb, account) @@ -108,30 +112,62 @@ class RustBackend private constructor() : RustBackendWelding { "$pathParamsDir/$OUTPUT_PARAM_FILE_NAME" ) + override fun shieldToAddress( + extsk: String, + tsk: String, + memo: ByteArray? + ): Long { + twig("TMP: shieldToAddress with db path: $pathDataDb, ${memo?.size}") + return shieldToAddress( + pathDataDb, + 0, + extsk, + tsk, + memo ?: ByteArray(0), + "${pathParamsDir}/$SPEND_PARAM_FILE_NAME", + "${pathParamsDir}/$OUTPUT_PARAM_FILE_NAME" + ) + } + + override fun putUtxo( + tAddress: String, + txId: ByteArray, + index: Int, + script: ByteArray, + value: Long, + height: Int + ): Boolean = putUtxo(pathDataDb, tAddress, txId, index, script, value, height) + + override fun getDownloadedUtxoBalance(address: String): CompactBlockProcessor.WalletBalance { + val verified = getVerifiedTransparentBalance(pathDataDb, address) + val total = getTotalTransparentBalance(pathDataDb, address) + return CompactBlockProcessor.WalletBalance(total, verified) + } + override fun isValidShieldedAddr(addr: String) = isValidShieldedAddress(addr) override fun isValidTransparentAddr(addr: String) = isValidTransparentAddress(addr) override fun getBranchIdForHeight(height: Int): Long = branchIdForHeight(height) - /** - * This is a proof-of-concept for doing Local RPC, where we are effectively using the JNI - * boundary as a grpc server. It is slightly inefficient in terms of both space and time but - * given that it is all done locally, on the heap, it seems to be a worthwhile tradeoff because - * it reduces the complexity and expands the capacity for the two layers to communicate. - * - * We're able to keep the "unsafe" byteArray functions private and wrap them in typeSafe - * equivalents and, eventually, surface any parse errors (for now, errors are only logged). - */ - override fun parseTransactionDataList(tdl: LocalRpcTypes.TransactionDataList): LocalRpcTypes.TransparentTransactionList { - return try { - // serialize the list, send it over to rust and get back a serialized set of results that we parse out and return - return LocalRpcTypes.TransparentTransactionList.parseFrom(parseTransactionDataList(tdl.toByteArray())) - } catch (t: Throwable) { - twig("ERROR: failed to parse transaction data list due to: $t caused by: ${t.cause}") - LocalRpcTypes.TransparentTransactionList.newBuilder().build() - } - } +// /** +// * This is a proof-of-concept for doing Local RPC, where we are effectively using the JNI +// * boundary as a grpc server. It is slightly inefficient in terms of both space and time but +// * given that it is all done locally, on the heap, it seems to be a worthwhile tradeoff because +// * it reduces the complexity and expands the capacity for the two layers to communicate. +// * +// * We're able to keep the "unsafe" byteArray functions private and wrap them in typeSafe +// * equivalents and, eventually, surface any parse errors (for now, errors are only logged). +// */ +// override fun parseTransactionDataList(tdl: LocalRpcTypes.TransactionDataList): LocalRpcTypes.TransparentTransactionList { +// return try { +// // serialize the list, send it over to rust and get back a serialized set of results that we parse out and return +// return LocalRpcTypes.TransparentTransactionList.parseFrom(parseTransactionDataList(tdl.toByteArray())) +// } catch (t: Throwable) { +// twig("ERROR: failed to parse transaction data list due to: $t caused by: ${t.cause}") +// LocalRpcTypes.TransparentTransactionList.newBuilder().build() +// } +// } /** * Exposes all of the librustzcash functions along with helpers for loading the static library. @@ -208,7 +244,9 @@ class RustBackend private constructor() : RustBackendWelding { saplingTree: String ): Boolean - @JvmStatic private external fun getAddress(dbDataPath: String, account: Int): String + @JvmStatic private external fun getShieldedAddress(dbDataPath: String, account: Int): String +// TODO: implement this in the zcash_client_sqlite layer. For now, use DerivationTool, instead. +// @JvmStatic private external fun getTransparentAddress(dbDataPath: String, account: Int): String @JvmStatic private external fun isValidShieldedAddress(addr: String): Boolean @@ -244,10 +282,38 @@ class RustBackend private constructor() : RustBackendWelding { outputParamsPath: String ): Long + @JvmStatic private external fun shieldToAddress( + dbDataPath: String, + account: Int, + extsk: String, + tsk: String, + memo: ByteArray, + spendParamsPath: String, + outputParamsPath: String + ): Long + @JvmStatic private external fun initLogs() @JvmStatic private external fun branchIdForHeight(height: Int): Long - @JvmStatic private external fun parseTransactionDataList(serializedList: ByteArray): ByteArray + @JvmStatic private external fun putUtxo( + dbDataPath: String, + tAddress: String, + txId: ByteArray, + index: Int, + script: ByteArray, + value: Long, + height: Int + ): Boolean + + @JvmStatic private external fun getVerifiedTransparentBalance( + pathDataDb: String, + taddr: String + ): Long + + @JvmStatic private external fun getTotalTransparentBalance( + pathDataDb: String, + taddr: String + ): Long } } 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 3224b0f1..57f006a2 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 @@ -1,6 +1,6 @@ package cash.z.ecc.android.sdk.jni -import cash.z.ecc.android.sdk.rpc.LocalRpcTypes +import cash.z.ecc.android.sdk.block.CompactBlockProcessor /** * Contract defining the exposed capabilities of the Rust backend. @@ -19,6 +19,12 @@ interface RustBackendWelding { memo: ByteArray? = byteArrayOf() ): Long + fun shieldToAddress( + extsk: String, + tsk: String, + memo: ByteArray? = byteArrayOf() + ): Long + fun decryptAndStoreTransaction(tx: ByteArray) fun initAccountsTable(seed: ByteArray, numberOfAccounts: Int): Array @@ -33,7 +39,9 @@ interface RustBackendWelding { fun isValidTransparentAddr(addr: String): Boolean - fun getAddress(account: Int = 0): String + fun getShieldedAddress(account: Int = 0): String + + fun getTransparentAddress(account: Int = 0, index: Int = 0): String fun getBalance(account: Int = 0): Long @@ -45,7 +53,7 @@ interface RustBackendWelding { fun getVerifiedBalance(account: Int = 0): Long - fun parseTransactionDataList(tdl: LocalRpcTypes.TransactionDataList): LocalRpcTypes.TransparentTransactionList +// fun parseTransactionDataList(tdl: LocalRpcTypes.TransactionDataList): LocalRpcTypes.TransparentTransactionList fun rewindToHeight(height: Int): Boolean @@ -53,6 +61,17 @@ interface RustBackendWelding { fun validateCombinedChain(): Int + fun putUtxo( + tAddress: String, + txId: ByteArray, + index: Int, + script: ByteArray, + value: Long, + height: Int + ): Boolean + + fun getDownloadedUtxoBalance(address: String): CompactBlockProcessor.WalletBalance + // Implemented by `DerivationTool` interface Derivation { fun deriveShieldedAddress(viewingKey: String): String @@ -61,7 +80,11 @@ interface RustBackendWelding { fun deriveSpendingKeys(seed: ByteArray, numberOfAccounts: Int = 1): Array - fun deriveTransparentAddress(seed: ByteArray): String + fun deriveTransparentAddress(seed: ByteArray, account: Int = 0, index: Int = 0): String + + fun deriveTransparentAddress(transparentSecretKey: String): String + + fun deriveTransparentSecretKey(seed: ByteArray, account: Int = 0, index: Int = 0): String fun deriveViewingKey(spendingKey: String): String diff --git a/src/main/java/cash/z/ecc/android/sdk/service/LightWalletGrpcService.kt b/src/main/java/cash/z/ecc/android/sdk/service/LightWalletGrpcService.kt index 1a2c2e32..61ec1d69 100644 --- a/src/main/java/cash/z/ecc/android/sdk/service/LightWalletGrpcService.kt +++ b/src/main/java/cash/z/ecc/android/sdk/service/LightWalletGrpcService.kt @@ -106,6 +106,18 @@ class LightWalletGrpcService private constructor( ) } + override fun fetchUtxos( + tAddress: String, + startHeight: Int + ): List { + channel.resetConnectBackoff() + val result = channel.createStub().getAddressUtxos( + Service.GetAddressUtxosArg.newBuilder().setAddress(tAddress) + .setStartHeight(startHeight.toLong()).build() + ) + return result.addressUtxosList + } + override fun getTAddressTransactions( tAddress: String, blockHeightRange: IntRange diff --git a/src/main/java/cash/z/ecc/android/sdk/service/LightWalletService.kt b/src/main/java/cash/z/ecc/android/sdk/service/LightWalletService.kt index 049b5220..b78e3a60 100644 --- a/src/main/java/cash/z/ecc/android/sdk/service/LightWalletService.kt +++ b/src/main/java/cash/z/ecc/android/sdk/service/LightWalletService.kt @@ -16,6 +16,16 @@ interface LightWalletService { */ fun fetchTransaction(txId: ByteArray): Service.RawTransaction? + /** + * Fetch all UTXOs for the given address, going back to the start height. + * + * @param tAddress the transparent address to use. + * @param startHeight the starting height to use. + * + * @return the UTXOs for the given address from the startHeight. + */ + fun fetchUtxos(tAddress: String, startHeight: Int): List + /** * Return the given range of blocks. * 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 index cb80c05c..240c6162 100644 --- a/src/main/java/cash/z/ecc/android/sdk/tool/DerivationTool.kt +++ b/src/main/java/cash/z/ecc/android/sdk/tool/DerivationTool.kt @@ -75,8 +75,16 @@ class DerivationTool { // 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) + override fun deriveTransparentAddress(seed: ByteArray, account: Int, index: Int): String = withRustBackendLoaded { + deriveTransparentAddressFromSeed(seed, account, index) + } + + override fun deriveTransparentAddress(transparentSecretKey: String): String = withRustBackendLoaded { + deriveTransparentAddressFromSecretKey(transparentSecretKey) + } + + override fun deriveTransparentSecretKey(seed: ByteArray, account: Int, index: Int): String = withRustBackendLoaded { + deriveTransparentSecretKeyFromSeed(seed, account, index) } fun validateViewingKey(viewingKey: String) { @@ -122,6 +130,13 @@ class DerivationTool { private external fun deriveShieldedAddressFromViewingKey(key: String): String @JvmStatic - private external fun deriveTransparentAddressFromSeed(seed: ByteArray): String + private external fun deriveTransparentAddressFromSeed(seed: ByteArray, account: Int, index: Int): String + + @JvmStatic + private external fun deriveTransparentAddressFromSecretKey(tsk: String): String + + @JvmStatic + private external fun deriveTransparentSecretKeyFromSeed(seed: ByteArray, account: Int, index: Int): String + } } diff --git a/src/main/java/cash/z/ecc/android/sdk/tool/SaplingParamTool.kt b/src/main/java/cash/z/ecc/android/sdk/tool/SaplingParamTool.kt index c46e663c..0e6ea5a1 100644 --- a/src/main/java/cash/z/ecc/android/sdk/tool/SaplingParamTool.kt +++ b/src/main/java/cash/z/ecc/android/sdk/tool/SaplingParamTool.kt @@ -70,10 +70,16 @@ class SaplingParamTool { twig("directory did not exist attempting to make it") file.parentFile.mkdirs() } + Okio.buffer(Okio.sink(file)).use { + twig("writing to $file") + it.writeAll(response.body().source()) + } } else { failureMessage += "Error while fetching $paramFileName : $response\n" twig(failureMessage) } + + twig("fetch succeeded, done writing $paramFileName") } if (failureMessage.isNotEmpty()) throw TransactionEncoderException.FetchParamsException( failureMessage diff --git a/src/main/java/cash/z/ecc/android/sdk/transaction/PersistentTransactionManager.kt b/src/main/java/cash/z/ecc/android/sdk/transaction/PersistentTransactionManager.kt index ee9ae512..f91849d3 100644 --- a/src/main/java/cash/z/ecc/android/sdk/transaction/PersistentTransactionManager.kt +++ b/src/main/java/cash/z/ecc/android/sdk/transaction/PersistentTransactionManager.kt @@ -139,6 +139,40 @@ class PersistentTransactionManager( tx } + override suspend fun encode( + spendingKey: String, + transparentSecretKey: String, + pendingTx: PendingTransaction + ): PendingTransaction { + twig("managing the creation of a shielding transaction") + var tx = pendingTx as PendingTransactionEntity + try { + twig("beginning to encode shielding transaction with : $encoder") + val encodedTx = encoder.createShieldingTransaction( + spendingKey, + transparentSecretKey, + tx.memo + ) + twig("successfully encoded shielding transaction!") + safeUpdate("updating shielding transaction encoding") { + updateEncoding(tx.id, encodedTx.raw, encodedTx.txId, encodedTx.expiryHeight) + } + } catch (t: Throwable) { + val message = "failed to encode shielding transaction due to : ${t.message} caused by: ${t.cause}" + twig(message) + safeUpdate("updating shielding transaction error info") { + updateError(tx.id, message, ERROR_ENCODING) + } + } finally { + safeUpdate("incrementing shielding transaction encodeAttempts (from: ${tx.encodeAttempts})") { + updateEncodeAttempts(tx.id, max(1, tx.encodeAttempts + 1)) + tx = findById(tx.id)!! + } + } + + return tx + } + override suspend fun submit(pendingTx: PendingTransaction): PendingTransaction = withContext(Dispatchers.IO) { // reload the tx to check for cancellation var tx = pendingTransactionDao { findById(pendingTx.id) } diff --git a/src/main/java/cash/z/ecc/android/sdk/transaction/TransactionEncoder.kt b/src/main/java/cash/z/ecc/android/sdk/transaction/TransactionEncoder.kt index 46d6adea..1d0befac 100644 --- a/src/main/java/cash/z/ecc/android/sdk/transaction/TransactionEncoder.kt +++ b/src/main/java/cash/z/ecc/android/sdk/transaction/TransactionEncoder.kt @@ -24,6 +24,12 @@ interface TransactionEncoder { fromAccountIndex: Int = 0 ): EncodedTransaction + suspend fun createShieldingTransaction( + spendingKey: String, + transparentSecretKey: String, + memo: ByteArray? = byteArrayOf() + ): EncodedTransaction + /** * Utility function to help with validation. This is not called during [createTransaction] * because this class asserts that all validation is done externally by the UI, for now. diff --git a/src/main/java/cash/z/ecc/android/sdk/transaction/TransactionManager.kt b/src/main/java/cash/z/ecc/android/sdk/transaction/TransactionManager.kt index d88955da..c852b697 100644 --- a/src/main/java/cash/z/ecc/android/sdk/transaction/TransactionManager.kt +++ b/src/main/java/cash/z/ecc/android/sdk/transaction/TransactionManager.kt @@ -39,6 +39,12 @@ interface OutboundTransactionManager { */ suspend fun encode(spendingKey: String, pendingTx: PendingTransaction): PendingTransaction + suspend fun encode( + spendingKey: String, + transparentSecretKey: String, + pendingTx: PendingTransaction + ): PendingTransaction + /** * Submits the transaction represented by [pendingTx] to lightwalletd to broadcast to the * network and, hopefully, include in the next block. diff --git a/src/main/java/cash/z/ecc/android/sdk/transaction/WalletTransactionEncoder.kt b/src/main/java/cash/z/ecc/android/sdk/transaction/WalletTransactionEncoder.kt index d4194a78..25bd64f6 100644 --- a/src/main/java/cash/z/ecc/android/sdk/transaction/WalletTransactionEncoder.kt +++ b/src/main/java/cash/z/ecc/android/sdk/transaction/WalletTransactionEncoder.kt @@ -51,6 +51,17 @@ class WalletTransactionEncoder( ?: throw TransactionEncoderException.TransactionNotFoundException(transactionId) } + override suspend fun createShieldingTransaction( + spendingKey: String, + transparentSecretKey: String, + memo: ByteArray? + ): EncodedTransaction = withContext(IO) { + twig("TMP: createShieldingTransaction with $spendingKey and $transparentSecretKey and ${memo?.size}") + val transactionId = createShieldingSpend(spendingKey, transparentSecretKey, memo) + repository.findEncodedTransactionById(transactionId) + ?: throw TransactionEncoderException.TransactionNotFoundException(transactionId) + } + /** * Utility function to help with validation. This is not called during [createTransaction] * because this class asserts that all validation is done externally by the UI, for now. @@ -126,4 +137,29 @@ class WalletTransactionEncoder( twig("result of sendToAddress: $result") } } + + private suspend fun createShieldingSpend( + spendingKey: String, + transparentSecretKey: String, + memo: ByteArray? = byteArrayOf() + ): Long = withContext(IO) { + twigTask("creating transaction to shield all UTXOs") { + try { + SaplingParamTool.ensureParams((rustBackend as RustBackend).pathParamsDir) + twig("params exist! attempting to shield...") + rustBackend.shieldToAddress( + spendingKey, + transparentSecretKey, + memo + ) + } catch (t: Throwable) { + // TODO: if this error matches: Insufficient balance (have 0, need 1000 including fee) + // then consider custom error that says no UTXOs existed to shield + twig("Shield failed due to: ${t.message}") + throw t + } + }.also { result -> + twig("result of shieldToAddress: $result") + } + } } diff --git a/src/main/proto/service.proto b/src/main/proto/service.proto index 0d642a21..d0a70856 100644 --- a/src/main/proto/service.proto +++ b/src/main/proto/service.proto @@ -34,7 +34,7 @@ message TxFilter { // RawTransaction contains the complete transaction data. It also optionally includes // the block height in which the transaction was included. message RawTransaction { - bytes data = 1; // exact data returned by zcash 'getrawtransaction' + bytes data = 1; // exact data returned by Zcash 'getrawtransaction' uint64 height = 2; // height that the transaction was mined (or -1) } @@ -66,6 +66,9 @@ message LightdInfo { string branch = 9; string buildDate = 10; string buildUser = 11; + uint64 estimatedHeight = 12; // less than tip height if zcashd is syncing + string zcashdBuild = 13; // example: "v4.1.1-877212414" + string zcashdSubversion = 14; // example: "/MagicBean:4.1.1/" } // TransparentAddressBlockFilter restricts the results to the given address @@ -104,7 +107,7 @@ message Exclude { repeated bytes txid = 1; } -// The TreeState is derived from the zcash z_gettreestate rpc. +// The TreeState is derived from the Zcash z_gettreestate rpc. message TreeState { string network = 1; // "main" or "test" uint64 height = 2; @@ -139,7 +142,7 @@ service CompactTxStreamer { // Return the requested full (not compact) transaction (as from zcashd) rpc GetTransaction(TxFilter) returns (RawTransaction) {} - // Submit the given transaction to the zcash network + // Submit the given transaction to the Zcash network rpc SendTransaction(RawTransaction) returns (SendResponse) {} // Return the txids corresponding to the given t-address within the given block range @@ -159,7 +162,7 @@ service CompactTxStreamer { rpc GetMempoolTx(Exclude) returns (stream CompactTx) {} // GetTreeState returns the note commitment tree state corresponding to the given block. - // See section 3.7 of the zcash protocol specification. It returns several other useful + // See section 3.7 of the Zcash protocol specification. It returns several other useful // values also (even though they can be obtained using GetBlock). // The block can be specified by either height or hash. rpc GetTreeState(BlockID) returns (TreeState) {}