[#525] Replace UnifiedViewingKey with UnifiedFullViewingKey

UnifiedViewingKey had a bug that made it incompatible with ZIP 316.

For compatibility with the current `zcash/librustzcash` revision we use
a temporary fake UFVK encoding that stores sufficient information to use
the current APIs, and a superset of the actual ZIP 316 FVK information.
This commit is contained in:
Carter Jernigan 2022-08-04 13:09:19 -04:00 committed by Jack Grigg
parent e01a906407
commit f7c9bad367
22 changed files with 321 additions and 209 deletions

View File

@ -1,7 +1,41 @@
Change Log
==========
Upcoming
## Unreleased
### Added
- `cash.z.ecc.android.sdk.type.UnifiedFullViewingKey`, representing a Unified Full Viewing
Key as specified in [ZIP 316](https://zips.z.cash/zip-0316#encoding-of-unified-full-incoming-viewing-keys).
- TODO: Actually encode per ZIP 316.
- `cash.z.ecc.android.sdk.tool`:
- `DerivationTool.deriveUnifiedAddress`
- `DerivationTool.deriveUnifiedFullViewingKeys`
- `DerivationTool.validateUnifiedFullViewingKey`
- Still unimplemented.
### Changed
- The following methods now take or return `UnifiedFullViewingKey` instead of
`UnifiedViewingKey`:
- `cash.z.ecc.android.sdk`:
- `Initializer.Config.addViewingKey`
- `Initializer.Config.importWallet`
- `Initializer.Config.newWallet`
- `Initializer.Config.setViewingKeys`
### Removed
- `cash.z.ecc.android.sdk.type.UnifiedViewingKey`
- This type had a bug where the `extpub` field actually was storing a plain transparent
public key, and not the extended public key as intended. This made it incompatible
with ZIP 316.
- `cash.z.ecc.android.sdk.tool`:
- `DerivationTool.deriveShieldedAddress`
- TODO: Do we still need to be able to derive Sapling shielded addresses for legacy
support? Currently removed because `UnifiedFullViewingKey` doesn't expose the
Sapling FVK on the Kotlin side (unlike the previous `UnifiedViewingKey`).
- `DerivationTool.deriveUnifiedViewingKeys`
- `DerivationTool.validateUnifiedViewingKey`
1.9.0-beta01
------------------------------------
- Split `ZcashNetwork` into `ZcashNetwork` and `LightWalletEndpoint` to decouple network and server configuration

View File

@ -74,8 +74,8 @@ class TestWallet(
val service = (synchronizer.processor.downloader.lightWalletService as LightWalletGrpcService)
val available get() = synchronizer.saplingBalances.value?.available
val shieldedAddress =
runBlocking { DerivationTool.deriveShieldedAddress(seed, network = network) }
val unifiedAddress =
runBlocking { DerivationTool.deriveUnifiedAddress(seed, network = network) }
val transparentAddress =
runBlocking { DerivationTool.deriveTransparentAddress(seed, network = network) }
val birthdayHeight get() = synchronizer.latestBirthdayHeight

View File

@ -11,7 +11,7 @@ import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
import cash.z.ecc.android.sdk.demoapp.util.fromResources
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@ -21,7 +21,7 @@ import kotlinx.coroutines.runBlocking
*/
class GetAddressFragment : BaseDemoFragment<FragmentGetAddressBinding>() {
private lateinit var viewingKey: UnifiedViewingKey
private lateinit var viewingKey: UnifiedFullViewingKey
private lateinit var seed: ByteArray
/**
@ -37,15 +37,22 @@ class GetAddressFragment : BaseDemoFragment<FragmentGetAddressBinding>() {
seed = Mnemonics.MnemonicCode(seedPhrase).toSeed()
// the derivation tool can be used for generating keys and addresses
viewingKey = runBlocking { DerivationTool.deriveUnifiedViewingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first() }
viewingKey = runBlocking {
DerivationTool.deriveUnifiedFullViewingKeys(
seed,
ZcashNetwork.fromResources(requireApplicationContext())
).first()
}
}
private fun displayAddress() {
// a full fledged app would just get the address from the synchronizer
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
val zaddress = DerivationTool.deriveShieldedAddress(seed, ZcashNetwork.fromResources(requireApplicationContext()))
val taddress = DerivationTool.deriveTransparentAddress(seed, ZcashNetwork.fromResources(requireApplicationContext()))
binding.textInfo.text = "z-addr:\n$zaddress\n\n\nt-addr:\n$taddress"
val uaddress = DerivationTool.deriveUnifiedAddress(
seed,
ZcashNetwork.fromResources(requireApplicationContext())
)
binding.textInfo.text = "address:\n$uaddress"
}
}
@ -72,11 +79,11 @@ class GetAddressFragment : BaseDemoFragment<FragmentGetAddressBinding>() {
override fun onActionButtonClicked() {
viewLifecycleOwner.lifecycleScope.launch {
copyToClipboard(
DerivationTool.deriveShieldedAddress(
viewingKey.extfvk,
DerivationTool.deriveUnifiedAddress(
viewingKey.encoding,
ZcashNetwork.fromResources(requireApplicationContext())
),
"Shielded address copied to clipboard!"
"Unified address copied to clipboard!"
)
}
}

View File

@ -47,7 +47,12 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
val seed = Mnemonics.MnemonicCode(seedPhrase).toSeed()
// converting seed into viewingKey
val viewingKey = runBlocking { DerivationTool.deriveUnifiedViewingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first() }
val viewingKey = runBlocking {
DerivationTool.deriveUnifiedFullViewingKeys(
seed,
ZcashNetwork.fromResources(requireApplicationContext())
).first()
}
// using the ViewingKey to initialize
runBlocking {

View File

@ -78,11 +78,11 @@ class GetPrivateKeyFragment : BaseDemoFragment<FragmentGetPrivateKeyBinding>() {
override fun onActionButtonClicked() {
lifecycleScope.launch {
copyToClipboard(
DerivationTool.deriveUnifiedViewingKeys(
DerivationTool.deriveUnifiedFullViewingKeys(
seed,
ZcashNetwork.fromResources(requireApplicationContext())
).first().extpub,
"ViewingKey copied to clipboard!"
).first().encoding,
"UnifiedFullViewingKey copied to clipboard!"
)
}
}

View File

@ -65,7 +65,7 @@ class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBindin
}
)
address = runBlocking {
DerivationTool.deriveShieldedAddress(
DerivationTool.deriveUnifiedAddress(
seed,
ZcashNetwork.fromResources(requireApplicationContext())
)

1
sdk-lib/Cargo.lock generated
View File

@ -1538,6 +1538,7 @@ version = "0.0.4"
dependencies = [
"android_logger",
"failure",
"hdwallet",
"hex",
"jni",
"log",

View File

@ -12,6 +12,7 @@ edition = "2018"
[dependencies]
android_logger = "0.9"
failure = "0.1"
hdwallet = "=0.3.0"
hex = "0.4"
jni = { version = "0.17", default-features = false }
log = "0.4"

View File

@ -21,8 +21,7 @@ import org.junit.runners.Parameterized
@RunWith(Parameterized::class)
class SanityTest(
private val wallet: TestWallet,
private val extfvk: String,
private val extpub: String,
private val encoding: String,
private val birthday: Int
) {
@ -61,14 +60,9 @@ class SanityTest(
@Test
fun testViewingKeys() {
assertEquals(
"$name has invalid extfvk",
extfvk,
wallet.initializer.viewingKeys[0].extfvk
)
assertEquals(
"$name has invalid extpub",
extpub,
wallet.initializer.viewingKeys[0].extpub
"$name has invalid encoding",
encoding,
wallet.initializer.viewingKeys[0].encoding
)
}
@ -101,14 +95,12 @@ class SanityTest(
arrayOf(
TestWallet(TestWallet.Backups.SAMPLE_WALLET),
"zxviewtestsapling1qv0ue89kqqqqpqqyt4cl5wvssx4wqq30e5m948p07dnwl9x3u75vvnzvjwwpjkrf8yk2gva0kkxk9p8suj4xawlzw9pajuxgap83wykvsuyzfrm33a2p2m4jz2205kgzx0l2lj2kyegtnuph6crkyvyjqmfxut84nu00wxgrstu5fy3eu49nzl8jzr4chmql4ysgg2t8htn9dtvxy8c7wx9rvcerqsjqm6lqln9syk3g8rr3xpy3l4nj0kawenzpcdtnv9qmy98vdhqzaf063",
"0234965f30c8611253d035f44e68d4e2ce82150e8665c95f41ccbaf916b16c69d8",
1320000
),
// Mainnet wallet
arrayOf(
TestWallet(TestWallet.Backups.SAMPLE_WALLET, ZcashNetwork.Mainnet),
"zxviews1q0hxkupsqqqqpqzsffgrk2smjuccedua7zswf5e3rgtv3ga9nhvhjug670egshd6me53r5n083s2m9mf4va4z7t39ltd3wr7hawnjcw09eu85q0ammsg0tsgx24p4ma0uvr4p8ltx5laum2slh2whc23ctwlnxme9w4dw92kalwk5u4wyem8dynknvvqvs68ktvm8qh7nx9zg22xfc77acv8hk3qqll9k3x4v2fa26puu2939ea7hy4hh60ywma69xtqhcy4037ne8g2sg8sq",
"031c6355641237643317e2d338f5e8734c57e8aa8ce960ee22283cf2d76bef73be",
1000000
)
)

View File

@ -32,8 +32,7 @@ class SmokeTest {
@Test
fun testViewingKeys() {
Assert.assertEquals("Invalid extfvk", "zxviewtestsapling1qv0ue89kqqqqpqqyt4cl5wvssx4wqq30e5m948p07dnwl9x3u75vvnzvjwwpjkrf8yk2gva0kkxk9p8suj4xawlzw9pajuxgap83wykvsuyzfrm33a2p2m4jz2205kgzx0l2lj2kyegtnuph6crkyvyjqmfxut84nu00wxgrstu5fy3eu49nzl8jzr4chmql4ysgg2t8htn9dtvxy8c7wx9rvcerqsjqm6lqln9syk3g8rr3xpy3l4nj0kawenzpcdtnv9qmy98vdhqzaf063", wallet.initializer.viewingKeys[0].extfvk)
Assert.assertEquals("Invalid extpub", "0234965f30c8611253d035f44e68d4e2ce82150e8665c95f41ccbaf916b16c69d8", wallet.initializer.viewingKeys[0].extpub)
Assert.assertEquals("Invalid encoding", "zxviewtestsapling1qv0ue89kqqqqpqqyt4cl5wvssx4wqq30e5m948p07dnwl9x3u75vvnzvjwwpjkrf8yk2gva0kkxk9p8suj4xawlzw9pajuxgap83wykvsuyzfrm33a2p2m4jz2205kgzx0l2lj2kyegtnuph6crkyvyjqmfxut84nu00wxgrstu5fy3eu49nzl8jzr4chmql4ysgg2t8htn9dtvxy8c7wx9rvcerqsjqm6lqln9syk3g8rr3xpy3l4nj0kawenzpcdtnv9qmy98vdhqzaf063", wallet.initializer.viewingKeys[0].encoding)
}
// This test takes an extremely long time

View File

@ -38,12 +38,12 @@ class TransparentTest(val expected: Expected, val network: ZcashNetwork) {
}
@Test
fun deriveUnifiedViewingKeysFromSeedTest() = runBlocking {
val uvks = DerivationTool.deriveUnifiedViewingKeys(SEED, network = network)
fun deriveUnifiedFullViewingKeysFromSeedTest() = runBlocking {
val uvks = DerivationTool.deriveUnifiedFullViewingKeys(SEED, network = network)
assertEquals(1, uvks.size)
val uvk = uvks.first()
assertEquals(expected.zAddr, DerivationTool.deriveShieldedAddress(uvk.extfvk, network = network))
assertEquals(expected.tAddr, DerivationTool.deriveTransparentAddressFromPublicKey(uvk.extpub, network = network))
assertEquals(expected.zAddr, DerivationTool.deriveUnifiedAddress(uvk.encoding, network = network))
assertEquals(expected.tAddr, DerivationTool.deriveTransparentAddressFromPublicKey(uvk.encoding, network = network))
}
companion object {

View File

@ -30,7 +30,7 @@ class ShieldFundsSample {
val wallet = TestWallet(TestWallet.Backups.DEV_WALLET, ZcashNetwork.Mainnet)
Assert.assertEquals("foo", "${wallet.shieldedAddress} ${wallet.transparentAddress}")
Assert.assertEquals("foo", "${wallet.unifiedAddress} ${wallet.transparentAddress}")
// wallet.shieldFunds()
Twig.clip("ShieldFundsSample")

View File

@ -36,7 +36,7 @@ class AddressGeneratorUtil {
.map { seedPhrase ->
mnemonics.toSeed(seedPhrase.toCharArray())
}.map { seed ->
DerivationTool.deriveShieldedAddress(seed, ZcashNetwork.Mainnet)
DerivationTool.deriveUnifiedAddress(seed, ZcashNetwork.Mainnet)
}.collect { address ->
println("xrxrx2\t$address")
assertTrue(address.startsWith("zs1"))

View File

@ -74,8 +74,8 @@ class TestWallet(
val service = (synchronizer.processor.downloader.lightWalletService as LightWalletGrpcService)
val available get() = synchronizer.saplingBalances.value?.available
val shieldedAddress =
runBlocking { DerivationTool.deriveShieldedAddress(seed, network = network) }
val unifiedAddress =
runBlocking { DerivationTool.deriveUnifiedAddress(seed, network = network) }
val transparentAddress =
runBlocking { DerivationTool.deriveTransparentAddress(seed, network = network) }
val birthdayHeight get() = synchronizer.latestBirthdayHeight

View File

@ -14,7 +14,7 @@ import cash.z.ecc.android.sdk.model.LightWalletEndpoint
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.tool.CheckpointTool
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.File
@ -29,7 +29,7 @@ class Initializer private constructor(
val network: ZcashNetwork,
val alias: String,
val lightWalletEndpoint: LightWalletEndpoint,
val viewingKeys: List<UnifiedViewingKey>,
val viewingKeys: List<UnifiedFullViewingKey>,
val overwriteVks: Boolean,
internal val checkpoint: Checkpoint
) {
@ -37,7 +37,7 @@ class Initializer private constructor(
suspend fun erase() = erase(context, network, alias)
class Config private constructor(
val viewingKeys: MutableList<UnifiedViewingKey> = mutableListOf(),
val viewingKeys: MutableList<UnifiedFullViewingKey> = mutableListOf(),
var alias: String = ZcashSdk.DEFAULT_ALIAS
) {
var birthdayHeight: BlockHeight? = null
@ -119,13 +119,13 @@ class Initializer private constructor(
* probably has serious bugs.
*/
fun setViewingKeys(
vararg unifiedViewingKeys: UnifiedViewingKey,
vararg unifiedFullViewingKeys: UnifiedFullViewingKey,
overwrite: Boolean = false
): Config = apply {
overwriteVks = overwrite
viewingKeys.apply {
clear()
addAll(unifiedViewingKeys)
addAll(unifiedFullViewingKeys)
}
}
@ -138,7 +138,7 @@ class Initializer private constructor(
* is not currently well supported. Consider it an alpha-preview feature that might work but
* probably has serious bugs.
*/
fun addViewingKey(unifiedFullViewingKey: UnifiedViewingKey): Config = apply {
fun addViewingKey(unifiedFullViewingKey: UnifiedFullViewingKey): Config = apply {
viewingKeys.add(unifiedFullViewingKey)
}
@ -175,7 +175,7 @@ class Initializer private constructor(
alias: String = ZcashSdk.DEFAULT_ALIAS
): Config =
importWallet(
DerivationTool.deriveUnifiedViewingKeys(seed, network = network)[0],
DerivationTool.deriveUnifiedFullViewingKeys(seed, network = network)[0],
birthday,
network,
lightWalletEndpoint,
@ -185,8 +185,9 @@ class Initializer private constructor(
/**
* Default function for importing a wallet.
*/
@Suppress("LongParameterList")
fun importWallet(
viewingKey: UnifiedViewingKey,
viewingKey: UnifiedFullViewingKey,
birthday: BlockHeight?,
network: ZcashNetwork,
lightWalletEndpoint: LightWalletEndpoint,
@ -207,7 +208,7 @@ class Initializer private constructor(
lightWalletEndpoint: LightWalletEndpoint,
alias: String = ZcashSdk.DEFAULT_ALIAS
): Config = newWallet(
DerivationTool.deriveUnifiedViewingKeys(seed, network)[0],
DerivationTool.deriveUnifiedFullViewingKeys(seed, network)[0],
network,
lightWalletEndpoint,
alias
@ -217,7 +218,7 @@ class Initializer private constructor(
* Default function for creating a new wallet.
*/
fun newWallet(
viewingKey: UnifiedViewingKey,
viewingKey: UnifiedFullViewingKey,
network: ZcashNetwork,
lightWalletEndpoint: LightWalletEndpoint,
alias: String = ZcashSdk.DEFAULT_ALIAS
@ -232,6 +233,7 @@ class Initializer private constructor(
* Convenience method for setting thew viewingKeys from a given seed. This is the same as
* calling `setViewingKeys` with the keys that match this seed.
*/
@Suppress("SpreadOperator")
suspend fun setSeed(
seed: ByteArray,
network: ZcashNetwork,
@ -239,7 +241,7 @@ class Initializer private constructor(
): Config =
apply {
setViewingKeys(
*DerivationTool.deriveUnifiedViewingKeys(
*DerivationTool.deriveUnifiedFullViewingKeys(
seed,
network,
numberOfAccounts
@ -288,7 +290,7 @@ class Initializer private constructor(
" have been set on this Initializer."
}
viewingKeys.forEach {
DerivationTool.validateUnifiedViewingKey(it)
DerivationTool.validateUnifiedFullViewingKey(it)
}
}

View File

@ -17,7 +17,7 @@ import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.jni.RustBackend
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext
@ -111,13 +111,14 @@ internal class PagedTransactionRepository private constructor(
// TODO: convert this into a wallet repository rather than "transaction repository"
companion object {
internal suspend fun new(
@Suppress("LongParameterList")
suspend fun new(
appContext: Context,
zcashNetwork: ZcashNetwork,
pageSize: Int = 10,
rustBackend: RustBackend,
birthday: Checkpoint,
viewingKeys: List<UnifiedViewingKey>,
viewingKeys: List<UnifiedFullViewingKey>,
overwriteVks: Boolean = false
): PagedTransactionRepository {
initMissingDatabases(rustBackend, birthday, viewingKeys)
@ -159,7 +160,7 @@ internal class PagedTransactionRepository private constructor(
private suspend fun initMissingDatabases(
rustBackend: RustBackend,
birthday: Checkpoint,
viewingKeys: List<UnifiedViewingKey>
viewingKeys: List<UnifiedFullViewingKey>
) {
maybeCreateDataDb(rustBackend)
maybeInitBlocksTable(rustBackend, birthday)
@ -199,7 +200,7 @@ internal class PagedTransactionRepository private constructor(
*/
private suspend fun maybeInitAccountsTable(
rustBackend: RustBackend,
viewingKeys: List<UnifiedViewingKey>
viewingKeys: List<UnifiedFullViewingKey>
) {
// TODO: consider converting these to typed exceptions in the welding layer
tryWarn(
@ -214,7 +215,7 @@ internal class PagedTransactionRepository private constructor(
private suspend fun applyKeyMigrations(
rustBackend: RustBackend,
overwriteVks: Boolean,
viewingKeys: List<UnifiedViewingKey>
viewingKeys: List<UnifiedFullViewingKey>
) {
if (overwriteVks) {
twig("applying key migrations . . .")

View File

@ -11,7 +11,7 @@ import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey
import kotlinx.coroutines.withContext
import java.io.File
@ -50,18 +50,13 @@ internal class RustBackend private constructor(
)
}
override suspend fun initAccountsTable(vararg keys: UnifiedViewingKey): Boolean {
val extfvks = Array(keys.size) { "" }
val extpubs = Array(keys.size) { "" }
keys.forEachIndexed { i, key ->
extfvks[i] = key.extfvk
extpubs[i] = key.extpub
}
override suspend fun initAccountsTable(vararg keys: UnifiedFullViewingKey): Boolean {
val ufvks = Array(keys.size) { keys[it].encoding }
return withContext(SdkDispatchers.DATABASE_IO) {
initAccountsTableWithKeys(
pathDataDb,
extfvks,
extpubs,
ufvks,
networkId = network.id
)
}
@ -70,8 +65,8 @@ internal class RustBackend private constructor(
override suspend fun initAccountsTable(
seed: ByteArray,
numberOfAccounts: Int
): Array<UnifiedViewingKey> {
return DerivationTool.deriveUnifiedViewingKeys(seed, network, numberOfAccounts).apply {
): Array<UnifiedFullViewingKey> {
return DerivationTool.deriveUnifiedFullViewingKeys(seed, network, numberOfAccounts).apply {
initAccountsTable(*this)
}
}
@ -387,8 +382,7 @@ internal class RustBackend private constructor(
@JvmStatic
private external fun initAccountsTableWithKeys(
dbDataPath: String,
extfvk: Array<out String>,
extpub: Array<out String>,
ufvks: Array<out String>,
networkId: Int
): Boolean

View File

@ -5,7 +5,7 @@ import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey
/**
* Contract defining the exposed capabilities of the Rust backend.
@ -34,9 +34,9 @@ internal interface RustBackendWelding {
suspend fun decryptAndStoreTransaction(tx: ByteArray)
suspend fun initAccountsTable(seed: ByteArray, numberOfAccounts: Int): Array<UnifiedViewingKey>
suspend fun initAccountsTable(seed: ByteArray, numberOfAccounts: Int): Array<UnifiedFullViewingKey>
suspend fun initAccountsTable(vararg keys: UnifiedViewingKey): Boolean
suspend fun initAccountsTable(vararg keys: UnifiedFullViewingKey): Boolean
suspend fun initBlocksTable(checkpoint: Checkpoint): Boolean
@ -88,12 +88,12 @@ internal interface RustBackendWelding {
// Implemented by `DerivationTool`
interface Derivation {
suspend fun deriveShieldedAddress(
suspend fun deriveUnifiedAddress(
viewingKey: String,
network: ZcashNetwork
): String
suspend fun deriveShieldedAddress(
suspend fun deriveUnifiedAddress(
seed: ByteArray,
network: ZcashNetwork,
accountIndex: Int = 0
@ -134,10 +134,10 @@ internal interface RustBackendWelding {
network: ZcashNetwork
): String
suspend fun deriveUnifiedViewingKeys(
suspend fun deriveUnifiedFullViewingKeys(
seed: ByteArray,
network: ZcashNetwork,
numberOfAccounts: Int = 1
): Array<UnifiedViewingKey>
): Array<UnifiedFullViewingKey>
}
}

View File

@ -3,25 +3,25 @@ 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.ZcashNetwork
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey
class DerivationTool {
companion object : RustBackendWelding.Derivation {
/**
* Given a seed and a number of accounts, return the associated viewing keys.
* Given a seed and a number of accounts, return the associated Unified Full Viewing Keys.
*
* @param seed the seed from which to derive viewing keys.
* @param numberOfAccounts the number of accounts to use. Multiple accounts are not fully
* supported so the default value of 1 is recommended.
*
* @return the viewing keys that correspond to the seed, formatted as Strings.
* @return the UFVKs derived from the seed, encoded as Strings.
*/
override suspend fun deriveUnifiedViewingKeys(seed: ByteArray, network: ZcashNetwork, numberOfAccounts: Int): Array<UnifiedViewingKey> =
override suspend fun deriveUnifiedFullViewingKeys(seed: ByteArray, network: ZcashNetwork, numberOfAccounts: Int): Array<UnifiedFullViewingKey> =
withRustBackendLoaded {
deriveUnifiedViewingKeysFromSeed(seed, numberOfAccounts, networkId = network.id).map {
UnifiedViewingKey(it[0], it[1])
deriveUnifiedFullViewingKeysFromSeed(seed, numberOfAccounts, networkId = network.id).map {
UnifiedFullViewingKey(it)
}.toTypedArray()
}
@ -51,7 +51,7 @@ class DerivationTool {
}
/**
* Given a seed and account index, return the associated address.
* Given a seed and account index, return the associated Unified Address.
*
* @param seed the seed from which to derive the address.
* @param accountIndex the index of the account to use for deriving the address. Multiple
@ -59,21 +59,21 @@ class DerivationTool {
*
* @return the address that corresponds to the seed and account index.
*/
override suspend fun deriveShieldedAddress(seed: ByteArray, network: ZcashNetwork, accountIndex: Int): String =
override suspend fun deriveUnifiedAddress(seed: ByteArray, network: ZcashNetwork, accountIndex: Int): String =
withRustBackendLoaded {
deriveShieldedAddressFromSeed(seed, accountIndex, networkId = network.id)
deriveUnifiedAddressFromSeed(seed, accountIndex, networkId = network.id)
}
/**
* Given a viewing key string, return the associated address.
* Given a Unified Full Viewing Key string, return the associated Unified Address.
*
* @param viewingKey the viewing key to use for deriving the address. The viewing key is tied to
* a specific account so no account index is required.
*
* @return the address that corresponds to the viewing key.
*/
override suspend fun deriveShieldedAddress(viewingKey: String, network: ZcashNetwork): String = withRustBackendLoaded {
deriveShieldedAddressFromViewingKey(viewingKey, networkId = network.id)
override suspend fun deriveUnifiedAddress(viewingKey: String, network: ZcashNetwork): String = withRustBackendLoaded {
deriveUnifiedAddressFromViewingKey(viewingKey, networkId = network.id)
}
// WIP probably shouldn't be used just yet. Why?
@ -95,7 +95,8 @@ class DerivationTool {
deriveTransparentSecretKeyFromSeed(seed, account, index, networkId = network.id)
}
fun validateUnifiedViewingKey(viewingKey: UnifiedViewingKey, networkId: Int = ZcashNetwork.Mainnet.id) {
@Suppress("UnusedPrivateMember")
fun validateUnifiedFullViewingKey(viewingKey: UnifiedFullViewingKey, networkId: Int = ZcashNetwork.Mainnet.id) {
// TODO
}
@ -121,24 +122,24 @@ class DerivationTool {
): Array<String>
@JvmStatic
private external fun deriveUnifiedViewingKeysFromSeed(
private external fun deriveUnifiedFullViewingKeysFromSeed(
seed: ByteArray,
numberOfAccounts: Int,
networkId: Int
): Array<Array<String>>
): Array<String>
@JvmStatic
private external fun deriveExtendedFullViewingKey(spendingKey: String, networkId: Int): String
@JvmStatic
private external fun deriveShieldedAddressFromSeed(
private external fun deriveUnifiedAddressFromSeed(
seed: ByteArray,
accountIndex: Int,
networkId: Int
): String
@JvmStatic
private external fun deriveShieldedAddressFromViewingKey(key: String, networkId: Int): String
private external fun deriveUnifiedAddressFromViewingKey(key: String, networkId: Int): String
@JvmStatic
private external fun deriveTransparentAddressFromSeed(seed: ByteArray, account: Int, index: Int, networkId: Int): String

View File

@ -1,16 +1,16 @@
package cash.z.ecc.android.sdk.type
/**
* A grouping of keys that correspond to a single wallet account but do not have spend authority.
* A [ZIP 316] Unified Full Viewing Key, corresponding to a single wallet account.
*
* @param extfvk the extended full viewing key which provides the ability to see inbound and
* outbound shielded transactions. It can also be used to derive a z-addr.
* @param extpub the extended public key which provides the ability to see transparent
* transactions. It can also be used to derive a t-addr.
* A `UnifiedFullViewingKey` has the authority to view transactions for an account, but
* does not have spend authority. It can be used to derive a [UnifiedAddress] for the
* account.
*
* @param[encoding] The string encoding of the UFVK.
*/
data class UnifiedViewingKey(
val extfvk: String = "",
val extpub: String = ""
data class UnifiedFullViewingKey(
val encoding: String = ""
)
data class UnifiedAddressAccount(

View File

@ -26,14 +26,12 @@ use zcash_client_backend::{
WalletRead,
},
encoding::{
decode_extended_full_viewing_key, decode_extended_spending_key,
encode_extended_full_viewing_key, encode_extended_spending_key, encode_payment_address,
AddressCodec,
decode_extended_spending_key, encode_extended_full_viewing_key,
encode_extended_spending_key, encode_payment_address, AddressCodec,
},
keys::{
derive_public_key_from_seed, derive_secret_key_from_seed,
derive_transparent_address_from_public_key, derive_transparent_address_from_secret_key,
spending_key, Wif,
derive_secret_key_from_seed, derive_transparent_address_from_public_key,
derive_transparent_address_from_secret_key, spending_key, Wif,
},
wallet::{AccountId, OvkPolicy, WalletTransparentOutput},
};
@ -127,42 +125,39 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initAccount
env: JNIEnv<'_>,
_: JClass<'_>,
db_data: JString<'_>,
extfvks_arr: jobjectArray,
extpubs_arr: jobjectArray,
ufvks_arr: jobjectArray,
network_id: jint,
) -> jboolean {
let res = panic::catch_unwind(|| {
let network = parse_network(network_id as u32)?;
let db_data = wallet_db(&env, network, db_data)?;
// TODO: avoid all this unwrapping and also surface errors, better
let count = env.get_array_length(extfvks_arr).unwrap();
let extfvks = (0..count)
.map(|i| env.get_object_array_element(extfvks_arr, i))
let count = env.get_array_length(ufvks_arr).unwrap();
let ufvks = (0..count)
.map(|i| env.get_object_array_element(ufvks_arr, i))
.map(|jstr| utils::java_string_to_rust(&env, jstr.unwrap().into()))
.map(|vkstr| {
decode_extended_full_viewing_key(
network.hrp_sapling_extended_full_viewing_key(),
&vkstr,
)
.map_err(|err| format_err!("Invalid bech32: {}", err))
.and_then(|extfvk|
extfvk.ok_or_else(|| {
.map(|ufvkstr| {
// TODO: replace with `zcash_address::unified::Ufvk`
utils::fake_ufvk_decode(&ufvkstr).ok_or_else(|| {
let (network_name, other) = if network == TestNetwork {
("testnet", "mainnet")
} else {
("mainnet", "testnet")
};
format_err!("Error: Wrong network! Unable to decode viewing key for {}. Check whether this is a key for {}.", network_name, other)
}))
})
})
.collect::<Result<Vec<_>, _>>()?;
let taddrs: Vec<_> = (0..count)
.map(|i| env.get_object_array_element(extpubs_arr, i))
.map(|jstr| utils::java_string_to_rust(&env, jstr.unwrap().into()))
.map(|extpub_str| PublicKey::from_str(&extpub_str).unwrap())
.map(|pk| derive_transparent_address_from_public_key(&pk))
.collect::<Vec<_>>();
let (taddrs, extfvks): (Vec<_>, Vec<_>) = ufvks
.into_iter()
.map(|(extpub, extfvk)| {
(
derive_transparent_address_from_public_key(&extpub.public_key),
extfvk,
)
})
.unzip();
match init_accounts_table(&db_data, &extfvks[..], &taddrs[..]) {
Ok(()) => Ok(JNI_TRUE),
@ -210,7 +205,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveE
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveUnifiedViewingKeysFromSeed(
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveUnifiedFullViewingKeysFromSeed(
env: JNIEnv<'_>,
_: JClass<'_>,
seed: jbyteArray,
@ -226,32 +221,25 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveU
return Err(format_err!("accounts argument must be greater than zero"));
};
let extfvks: Vec<_> = (0..accounts)
let ufvks: Vec<_> = (0..accounts)
.map(|account| {
encode_extended_full_viewing_key(
network.hrp_sapling_extended_full_viewing_key(),
&ExtendedFullViewingKey::from(&spending_key(
&seed,
network.coin_type(),
AccountId(account),
)),
)
let sapling = ExtendedFullViewingKey::from(&spending_key(
&seed,
network.coin_type(),
AccountId(account),
));
let p2pkh =
utils::p2pkh_full_viewing_key(&network, &seed, AccountId(account)).unwrap();
// TODO: Replace with `zcash_address::unified::Ufvk`
utils::fake_ufvk_encode(&p2pkh, &sapling)
})
.collect();
let extpubs: Vec<_> = (0..accounts)
.map(|account| {
let pk =
derive_public_key_from_seed(&network, &seed, AccountId(account), 0).unwrap();
hex::encode(&pk.serialize())
})
.collect();
Ok(utils::rust_vec_to_java_2d(
Ok(utils::rust_vec_to_java(
&env,
extfvks,
extpubs,
|env, extfvkstr| env.new_string(extfvkstr),
ufvks,
"java/lang/String",
|env, ufvk| env.new_string(ufvk),
|env| env.new_string(""),
))
});
@ -259,7 +247,7 @@ 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_deriveShieldedAddressFromSeed(
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveUnifiedAddressFromSeed(
env: JNIEnv<'_>,
_: JClass<'_>,
seed: jbyteArray,
@ -275,11 +263,16 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveS
return Err(format_err!("accountIndex argument must be positive"));
};
let address = spending_key(&seed, network.coin_type(), AccountId(account_index))
let (di, sapling) = spending_key(&seed, network.coin_type(), AccountId(account_index))
.default_address()
.unwrap()
.1;
let address_str = encode_payment_address(network.hrp_sapling_payment_address(), &address);
.unwrap();
let p2pkh = utils::p2pkh_addr(
utils::p2pkh_full_viewing_key(&network, &seed, AccountId(account_index)).unwrap(),
di,
)
.unwrap();
// TODO: replace this with `zcash_address::unified::Address`.
let address_str = utils::fake_ua_encode(&p2pkh, &sapling);
let output = env
.new_string(address_str)
.expect("Couldn't create Java string!");
@ -289,33 +282,31 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveS
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveShieldedAddressFromViewingKey(
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveUnifiedAddressFromViewingKey(
env: JNIEnv<'_>,
_: JClass<'_>,
extfvk_string: JString<'_>,
network_id: jint,
ufvk_string: JString<'_>,
_network_id: jint,
) -> jstring {
let res = panic::catch_unwind(|| {
let network = parse_network(network_id as u32)?;
let extfvk_string = utils::java_string_to_rust(&env, extfvk_string);
let extfvk = match decode_extended_full_viewing_key(
network.hrp_sapling_extended_full_viewing_key(),
&extfvk_string,
) {
Ok(Some(extfvk)) => extfvk,
Ok(None) => {
return Err(format_err!("Failed to parse viewing key string in order to derive the address. Deriving a viewing key from the string returned no results. Encoding was valid but type was incorrect."));
}
Err(e) => {
//let network = parse_network(network_id as u32)?;
let ufvk_string = utils::java_string_to_rust(&env, ufvk_string);
let ufvk = match utils::fake_ufvk_decode(&ufvk_string) {
Some(ufvk) => ufvk,
None => {
return Err(format_err!(
"Error while deriving viewing key from string input: {}",
e
"Error while deriving viewing key from string input"
));
}
};
let address = extfvk.default_address().unwrap().1;
let address_str = encode_payment_address(network.hrp_sapling_payment_address(), &address);
// Derive the default Sapling payment address (like older SDKs used).
let (di, sapling) = ufvk.1.default_address().unwrap();
// Derive the transparent address corresponding to the default Sapling diversifier
// index (matching ZIP 316).
let p2pkh = utils::p2pkh_addr(ufvk.0, di).unwrap();
// TODO: replace this with `zcash_address::unified::Address`.
let address_str = utils::fake_ua_encode(&p2pkh, &sapling);
let output = env
.new_string(address_str)
.expect("Couldn't create Java string!");

View File

@ -1,3 +1,7 @@
use hdwallet::{
traits::{Deserialize, Serialize},
ExtendedPrivKey, ExtendedPubKey, KeyIndex,
};
use jni::{
descriptors::Desc,
errors::Result as JNIResult,
@ -5,7 +9,15 @@ use jni::{
sys::{jobjectArray, jsize},
JNIEnv,
};
use std::ops::Deref;
use zcash_client_backend::{keys::derive_transparent_address_from_public_key, wallet::AccountId};
use zcash_primitives::{
consensus,
legacy::TransparentAddress,
sapling::PaymentAddress,
zip32::{DiversifierIndex, ExtendedFullViewingKey},
};
use std::{convert::TryInto, ops::Deref};
pub(crate) mod exception;
@ -41,39 +53,111 @@ where
}
// 2D array
pub(crate) fn rust_vec_to_java_2d<'a, T, V, F, G>(
env: &JNIEnv<'a>,
data1: Vec<T>,
data2: Vec<T>,
element_map: F,
empty_element: G,
) -> jobjectArray
where
V: Deref<Target = JObject<'a>>,
F: Fn(&JNIEnv<'a>, T) -> JNIResult<V>,
G: Fn(&JNIEnv<'a>) -> JNIResult<V>,
{
let jempty = empty_element(env).expect("Couldn't create Java string!");
let outer = env
.new_object_array(
data1.len() as jsize,
"[Ljava/lang/String;",
*jni::objects::JObject::null(),
)
.expect("Couldn't create Java array of string arrays!");
//pub(crate) fn rust_vec_to_java_2d<'a, T, V, F, G>(
// env: &JNIEnv<'a>,
// data1: Vec<T>,
// data2: Vec<T>,
// element_map: F,
// empty_element: G,
//) -> jobjectArray
//where
// V: Deref<Target = JObject<'a>>,
// F: Fn(&JNIEnv<'a>, T) -> JNIResult<V>,
// G: Fn(&JNIEnv<'a>) -> JNIResult<V>,
//{
// let jempty = empty_element(env).expect("Couldn't create Java string!");
// let outer = env
// .new_object_array(
// data1.len() as jsize,
// "[Ljava/lang/String;",
// *jni::objects::JObject::null(),
// )
// .expect("Couldn't create Java array of string arrays!");
//
// for (i, (elem1, elem2)) in data1.into_iter().zip(data2.into_iter()).enumerate() {
// let inner = env
// .new_object_array(2 as jsize, "java/lang/String", *jempty)
// .expect("Couldn't create Java array!");
// let jelem1 = element_map(env, elem1).expect("Couldn't map element to Java!");
// let jelem2 = element_map(env, elem2).expect("Couldn't map element to Java!");
// env.set_object_array_element(inner, 0 as jsize, *jelem1)
// .expect("Couldn't set Java array element!");
// env.set_object_array_element(inner, 1 as jsize, *jelem2)
// .expect("Couldn't set Java array element!");
// env.set_object_array_element(outer, i as jsize, inner)
// .expect("Couldn't set Java array element!");
// }
// outer
//}
for (i, (elem1, elem2)) in data1.into_iter().zip(data2.into_iter()).enumerate() {
let inner = env
.new_object_array(2 as jsize, "java/lang/String", *jempty)
.expect("Couldn't create Java array!");
let jelem1 = element_map(env, elem1).expect("Couldn't map element to Java!");
let jelem2 = element_map(env, elem2).expect("Couldn't map element to Java!");
env.set_object_array_element(inner, 0 as jsize, *jelem1)
.expect("Couldn't set Java array element!");
env.set_object_array_element(inner, 1 as jsize, *jelem2)
.expect("Couldn't set Java array element!");
env.set_object_array_element(outer, i as jsize, inner)
.expect("Couldn't set Java array element!");
}
outer
pub(crate) fn p2pkh_full_viewing_key<P: consensus::Parameters>(
params: &P,
seed: &[u8],
account: AccountId,
) -> Result<ExtendedPubKey, hdwallet::error::Error> {
let pk = ExtendedPrivKey::with_seed(&seed)?;
let private_key = pk
.derive_private_key(KeyIndex::hardened_from_normalize_index(44)?)?
.derive_private_key(KeyIndex::hardened_from_normalize_index(params.coin_type())?)?
.derive_private_key(KeyIndex::hardened_from_normalize_index(account.0)?)?;
Ok(ExtendedPubKey::from_private_key(&private_key))
}
pub(crate) fn p2pkh_addr(
fvk: ExtendedPubKey,
index: DiversifierIndex,
) -> Result<TransparentAddress, hdwallet::error::Error> {
let pubkey = fvk
.derive_public_key(KeyIndex::Normal(0))?
.derive_public_key(KeyIndex::Normal(u32::from_le_bytes(
index.0[..4].try_into().unwrap(),
)))?
.public_key;
Ok(derive_transparent_address_from_public_key(&pubkey))
}
/// This is temporary, and will be replaced by `zcash_address::unified::Ufvk`.
pub(crate) fn fake_ufvk_encode(p2pkh: &ExtendedPubKey, sapling: &ExtendedFullViewingKey) -> String {
let mut ufvk = p2pkh.serialize();
sapling.write(&mut ufvk).unwrap();
format!("DONOTUSEUFVK{}", hex::encode(&ufvk))
}
/// This is temporary, and will be replaced by `zcash_address::unified::Ufvk`.
pub(crate) fn fake_ufvk_decode(encoding: &str) -> Option<(ExtendedPubKey, ExtendedFullViewingKey)> {
encoding
.strip_prefix("DONOTUSEUFVK")
.and_then(|data| hex::decode(data).ok())
.and_then(|data| {
ExtendedPubKey::deserialize(&data[..65])
.ok()
.zip(ExtendedFullViewingKey::read(&data[65..]).ok())
})
}
/// This is temporary, and will be replaced by `zcash_address::unified::Address`.
pub(crate) fn fake_ua_encode(p2pkh: &TransparentAddress, sapling: &PaymentAddress) -> String {
format!(
"DONOTUSEUA{}{}",
hex::encode(match p2pkh {
TransparentAddress::PublicKey(data) => data,
TransparentAddress::Script(_) => panic!(),
}),
hex::encode(&sapling.to_bytes())
)
}
// This is temporary, and will be replaced by `zcash_address::unified::Address`.
//pub(crate) fn fake_ua_decode(encoding: &str) -> Option<(TransparentAddress, PaymentAddress)> {
// encoding
// .strip_prefix("DONOTUSEUA")
// .and_then(|data| hex::decode(data).ok())
// .and_then(|data| {
// PaymentAddress::from_bytes(&data[20..].try_into().unwrap()).map(|pa| {
// (
// TransparentAddress::PublicKey(data[..20].try_into().unwrap()),
// pa,
// )
// })
// })
//}