Use the binary USK format for transaction creation
Co-authored-by: Carter Jernigan <git@carterjernigan.com>
This commit is contained in:
parent
3b826f8f6a
commit
88bbd0afcb
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -13,6 +13,8 @@ Change Log
|
|||
- `FirstClassByteArray`
|
||||
- `UnifiedSpendingKey`
|
||||
- `cash.z.ecc.android.sdk.tool`:
|
||||
- `DerivationTool.deriveUnifiedSpendingKey`
|
||||
- `DerivationTool.deriveUnifiedFullViewingKey`
|
||||
- `DerivationTool.deriveTransparentAccountPrivateKey`
|
||||
- `DerivationTool.deriveTransparentAddressFromAccountPrivateKey`
|
||||
- `DerivationTool.deriveUnifiedAddress`
|
||||
|
@ -36,8 +38,11 @@ Change Log
|
|||
- `Synchronizer.Companion.new` now takes a `seed` argument. A non-null value should be
|
||||
provided if `Synchronizer.Companion.new` throws an error that a database migration
|
||||
requires the wallet seed.
|
||||
- `Synchronizer.shieldFunds` now takes a transparent account private key (representing
|
||||
all transparent secret keys within an account) instead of a transparent secret key.
|
||||
- `Synchronizer.sendToAddress` now takes a `UnifiedSpendingKey` instead of an encoded
|
||||
Sapling extended spending key, and the `fromAccountIndex` argument is now implicit in
|
||||
the `UnifiedSpendingKey`.
|
||||
- `Synchronizer.shieldFunds` now takes a `UnifiedSpendingKey` instead of separately
|
||||
encoded Sapling and transparent keys.
|
||||
|
||||
### Removed
|
||||
- `cash.z.ecc.android.sdk`:
|
||||
|
@ -49,6 +54,10 @@ Change Log
|
|||
public key, and not the extended public key as intended. This made it incompatible
|
||||
with ZIP 316.
|
||||
- `cash.z.ecc.android.sdk.tool`:
|
||||
- `DerivationTool.deriveSpendingKeys` (use
|
||||
`DerivationTool.deriveUnifiedSpendingKey` instead).
|
||||
- `DerivationTool.deriveViewingKey` (use
|
||||
- `DerivationTool.deriveUnifiedFullViewingKey` instead).
|
||||
- `DerivationTool.deriveTransparentAddressFromPrivateKey` (use
|
||||
`DerivationTool.deriveTransparentAddressFromAccountPrivateKey` instead).
|
||||
- `DerivationTool.deriveTransparentSecretKey` (use
|
||||
|
|
|
@ -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())
|
||||
)
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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!")
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue