Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch from ZIP 32 account indices to UUID account identifiers #1640

Merged
merged 47 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
9b35832
Switch from ZIP 32 account indices to UUID account identifiers
str4d Nov 22, 2024
56a3858
Rename TransactionRecipient.Account
HonzaR Dec 3, 2024
c66b7ad
Fix `Backend.createAccount` API parameters
HonzaR Dec 4, 2024
5d07455
Add `importAccountUfvk` to the Rust backend.
nuttycom Dec 4, 2024
c8c1b2a
Propagate accountName and keySource across SDK
HonzaR Dec 4, 2024
5c3d1b5
Propagate new importAccount across SDK
HonzaR Dec 5, 2024
32e68e3
Wrap createAccount setup information
HonzaR Dec 5, 2024
cdfdbc9
Wrap importAccount setup information
HonzaR Dec 5, 2024
1cfba57
WIP: Add key derivation metadata to JniAccount
nuttycom Dec 6, 2024
7efea35
Fix JniAccount
HonzaR Dec 6, 2024
c1d2ff8
Use updated JniAccount across SDK
HonzaR Dec 6, 2024
ee27c0e
Several tests fix
HonzaR Dec 8, 2024
d87c5bd
Add `Zip32AccountIndex` wrapper
HonzaR Dec 9, 2024
d2b4d1f
Update key source parameter
HonzaR Dec 9, 2024
e4ffb26
Merge branch 'feature-2.2.7' into account-uuids
HonzaR Dec 9, 2024
02e1569
Remove account from `UnifiedSpendingKey`
HonzaR Dec 9, 2024
8823694
Fix `importAccountByUfvk` API
HonzaR Dec 9, 2024
922eb0e
Remove account_uuid from `JniUnifiedSpendingKey` creation
HonzaR Dec 9, 2024
7a95e23
Fixture fix
HonzaR Dec 9, 2024
89b06e4
Refactor Account.accountUuid to wrapper class
HonzaR Dec 9, 2024
d9482b1
Revert "Remove account_uuid from `JniUnifiedSpendingKey` creation"
HonzaR Dec 10, 2024
f669645
Add `JniAccountUsk`
HonzaR Dec 10, 2024
63a9a54
Add `seedFingerprint` and `zip32AccountIndex`
HonzaR Dec 10, 2024
fe653d4
Transactions by account UUID
HonzaR Dec 10, 2024
52f5b28
Refactor default account creation
HonzaR Dec 10, 2024
0909a73
Migrate to Rust crate revision with bugfix to account UUID migration
str4d Dec 10, 2024
477c933
Migrate to Rust crate revision with another bugfix to account UUID mi…
str4d Dec 10, 2024
d299b21
Test fix
HonzaR Dec 11, 2024
3c74069
Migrate to Rust crate revision with a third bugfix to account UUID mi…
str4d Dec 11, 2024
e54fb17
Self-review fixes
HonzaR Dec 11, 2024
51f5cd4
Tests fix
HonzaR Dec 11, 2024
c9bc790
Changelog update
HonzaR Dec 11, 2024
8b558f7
Update backend-lib/src/main/rust/lib.rs
HonzaR Dec 11, 2024
105da9d
Update backend-lib/src/main/rust/lib.rs
HonzaR Dec 11, 2024
0b603e2
Changelog update
HonzaR Dec 11, 2024
c68be81
Update sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt
HonzaR Dec 11, 2024
d07857c
Update sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/Typesafe…
HonzaR Dec 11, 2024
0fae465
Update sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/Typesafe…
HonzaR Dec 11, 2024
f78ed39
Update sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/Typesafe…
HonzaR Dec 11, 2024
b59e768
Update sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/Typesafe…
HonzaR Dec 11, 2024
e01773e
Update sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/Typesafe…
HonzaR Dec 11, 2024
840df94
Update sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountImpo…
HonzaR Dec 11, 2024
07f357e
Update sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountCrea…
HonzaR Dec 11, 2024
a03ae02
Update sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Account.kt
HonzaR Dec 11, 2024
cb8036d
Update sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt
HonzaR Dec 11, 2024
e07d65d
Refactor inputs of `importAccountUfvk`
HonzaR Dec 11, 2024
7a064ec
Use `FirsClassByteArray` for `seed` parameter
HonzaR Dec 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ const val JNI_ACCOUNT_UUID_BYTES_SIZE = 16
/**
* The number of bytes in the seed fingerprint parameter. It's used e.g. in [JniAccount.seedFingerprint]
*/
const val JNI_ACCOUNT_SEED_FP_BYTES_SIZE = 16
const val JNI_ACCOUNT_SEED_FP_BYTES_SIZE = 32
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package cash.z.ecc.android.sdk.internal.model

