Use the binary USK format for transaction creation

Co-authored-by: Carter Jernigan <git@carterjernigan.com>
This commit is contained in:
Jack Grigg 2022-09-29 18:04:00 +01:00 committed by Carter Jernigan
parent 3b826f8f6a
commit 88bbd0afcb
15 changed files with 205 additions and 181 deletions

View File

@ -13,6 +13,8 @@ Change Log
- `FirstClassByteArray` - `FirstClassByteArray`
- `UnifiedSpendingKey` - `UnifiedSpendingKey`
- `cash.z.ecc.android.sdk.tool`: - `cash.z.ecc.android.sdk.tool`:
- `DerivationTool.deriveUnifiedSpendingKey`
- `DerivationTool.deriveUnifiedFullViewingKey`
- `DerivationTool.deriveTransparentAccountPrivateKey` - `DerivationTool.deriveTransparentAccountPrivateKey`
- `DerivationTool.deriveTransparentAddressFromAccountPrivateKey` - `DerivationTool.deriveTransparentAddressFromAccountPrivateKey`
- `DerivationTool.deriveUnifiedAddress` - `DerivationTool.deriveUnifiedAddress`
@ -36,8 +38,11 @@ Change Log
- `Synchronizer.Companion.new` now takes a `seed` argument. A non-null value should be - `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 provided if `Synchronizer.Companion.new` throws an error that a database migration
requires the wallet seed. requires the wallet seed.
- `Synchronizer.shieldFunds` now takes a transparent account private key (representing - `Synchronizer.sendToAddress` now takes a `UnifiedSpendingKey` instead of an encoded
all transparent secret keys within an account) instead of a transparent secret key. 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 ### Removed
- `cash.z.ecc.android.sdk`: - `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 public key, and not the extended public key as intended. This made it incompatible
with ZIP 316. with ZIP 316.
- `cash.z.ecc.android.sdk.tool`: - `cash.z.ecc.android.sdk.tool`:
- `DerivationTool.deriveSpendingKeys` (use
`DerivationTool.deriveUnifiedSpendingKey` instead).
- `DerivationTool.deriveViewingKey` (use
- `DerivationTool.deriveUnifiedFullViewingKey` instead).
- `DerivationTool.deriveTransparentAddressFromPrivateKey` (use - `DerivationTool.deriveTransparentAddressFromPrivateKey` (use
`DerivationTool.deriveTransparentAddressFromAccountPrivateKey` instead). `DerivationTool.deriveTransparentAddressFromAccountPrivateKey` instead).
- `DerivationTool.deriveTransparentSecretKey` (use - `DerivationTool.deriveTransparentSecretKey` (use

View File

@ -43,14 +43,14 @@ class GetPrivateKeyFragment : BaseDemoFragment<FragmentGetPrivateKeyBinding>() {
// demonstrate deriving spending keys for five accounts but only take the first one // demonstrate deriving spending keys for five accounts but only take the first one
lifecycleScope.launchWhenStarted { lifecycleScope.launchWhenStarted {
@Suppress("MagicNumber") @Suppress("MagicNumber")
val spendingKey = DerivationTool.deriveSpendingKeys( val spendingKey = DerivationTool.deriveUnifiedSpendingKey(
seed, seed,
ZcashNetwork.fromResources(requireApplicationContext()), ZcashNetwork.fromResources(requireApplicationContext()),
5 5
).first() )
// derive the key that allows you to view but not spend transactions // derive the key that allows you to view but not spend transactions
val viewingKey = DerivationTool.deriveViewingKey( val viewingKey = DerivationTool.deriveUnifiedFullViewingKey(
spendingKey, spendingKey,
ZcashNetwork.fromResources(requireApplicationContext()) ZcashNetwork.fromResources(requireApplicationContext())
) )

View File

@ -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.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.LightWalletEndpoint
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.WalletBalance import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.model.defaultForNetwork import cash.z.ecc.android.sdk.model.defaultForNetwork
@ -53,7 +54,7 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
// in a normal app, this would be stored securely with the trusted execution environment (TEE) // 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 // 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 * Initialize the required values that would normally live outside the demo but are repeated
@ -76,7 +77,7 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
birthday = null birthday = null
) )
spendingKey = runBlocking { spendingKey = runBlocking {
DerivationTool.deriveSpendingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first() DerivationTool.deriveUnifiedSpendingKey(seed, ZcashNetwork.fromResources(requireApplicationContext()))
} }
} }

View File

@ -661,17 +661,16 @@ class SdkSynchronizer internal constructor(
processor.getTransparentAddress(accountId) processor.getTransparentAddress(accountId)
override fun sendToAddress( override fun sendToAddress(
spendingKey: String, usk: UnifiedSpendingKey,
amount: Zatoshi, amount: Zatoshi,
toAddress: String, toAddress: String,
memo: String, memo: String,
fromAccountIndex: Int
): Flow<PendingTransaction> = flow { ): Flow<PendingTransaction> = flow {
twig("Initializing pending transaction") twig("Initializing pending transaction")
// Emit the placeholder transaction, then switch to monitoring the database // 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) 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. // only submit if it wasn't cancelled. Otherwise cleanup, immediately for best UX.
if (encodedTx.isCancelled()) { if (encodedTx.isCancelled()) {
twig("[cleanup] this tx has been cancelled so we will cleanup instead of submitting") twig("[cleanup] this tx has been cancelled so we will cleanup instead of submitting")
@ -691,20 +690,20 @@ class SdkSynchronizer internal constructor(
}.distinctUntilChanged() }.distinctUntilChanged()
override fun shieldFunds( override fun shieldFunds(
spendingKey: String, usk: UnifiedSpendingKey,
transparentAccountPrivateKey: String,
memo: String memo: String
): Flow<PendingTransaction> = flow { ): Flow<PendingTransaction> = flow {
twig("Initializing shielding transaction") twig("Initializing shielding transaction")
val tAddr = // TODO(str4d): This only shields funds from the current UA's transparent receiver. Fix this once we start
DerivationTool.deriveTransparentAddressFromAccountPrivateKey(transparentAccountPrivateKey, network) // rolling UAs.
val tAddr = processor.getTransparentAddress(usk.account)
val tBalance = processor.getUtxoCacheBalance(tAddr) val tBalance = processor.getUtxoCacheBalance(tAddr)
val zAddr = getCurrentAddress(0) val zAddr = getCurrentAddress(usk.account)
// Emit the placeholder transaction, then switch to monitoring the database // 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) 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. // only submit if it wasn't cancelled. Otherwise cleanup, immediately for best UX.
if (encodedTx.isCancelled()) { if (encodedTx.isCancelled()) {
twig("[cleanup] this shielding tx has been cancelled so we will cleanup instead of submitting") twig("[cleanup] this shielding tx has been cancelled so we will cleanup instead of submitting")

View File

@ -225,11 +225,10 @@ interface Synchronizer {
/** /**
* Sends zatoshi. * 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 zatoshi the amount of zatoshi to send.
* @param toAddress the recipient's address. * @param toAddress the recipient's address.
* @param memo the optional memo to include as part of the transaction. * @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 * @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 * 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. * for any wallet that wants to ignore this return value.
*/ */
fun sendToAddress( fun sendToAddress(
spendingKey: String, usk: UnifiedSpendingKey,
amount: Zatoshi, amount: Zatoshi,
toAddress: String, toAddress: String,
memo: String = "", memo: String = "",
fromAccountIndex: Int = 0
): Flow<PendingTransaction> ): Flow<PendingTransaction>
fun shieldFunds( fun shieldFunds(
spendingKey: String, usk: UnifiedSpendingKey,
transparentAccountPrivateKey: String,
memo: String = ZcashSdk.DEFAULT_SHIELD_FUNDS_MEMO_PREFIX memo: String = ZcashSdk.DEFAULT_SHIELD_FUNDS_MEMO_PREFIX
): Flow<PendingTransaction> ): Flow<PendingTransaction>

View File

@ -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.service.LightWalletService
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight 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 cash.z.ecc.android.sdk.model.Zatoshi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
@ -108,21 +109,23 @@ class PersistentTransactionManager(
} }
override suspend fun encode( override suspend fun encode(
spendingKey: String, usk: UnifiedSpendingKey,
pendingTx: PendingTransaction pendingTx: PendingTransaction
): PendingTransaction = withContext(Dispatchers.IO) { ): PendingTransaction = withContext(Dispatchers.IO) {
twig("managing the creation of a transaction") twig("managing the creation of a transaction")
var tx = pendingTx as PendingTransactionEntity 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") @Suppress("TooGenericExceptionCaught")
try { try {
twig("beginning to encode transaction with : $encoder") twig("beginning to encode transaction with : $encoder")
val encodedTx = encoder.createTransaction( val encodedTx = encoder.createTransaction(
spendingKey, usk,
tx.valueZatoshi, tx.valueZatoshi,
tx.toAddress, tx.toAddress,
tx.memo, tx.memo,
tx.accountIndex
) )
twig("successfully encoded transaction!") twig("successfully encoded transaction!")
safeUpdate("updating transaction encoding", -1) { 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. // spendingKey is removed. Figure out where these methods need to be renamed, and do so.
override suspend fun encode( override suspend fun encode(
spendingKey: String, // TODO(str4d): Remove this argument. spendingKey: String, // TODO(str4d): Remove this argument.
transparentAccountPrivateKey: String, usk: UnifiedSpendingKey,
pendingTx: PendingTransaction pendingTx: PendingTransaction
): PendingTransaction { ): PendingTransaction {
twig("managing the creation of a shielding transaction") twig("managing the creation of a shielding transaction")
@ -158,7 +161,7 @@ class PersistentTransactionManager(
try { try {
twig("beginning to encode shielding transaction with : $encoder") twig("beginning to encode shielding transaction with : $encoder")
val encodedTx = encoder.createShieldingTransaction( val encodedTx = encoder.createShieldingTransaction(
transparentAccountPrivateKey, usk,
tx.memo tx.memo
) )
twig("successfully encoded shielding transaction!") twig("successfully encoded shielding transaction!")

View File

@ -1,41 +1,37 @@
package cash.z.ecc.android.sdk.internal.transaction package cash.z.ecc.android.sdk.internal.transaction
import cash.z.ecc.android.sdk.db.entity.EncodedTransaction 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 import cash.z.ecc.android.sdk.model.Zatoshi
interface TransactionEncoder { interface TransactionEncoder {
// TODO(str4d): Migrate to binary USK format.
/** /**
* Creates a transaction, throwing an exception whenever things are missing. When the provided * 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 * wallet implementation doesn't throw an exception, we wrap the issue into a descriptive
* exception ourselves (rather than using double-bangs for things). * 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 amount the amount of zatoshi to send.
* @param toAddress the recipient's address. * @param toAddress the recipient's address.
* @param memo the optional memo to include as part of the transaction. * @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 * @return the successfully encoded transaction or an exception
*/ */
suspend fun createTransaction( suspend fun createTransaction(
spendingKey: String, usk: UnifiedSpendingKey,
amount: Zatoshi, amount: Zatoshi,
toAddress: String, toAddress: String,
memo: ByteArray? = byteArrayOf(), memo: ByteArray? = byteArrayOf(),
fromAccountIndex: Int = 0
): EncodedTransaction ): 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. * @param memo the optional memo to include as part of the transaction.
*/ */
suspend fun createShieldingTransaction( suspend fun createShieldingTransaction(
transparentAccountPrivateKey: String, usk: UnifiedSpendingKey,
memo: ByteArray? = byteArrayOf() memo: ByteArray? = byteArrayOf()
): EncodedTransaction ): EncodedTransaction

View File

@ -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.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.model.BlockHeight 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 cash.z.ecc.android.sdk.model.Zatoshi
import kotlinx.coroutines.flow.Flow 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 * Encode the pending transaction using the given spending key. This is a local operation that
* produces a raw transaction to submit to lightwalletd. * 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 * @param pendingTx the transaction information created by [initSpend] that will be used to
* construct a transaction. * construct a transaction.
* *
* @return the resulting pending transaction whose ID can be used to monitor for changes. * @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( suspend fun encode(
spendingKey: String, spendingKey: String,
transparentAccountPrivateKey: String, usk: UnifiedSpendingKey,
pendingTx: PendingTransaction pendingTx: PendingTransaction
): PendingTransaction ): PendingTransaction

View File

@ -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.internal.twigTask
import cash.z.ecc.android.sdk.jni.RustBackend import cash.z.ecc.android.sdk.jni.RustBackend
import cash.z.ecc.android.sdk.jni.RustBackendWelding import cash.z.ecc.android.sdk.jni.RustBackendWelding
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.Zatoshi 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 * wallet implementation doesn't throw an exception, we wrap the issue into a descriptive
* exception ourselves (rather than using double-bangs for things). * 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 amount the amount of zatoshi to send.
* @param toAddress the recipient's address. * @param toAddress the recipient's address.
* @param memo the optional memo to include as part of the transaction. * @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 * @return the successfully encoded transaction or an exception
*/ */
override suspend fun createTransaction( override suspend fun createTransaction(
spendingKey: String, usk: UnifiedSpendingKey,
amount: Zatoshi, amount: Zatoshi,
toAddress: String, toAddress: String,
memo: ByteArray?, memo: ByteArray?,
fromAccountIndex: Int
): EncodedTransaction { ): EncodedTransaction {
val transactionId = createSpend(spendingKey, amount, toAddress, memo) val transactionId = createSpend(usk, amount, toAddress, memo)
return repository.findEncodedTransactionById(transactionId) return repository.findEncodedTransactionById(transactionId)
?: throw TransactionEncoderException.TransactionNotFoundException(transactionId) ?: throw TransactionEncoderException.TransactionNotFoundException(transactionId)
} }
override suspend fun createShieldingTransaction( override suspend fun createShieldingTransaction(
transparentAccountPrivateKey: String, usk: UnifiedSpendingKey,
memo: ByteArray? memo: ByteArray?
): EncodedTransaction { ): EncodedTransaction {
val transactionId = createShieldingSpend(transparentAccountPrivateKey, memo) val transactionId = createShieldingSpend(usk, memo)
return repository.findEncodedTransactionById(transactionId) return repository.findEncodedTransactionById(transactionId)
?: throw TransactionEncoderException.TransactionNotFoundException(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 * 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. * 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 amount the amount of zatoshi to send.
* @param toAddress the recipient's address. * @param toAddress the recipient's address.
* @param memo the optional memo to include as part of the transaction. * @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 * @return the row id in the transactions table that contains the spend transaction or -1 if it
* failed. * failed.
*/ */
private suspend fun createSpend( private suspend fun createSpend(
spendingKey: String, usk: UnifiedSpendingKey,
amount: Zatoshi, amount: Zatoshi,
toAddress: String, toAddress: String,
memo: ByteArray? = byteArrayOf(), memo: ByteArray? = byteArrayOf(),
fromAccountIndex: Int = 0
): Long { ): Long {
return twigTask( return twigTask(
"creating transaction to spend $amount zatoshi to" + "creating transaction to spend $amount zatoshi to" +
@ -128,8 +125,7 @@ internal class WalletTransactionEncoder(
SaplingParamTool.ensureParams((rustBackend as RustBackend).pathParamsDir) SaplingParamTool.ensureParams((rustBackend as RustBackend).pathParamsDir)
twig("params exist! attempting to send...") twig("params exist! attempting to send...")
rustBackend.createToAddress( rustBackend.createToAddress(
fromAccountIndex, usk,
spendingKey,
toAddress, toAddress,
amount.value, amount.value,
memo memo
@ -144,7 +140,7 @@ internal class WalletTransactionEncoder(
} }
private suspend fun createShieldingSpend( private suspend fun createShieldingSpend(
transparentAccountPrivateKey: String, usk: UnifiedSpendingKey,
memo: ByteArray? = byteArrayOf() memo: ByteArray? = byteArrayOf()
): Long { ): Long {
return twigTask("creating transaction to shield all UTXOs") { return twigTask("creating transaction to shield all UTXOs") {
@ -153,7 +149,7 @@ internal class WalletTransactionEncoder(
SaplingParamTool.ensureParams((rustBackend as RustBackend).pathParamsDir) SaplingParamTool.ensureParams((rustBackend as RustBackend).pathParamsDir)
twig("params exist! attempting to shield...") twig("params exist! attempting to shield...")
rustBackend.shieldToAddress( rustBackend.shieldToAddress(
transparentAccountPrivateKey, usk,
memo memo
) )
} catch (t: Throwable) { } catch (t: Throwable) {

View File

@ -225,16 +225,15 @@ internal class RustBackend private constructor(
} }
override suspend fun createToAddress( override suspend fun createToAddress(
account: Int, usk: UnifiedSpendingKey,
extsk: String,
to: String, to: String,
value: Long, value: Long,
memo: ByteArray? memo: ByteArray?
): Long = withContext(SdkDispatchers.DATABASE_IO) { ): Long = withContext(SdkDispatchers.DATABASE_IO) {
createToAddress( createToAddress(
dataDbFile.absolutePath, dataDbFile.absolutePath,
account, usk.account,
extsk, usk.bytes.byteArray,
to, to,
value, value,
memo ?: ByteArray(0), memo ?: ByteArray(0),
@ -245,15 +244,15 @@ internal class RustBackend private constructor(
} }
override suspend fun shieldToAddress( override suspend fun shieldToAddress(
xprv: String, usk: UnifiedSpendingKey,
memo: ByteArray? memo: ByteArray?
): Long { ): Long {
twig("TMP: shieldToAddress with db path: $dataDbFile, ${memo?.size}") twig("TMP: shieldToAddress with db path: $dataDbFile, ${memo?.size}")
return withContext(SdkDispatchers.DATABASE_IO) { return withContext(SdkDispatchers.DATABASE_IO) {
shieldToAddress( shieldToAddress(
dataDbFile.absolutePath, dataDbFile.absolutePath,
0, usk.account,
xprv, usk.bytes.byteArray,
memo ?: ByteArray(0), memo ?: ByteArray(0),
"$pathParamsDir/$SPEND_PARAM_FILE_NAME", "$pathParamsDir/$SPEND_PARAM_FILE_NAME",
"$pathParamsDir/$OUTPUT_PARAM_FILE_NAME", "$pathParamsDir/$OUTPUT_PARAM_FILE_NAME",
@ -429,6 +428,12 @@ internal class RustBackend private constructor(
@JvmStatic @JvmStatic
private external fun getSaplingReceiverForUnifiedAddress(ua: String): String? private external fun getSaplingReceiverForUnifiedAddress(ua: String): String?
internal fun validateUnifiedSpendingKey(bytes: ByteArray) =
isValidSpendingKey(bytes)
@JvmStatic
private external fun isValidSpendingKey(bytes: ByteArray): Boolean
@JvmStatic @JvmStatic
private external fun isValidShieldedAddress(addr: String, networkId: Int): Boolean private external fun isValidShieldedAddress(addr: String, networkId: Int): Boolean
@ -510,7 +515,7 @@ internal class RustBackend private constructor(
private external fun createToAddress( private external fun createToAddress(
dbDataPath: String, dbDataPath: String,
account: Int, account: Int,
extsk: String, usk: ByteArray,
to: String, to: String,
value: Long, value: Long,
memo: ByteArray, memo: ByteArray,
@ -524,7 +529,7 @@ internal class RustBackend private constructor(
private external fun shieldToAddress( private external fun shieldToAddress(
dbDataPath: String, dbDataPath: String,
account: Int, account: Int,
xprv: String, usk: ByteArray,
memo: ByteArray, memo: ByteArray,
spendParamsPath: String, spendParamsPath: String,
outputParamsPath: String, outputParamsPath: String,

View File

@ -21,15 +21,14 @@ internal interface RustBackendWelding {
@Suppress("LongParameterList") @Suppress("LongParameterList")
suspend fun createToAddress( suspend fun createToAddress(
account: Int, usk: UnifiedSpendingKey,
extsk: String,
to: String, to: String,
value: Long, value: Long,
memo: ByteArray? = byteArrayOf() memo: ByteArray? = byteArrayOf()
): Long ): Long
suspend fun shieldToAddress( suspend fun shieldToAddress(
xprv: String, usk: UnifiedSpendingKey,
memo: ByteArray? = byteArrayOf() memo: ByteArray? = byteArrayOf()
): Long ): Long
@ -110,11 +109,11 @@ internal interface RustBackendWelding {
accountIndex: Int = 0 accountIndex: Int = 0
): String ): String
suspend fun deriveSpendingKeys( suspend fun deriveUnifiedSpendingKey(
seed: ByteArray, seed: ByteArray,
network: ZcashNetwork, network: ZcashNetwork,
numberOfAccounts: Int = 1 account: Int = 0
): Array<String> ): UnifiedSpendingKey
suspend fun deriveTransparentAddress( suspend fun deriveTransparentAddress(
seed: ByteArray, seed: ByteArray,
@ -140,10 +139,10 @@ internal interface RustBackendWelding {
account: Int = 0 account: Int = 0
): String ): String
suspend fun deriveViewingKey( suspend fun deriveUnifiedFullViewingKey(
spendingKey: String, usk: UnifiedSpendingKey,
network: ZcashNetwork network: ZcashNetwork
): String ): UnifiedFullViewingKey
suspend fun deriveUnifiedFullViewingKeys( suspend fun deriveUnifiedFullViewingKeys(
seed: ByteArray, seed: ByteArray,

View File

@ -1,5 +1,7 @@
package cash.z.ecc.android.sdk.model 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. * 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)" override fun toString() = "UnifiedSpendingKey(account=$account)"
fun copyBytes() = bytes.byteArray.copyOf() fun copyBytes() = bytes.byteArray.copyOf()
companion object {
suspend fun new(account: Int, bytes: ByteArray): Result<UnifiedSpendingKey> {
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)))
}
}
}
} }

View File

@ -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.RustBackend
import cash.z.ecc.android.sdk.jni.RustBackendWelding 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.model.ZcashNetwork
import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey 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( override suspend fun deriveUnifiedFullViewingKey(
spendingKey: String, usk: UnifiedSpendingKey,
network: ZcashNetwork network: ZcashNetwork
): String = withRustBackendLoaded { ): UnifiedFullViewingKey = withRustBackendLoaded {
deriveExtendedFullViewingKey(spendingKey, networkId = network.id) 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 seed the seed from which to derive spending keys.
* @param numberOfAccounts the number of accounts to use. Multiple accounts are not fully * @param account the account to derive.
* supported so the default value of 1 is recommended.
* *
* @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, seed: ByteArray,
network: ZcashNetwork, network: ZcashNetwork,
numberOfAccounts: Int account: Int
): Array<String> = withRustBackendLoaded { ): UnifiedSpendingKey = withRustBackendLoaded {
deriveExtendedSpendingKeys(seed, numberOfAccounts, networkId = network.id) deriveSpendingKey(seed, account, networkId = network.id)
} }
/** /**
@ -146,11 +152,11 @@ class DerivationTool {
// //
@JvmStatic @JvmStatic
private external fun deriveExtendedSpendingKeys( private external fun deriveSpendingKey(
seed: ByteArray, seed: ByteArray,
numberOfAccounts: Int, account: Int,
networkId: Int networkId: Int
): Array<String> ): UnifiedSpendingKey
@JvmStatic @JvmStatic
private external fun deriveUnifiedFullViewingKeysFromSeed( private external fun deriveUnifiedFullViewingKeysFromSeed(
@ -160,7 +166,7 @@ class DerivationTool {
): Array<String> ): Array<String>
@JvmStatic @JvmStatic
private external fun deriveExtendedFullViewingKey(spendingKey: String, networkId: Int): String private external fun deriveUnifiedFullViewingKey(usk: ByteArray, networkId: Int): String
@JvmStatic @JvmStatic
private external fun deriveUnifiedAddressFromSeed( private external fun deriveUnifiedAddressFromSeed(

View File

@ -1,5 +1,7 @@
package cash.z.ecc.android.sdk.type 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. * A [ZIP 316] Unified Full Viewing Key, corresponding to a single wallet account.
* *

View File

@ -20,9 +20,9 @@ use jni::{
use log::Level; use log::Level;
use schemer::MigratorError; use schemer::MigratorError;
use secp256k1::PublicKey; use secp256k1::PublicKey;
use secrecy::SecretVec; use secrecy::{ExposeSecret, SecretVec};
use zcash_address::{ToAddress, ZcashAddress}; use zcash_address::{ToAddress, ZcashAddress};
use zcash_client_backend::keys::UnifiedSpendingKey; use zcash_client_backend::keys::{DecodingError, UnifiedSpendingKey};
use zcash_client_backend::{ use zcash_client_backend::{
address::{RecipientAddress, UnifiedAddress}, address::{RecipientAddress, UnifiedAddress},
data_api::{ data_api::{
@ -33,11 +33,8 @@ use zcash_client_backend::{
}, },
WalletRead, WalletReadTransparent, WalletWrite, WalletWriteTransparent, WalletRead, WalletReadTransparent, WalletWrite, WalletWriteTransparent,
}, },
encoding::{ encoding::AddressCodec,
decode_extended_spending_key, encode_extended_full_viewing_key, keys::{Era, UnifiedFullViewingKey},
encode_extended_spending_key, AddressCodec,
},
keys::{sapling, Era, UnifiedFullViewingKey},
wallet::{OvkPolicy, WalletTransparentOutput}, wallet::{OvkPolicy, WalletTransparentOutput},
}; };
use zcash_client_sqlite::wallet::init::WalletMigrationError; use zcash_client_sqlite::wallet::init::WalletMigrationError;
@ -60,7 +57,7 @@ use zcash_primitives::{
components::{Amount, OutPoint, TxOut}, components::{Amount, OutPoint, TxOut},
Transaction, Transaction,
}, },
zip32::{AccountId, DiversifierIndex, ExtendedFullViewingKey}, zip32::{AccountId, DiversifierIndex},
}; };
use zcash_proofs::prover::LocalTxProver; 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) unwrap_exc_or(&env, res, -1)
} }
fn encode_usk(
env: &JNIEnv<'_>,
account: AccountId,
usk: UnifiedSpendingKey,
) -> Result<jobject, failure::Error> {
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<UnifiedSpendingKey, failure::Error> {
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 /// Adds the next available account-level spend authority, given the current set of
/// [ZIP 316] account identifiers known, to the wallet database. /// [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) .create_account(&seed)
.map_err(|e| format_err!("Error while initializing accounts: {}", e))?; .map_err(|e| format_err!("Error while initializing accounts: {}", e))?;
let encoded = usk.to_bytes(Era::Orchard); encode_usk(&env, account, usk)
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())
}); });
unwrap_exc_or(&env, res, ptr::null_mut()) 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) 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] #[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<'_>, env: JNIEnv<'_>,
_: JClass<'_>, _: JClass<'_>,
seed: jbyteArray, seed: jbyteArray,
accounts: jint, account: jint,
network_id: jint, network_id: jint,
) -> jobjectArray { ) -> jobject {
let res = panic::catch_unwind(|| { let res = panic::catch_unwind(|| {
let network = parse_network(network_id as u32)?; let network = parse_network(network_id as u32)?;
let seed = env.convert_byte_array(seed).unwrap(); let seed = SecretVec::new(env.convert_byte_array(seed).unwrap());
let accounts = if accounts > 0 { let account = if account >= 0 {
accounts as u32 AccountId::from(account as u32)
} else { } else {
return Err(format_err!("accounts argument must be greater than zero")); return Err(format_err!("accounts argument must be greater than zero"));
}; };
let extsks: Vec<_> = (0..accounts) let usk = UnifiedSpendingKey::from_seed(&network, seed.expose_secret(), account)
.map(|account| { .map_err(|e| format_err!("error generating unified spending key from seed: {:?}", e))?;
sapling::spending_key(&seed, network.coin_type(), AccountId::from(account))
})
.collect();
Ok(utils::rust_vec_to_java( encode_usk(&env, account, usk)
&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(""),
))
}); });
unwrap_exc_or(&env, res, ptr::null_mut()) 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] #[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<'_>, env: JNIEnv<'_>,
_: JClass<'_>, _: JClass<'_>,
extsk_string: JString<'_>, usk: jbyteArray,
network_id: jint, network_id: jint,
) -> jobjectArray { ) -> jstring {
let res = panic::catch_unwind(|| { let res = panic::catch_unwind(|| {
let usk = decode_usk(&env, usk)?;
let network = parse_network(network_id as u32)?; 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( let ufvk = usk.to_unified_full_viewing_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 output = env let output = env
.new_string(encode_extended_full_viewing_key( .new_string(ufvk.encode(&network))
network.hrp_sapling_extended_full_viewing_key(),
&extfvk,
))
.expect("Couldn't create Java string!"); .expect("Couldn't create Java string!");
Ok(output.into_inner()) 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()) 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] #[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_isValidShieldedAddress( pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_isValidShieldedAddress(
env: JNIEnv<'_>, env: JNIEnv<'_>,
@ -1222,7 +1235,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_createToAdd
_: JClass<'_>, _: JClass<'_>,
db_data: JString<'_>, db_data: JString<'_>,
account: jint, account: jint,
extsk: JString<'_>, usk: jbyteArray,
to: JString<'_>, to: JString<'_>,
value: jlong, value: jlong,
memo: jbyteArray, memo: jbyteArray,
@ -1239,7 +1252,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_createToAdd
} else { } else {
return Err(format_err!("account argument must be nonnegative")); 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 to = utils::java_string_to_rust(&env, to);
let value = let value =
Amount::from_i64(value).map_err(|()| format_err!("Invalid amount, out of range"))?; 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 spend_params = utils::java_string_to_rust(&env, spend_params);
let output_params = utils::java_string_to_rust(&env, output_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) { let to = match RecipientAddress::decode(&network, &to) {
Some(to) => to, Some(to) => to,
None => { None => {
@ -1284,7 +1288,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_createToAdd
&network, &network,
prover, prover,
AccountId::from(account), AccountId::from(account),
&extsk, usk.sapling(),
&to, &to,
value, value,
memo, memo,
@ -1302,7 +1306,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_shieldToAdd
_: JClass<'_>, _: JClass<'_>,
db_data: JString<'_>, db_data: JString<'_>,
account: jint, account: jint,
xprv: JString<'_>, usk: jbyteArray,
memo: jbyteArray, memo: jbyteArray,
spend_params: JString<'_>, spend_params: JString<'_>,
output_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 network = parse_network(network_id as u32)?;
let db_data = wallet_db(&env, network, db_data)?; let db_data = wallet_db(&env, network, db_data)?;
let mut db_data = db_data.get_update_ops()?; let mut db_data = db_data.get_update_ops()?;
let account = if account == 0 { let account = if account >= 0 {
account as u32 account as u32
} else { } else {
return Err(format_err!( return Err(format_err!("account argument must be nonnegative"));
"account argument {} must be nonnegative",
account
));
}; };
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 memo_bytes = env.convert_byte_array(memo).unwrap();
let spend_params = utils::java_string_to_rust(&env, spend_params); let spend_params = utils::java_string_to_rust(&env, spend_params);
let output_params = utils::java_string_to_rust(&env, output_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 memo = Memo::from_bytes(&memo_bytes).unwrap();
let prover = LocalTxProver::new(Path::new(&spend_params), Path::new(&output_params)); 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, &mut db_data,
&network, &network,
prover, prover,
&sk, usk.transparent(),
AccountId::from(account), AccountId::from(account),
&MemoBytes::from(&memo), &MemoBytes::from(&memo),
0, 0,