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`
- `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

View File

@ -43,14 +43,14 @@ class GetPrivateKeyFragment : BaseDemoFragment<FragmentGetPrivateKeyBinding>() {
// 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())
)

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.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<FragmentSendBinding>() {
// 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<FragmentSendBinding>() {
birthday = null
)
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)
override fun sendToAddress(
spendingKey: String,
usk: UnifiedSpendingKey,
amount: Zatoshi,
toAddress: String,
memo: String,
fromAccountIndex: Int
): Flow<PendingTransaction> = 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<PendingTransaction> = 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")

View File

@ -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<PendingTransaction>
fun shieldFunds(
spendingKey: String,
transparentAccountPrivateKey: String,
usk: UnifiedSpendingKey,
memo: String = ZcashSdk.DEFAULT_SHIELD_FUNDS_MEMO_PREFIX
): 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.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!")

View File

@ -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

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.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

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.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) {

View File

@ -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,

View File

@ -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<String>
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,

View File

@ -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<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.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<String> = 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<String>
): UnifiedSpendingKey
@JvmStatic
private external fun deriveUnifiedFullViewingKeysFromSeed(
@ -160,7 +166,7 @@ class DerivationTool {
): Array<String>
@JvmStatic
private external fun deriveExtendedFullViewingKey(spendingKey: String, networkId: Int): String
private external fun deriveUnifiedFullViewingKey(usk: ByteArray, networkId: Int): String
@JvmStatic
private external fun deriveUnifiedAddressFromSeed(

View File

@ -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.
*

View File

@ -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<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
/// [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,