import androidx.annotation.Keep
import cash.z.ecc.android.sdk.internal.jni.JNI_ACCOUNT_UUID_BYTES_SIZE
import cash.z.ecc.android.sdk.internal.jni.JNI_ACCOUNT_SEED_FP_BYTES_SIZE
import cash.z.ecc.android.sdk.internal.jni.JNI_ACCOUNT_UUID_BYTES_SIZE

/**
* Serves as cross layer (Kotlin, Rust) communication class.
Expand All @@ -12,18 +12,19 @@ import cash.z.ecc.android.sdk.internal.jni.JNI_ACCOUNT_SEED_FP_BYTES_SIZE
* @param accountName A human-readable name for the account
* @param keySource A string identifier or other metadata describing the source of the seed
* @param seedFingerprint The seed fingerprint
* @param hdAccountIndex
* @param hdAccountIndex ZIP 32 account index
*
* @throws IllegalArgumentException if the values are inconsistent.
*/
@Keep
class JniAccount(
val accountUuid: ByteArray,
val ufvk: String?,
val accountName: String?,
val accountUuid: ByteArray,
// We use -1L to represent NULL across JNI
val hdAccountIndex: Long,
val keySource: String?,
val seedFingerprint: ByteArray?,
val hdAccountIndex: Long?,
val ufvk: String?,
) {
init {
require(accountUuid.size == JNI_ACCOUNT_UUID_BYTES_SIZE) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package cash.z.ecc.android.sdk.internal.model

import androidx.annotation.Keep
import cash.z.ecc.android.sdk.internal.jni.JNI_ACCOUNT_UUID_BYTES_SIZE

/**
* Serves as cross layer (Kotlin, Rust) communication class.
*
* @param account the account ID
* @param accountUuid the account UUID in ByteArray
* @param saplingVerifiedBalance The verified account balance in the Sapling pool.
* @param saplingChangePending The value in the account of Sapling change notes that do
* not yet have sufficient confirmations to be spendable.
Expand All @@ -25,7 +26,7 @@ import androidx.annotation.Keep
@Keep
@Suppress("LongParameterList")
class JniAccountBalance(
val account: ByteArray,
val accountUuid: ByteArray,
val saplingVerifiedBalance: Long,
val saplingChangePending: Long,
val saplingValuePending: Long,
Expand All @@ -35,6 +36,9 @@ class JniAccountBalance(
val unshieldedBalance: Long,
) {
init {
require(accountUuid.size == JNI_ACCOUNT_UUID_BYTES_SIZE) {
"Account UUID must be 16 bytes"
}
require(saplingVerifiedBalance >= MIN_INCLUSIVE) {
"Sapling verified balance $saplingVerifiedBalance must by equal or above $MIN_INCLUSIVE"
}
Expand Down
12 changes: 6 additions & 6 deletions backend-lib/src/main/rust/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,22 +288,22 @@ fn encode_account<'a, P: Parameters>(
None => JObject::null(),
};

let null = JObject::null();
let hd_account_index = match account.source().key_derivation() {
Some(d) => JValue::Long(i64::from(u32::from(d.account_index()))),
None => (&null).into(),
// Use -1 to return null across the FFI.
None => JValue::Long(-1),
};

env.new_object(
JNI_ACCOUNT,
"([BLjava/lang/String;Ljava/lang/String;Ljava/lang/String;[BI)V",
"(Ljava/lang/String;[BJLjava/lang/String;[BLjava/lang/String;)V",
&[
(&env.byte_array_from_slice(account.id().expose_uuid().as_bytes())?).into(),
(&ufvk).into(),
(&account_name).into(),
(&env.byte_array_from_slice(account.id().expose_uuid().as_bytes())?).into(),
hd_account_index,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the hd_account_index and seed fingerprint go together, as they're the two necessary bits of HD derivation information.

(&key_source).into(),
(&seed_fingerprint).into(),
hd_account_index
(&ufvk).into()
],
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ object JniAccountBalanceFixture {
orchardValuePending: Long = ORCHARD_VALUE_PENDING,
unshieldedBalance: Long = UNSHIELDED_BALANCE,
) = JniAccountBalance(
account = account,
accountUuid = account,
saplingVerifiedBalance = saplingVerifiedBalance,
saplingChangePending = saplingChangePending,
saplingValuePending = saplingValuePending,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ val ANDROID_STATE_FLOW_TIMEOUT = 5.seconds
*/
const val MINIMAL_WEIGHT = 0.0001f

// TODO [#1644]: Refactor Account ZIP32 index across SDK
// TODO [#1644]: https://github.com/Electric-Coin-Company/zcash-android-wallet-sdk/issues/1644
/**
* Until we support full multi-account feature in Demo app we use this constant as a single source of truth for
* account selection
*/
const val CURRENT_ZIP_32_ACCOUNT_INDEX = 0
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import cash.z.ecc.android.sdk.demoapp.ext.defaultForNetwork
import cash.z.ecc.android.sdk.demoapp.util.fromResources
import cash.z.ecc.android.sdk.ext.onFirst
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.model.AccountCreateSetup
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.ZcashNetwork
import co.electriccoin.lightwallet.client.ext.BenchmarkingExt
Expand Down Expand Up @@ -76,19 +77,19 @@ class SharedViewModel(application: Application) : AndroidViewModel(application)
val network = ZcashNetwork.fromResources(application)
val synchronizer =
Synchronizer.new(
application,
network,
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
seed = seedBytes,
alias = OLD_UI_SYNCHRONIZER_ALIAS,
birthday =
if (BenchmarkingExt.isBenchmarking()) {
BlockHeight.new(BenchmarkingBlockRangeFixture.new().start)
} else {
birthdayHeight.value
},
if (BenchmarkingExt.isBenchmarking()) {
BlockHeight.new(BenchmarkingBlockRangeFixture.new().start)
} else {
birthdayHeight.value
},
context = application,
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
setup = AccountCreateSetup("ZCASH", "Zcash Account 1", seedBytes),
// We use restore mode as this is always initialization with an older seed
walletInitMode = WalletInitMode.RestoreWallet,
alias = OLD_UI_SYNCHRONIZER_ALIAS
zcashNetwork = network,
)

send(InternalSynchronizerStatus.Available(synchronizer))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import cash.z.ecc.android.sdk.ext.convertZatoshiToZec
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
import cash.z.ecc.android.sdk.ext.toUsdString
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.model.Account
import cash.z.ecc.android.sdk.model.PercentDecimal
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
Expand Down Expand Up @@ -79,7 +78,7 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
DerivationTool.getInstance().deriveUnifiedSpendingKey(
seed,
network,
Account(CURRENT_ZIP_32_ACCOUNT_INDEX)
CURRENT_ZIP_32_ACCOUNT_INDEX
)
sharedViewModel.synchronizerFlow.value?.let { synchronizer ->
synchronizer.proposeShielding(usk.account, Zatoshi(100000))?.let { it1 ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetPrivateKeyBinding
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.Account
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -50,7 +49,7 @@ class GetPrivateKeyFragment : BaseDemoFragment<FragmentGetPrivateKeyBinding>() {
DerivationTool.getInstance().deriveUnifiedSpendingKey(
seed,
ZcashNetwork.fromResources(requireApplicationContext()),
Account(5)
5
)

// derive the key that allows you to view but not spend transactions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import cash.z.ecc.android.sdk.demoapp.ui.common.throttle
import cash.z.ecc.android.sdk.demoapp.util.fromResources
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.model.Account
import cash.z.ecc.android.sdk.model.AccountBalance
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.ObserveFiatCurrencyResult
import cash.z.ecc.android.sdk.model.PercentDecimal
Expand Down Expand Up @@ -104,11 +105,13 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
withContext(Dispatchers.IO) {
Mnemonics.MnemonicCode(it.seedPhrase.joinToString()).toSeed()
}
val accountIndex = getCurrentAccount().hdAccountIndex?.toInt()
?: error("The account must be set at this point")

DerivationTool.getInstance().deriveUnifiedSpendingKey(
seed = bip39Seed,
network = it.network,
// TODO: Get accountIndex from account returned by getCurrentAccount()
accountIndex = 0
accountIndex = accountIndex
)
}.stateIn(
viewModelScope,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk
import android.content.Context
import cash.z.ecc.android.sdk.ext.onFirst
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.model.AccountCreateSetup
import cash.z.ecc.android.sdk.model.PersistableWallet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
Expand Down Expand Up @@ -79,7 +80,11 @@ class WalletCoordinator(
zcashNetwork = persistableWallet.network,
lightWalletEndpoint = persistableWallet.endpoint,
birthday = persistableWallet.birthday,
seed = persistableWallet.seedPhrase.toByteArray(),
setup = AccountCreateSetup(
accountName = "Zcash Account 1",
keySource = "ZCASH",
seed = persistableWallet.seedPhrase.toByteArray()
),
walletInitMode = persistableWallet.walletInitMode,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.fixture
import cash.z.ecc.android.sdk.model.Account
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.ZcashNetwork
import java.util.UUID

/**
* Provides two default wallets, making it easy to test sending funds back and forth between them.
Expand All @@ -19,7 +20,7 @@ sealed class WalletFixture {
@Suppress("MaxLineLength")
data object Ben : WalletFixture() {
override val accounts: List<Account>
get() = listOf(AccountFixture.new(0))
get() = listOf(AccountFixture.new(UUID.fromString("52175368-821a-4664-8a7c-6a75d850f71c")))

override val seedPhrase: String
get() =
Expand Down Expand Up @@ -56,7 +57,7 @@ sealed class WalletFixture {
@Suppress("MaxLineLength")
data object Alice : WalletFixture() {
override val accounts: List<Account>
get() = listOf(AccountFixture.new(1))
get() = listOf(AccountFixture.new(UUID.fromString("8a204240-73a5-4e7a-93c2-a6e05711a000")))

override val seedPhrase: String
get() =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ class UnifiedSpendingKeyTest {
fun factory_copies_bytes() =
runTest {
val spendingKey = WalletFixture.getUnifiedSpendingKey()
val expected = spendingKey.copyBytes().copyOf()
val expected = spendingKey.copyOf()

val bytes = spendingKey.copyBytes()
val bytes = spendingKey
val newSpendingKey = UnifiedSpendingKey.new(spendingKey.account, bytes)
bytes.clear()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,23 @@ import java.util.UUID
* Note that these values are used in the automated tests only and are not passed across the JNI.
*/
object AccountFixture {
@Suppress("UnusedPrivateMember")
const val ZIP_32_ACCOUNT_INDEX = 0L

val ACCOUNT_UUID = UUID.fromString("01234567-89ab-cdef-0123-456789abcdef")
const val ZIP_32_ACCOUNT_INDEX = 0
val ACCOUNT_NAME = "Test Account"
val UFVK = "ufvk1d68jqrx0q98rl0w8f5085y898x0p9z5k0sksqre87949w9494949"
val KEY_SOURCE = "ZASHI"
val SEED_FINGER_PRINT = "8ac5439f"
val HD_ACCOUNT_INDEX = ZIP_32_ACCOUNT_INDEX

fun new(accountId: UUID = ACCOUNT_UUID) =
Account(
accountUuid = accountId.toByteArray()
accountName = ACCOUNT_NAME,
accountUuid = accountId.toByteArray(),
hdAccountIndex = HD_ACCOUNT_INDEX,
keySource = KEY_SOURCE,
seedFingerprint = SEED_FINGER_PRINT.toByteArray(),
ufvk = UFVK,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@ fun Derivation.deriveUnifiedSpendingKey(
seed: ByteArray,
network: ZcashNetwork,
accountIndex: Int
): UnifiedSpendingKey = UnifiedSpendingKey(deriveUnifiedSpendingKey(seed, network.id, accountIndex))
): UnifiedSpendingKey =
UnifiedSpendingKey(
JniUnifiedSpendingKey(
// fixme
accountUuid = byteArrayOf(),
bytes = deriveUnifiedSpendingKey(seed, network.id, accountIndex)
)
)

fun Derivation.deriveUnifiedFullViewingKey(
usk: UnifiedSpendingKey,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke
override val network: ZcashNetwork
get() = ZcashNetwork.from(backend.networkId)

override suspend fun getAccounts(): List<Account> = backend.getAccounts().map { Account(it.accountUuid) }
override suspend fun getAccounts(): List<Account> = backend.getAccounts().map { Account.new(it) }

override suspend fun createAccountAndGetSpendingKey(
accountName: String,
Expand All @@ -55,15 +55,16 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke
setup: AccountImportSetup,
treeState: ByteArray,
): Account {
return Account(
backend.importAccountUfvk(
accountName = setup.accountName,
keySource = setup.keySource,
purpose = purpose.value,
recoverUntil = recoverUntil,
treeState = treeState,
ufvk = setup.ufvk.encoding,
).accountUuid
return Account.new(
jniAccount =
backend.importAccountUfvk(
accountName = setup.accountName,
keySource = setup.keySource,
purpose = purpose.value,
recoverUntil = recoverUntil,
treeState = treeState,
ufvk = setup.ufvk.encoding
)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ internal data class WalletSummary(
fun new(jni: JniWalletSummary): WalletSummary {
return WalletSummary(
accountBalances =
jni.accountBalances.associateBy({ Account(it.account) }, {
AccountBalance.new(it)
}),
jni.accountBalances.associateBy(
{ Account.new(it.accountUuid) },
{ AccountBalance.new(it) }
),
chainTipHeight = BlockHeight(jni.chainTipHeight),
fullyScannedHeight = BlockHeight(jni.fullyScannedHeight),
scanProgress = ScanProgress.new(jni),
Expand Down
Loading
Loading