diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b4aacf6..d20ec467 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ Change Log ### Added - `cash.z.ecc.android.sdk`: - `Synchronizer.isValidUnifiedAddr` +- `cash.z.ecc.android.sdk.model`: + - `FirstClassByteArray` + - `UnifiedSpendingKey` - `cash.z.ecc.android.sdk.tool`: - `DerivationTool.deriveTransparentAccountPrivateKey` - `DerivationTool.deriveTransparentAddressFromAccountPrivateKey` diff --git a/sdk-lib/Cargo.lock b/sdk-lib/Cargo.lock index 100045e6..72c99df2 100644 --- a/sdk-lib/Cargo.lock +++ b/sdk-lib/Cargo.lock @@ -591,7 +591,7 @@ dependencies = [ [[package]] name = "equihash" version = "0.2.0" -source = "git+https://github.com/zcash/librustzcash.git?rev=6cb0d212195a32b7456c866bcd367fc98967b668#6cb0d212195a32b7456c866bcd367fc98967b668" +source = "git+https://github.com/zcash/librustzcash.git?rev=774ffadf5a0120a74d70d281974d079ccd58c600#774ffadf5a0120a74d70d281974d079ccd58c600" dependencies = [ "blake2b_simd", "byteorder", @@ -609,7 +609,7 @@ dependencies = [ [[package]] name = "f4jumble" version = "0.1.0" -source = "git+https://github.com/zcash/librustzcash.git?rev=6cb0d212195a32b7456c866bcd367fc98967b668#6cb0d212195a32b7456c866bcd367fc98967b668" +source = "git+https://github.com/zcash/librustzcash.git?rev=774ffadf5a0120a74d70d281974d079ccd58c600#774ffadf5a0120a74d70d281974d079ccd58c600" dependencies = [ "blake2b_simd", ] @@ -2037,7 +2037,7 @@ dependencies = [ [[package]] name = "zcash_address" version = "0.1.0" -source = "git+https://github.com/zcash/librustzcash.git?rev=6cb0d212195a32b7456c866bcd367fc98967b668#6cb0d212195a32b7456c866bcd367fc98967b668" +source = "git+https://github.com/zcash/librustzcash.git?rev=774ffadf5a0120a74d70d281974d079ccd58c600#774ffadf5a0120a74d70d281974d079ccd58c600" dependencies = [ "bech32", "bs58", @@ -2048,12 +2048,13 @@ dependencies = [ [[package]] name = "zcash_client_backend" version = "0.5.0" -source = "git+https://github.com/zcash/librustzcash.git?rev=6cb0d212195a32b7456c866bcd367fc98967b668#6cb0d212195a32b7456c866bcd367fc98967b668" +source = "git+https://github.com/zcash/librustzcash.git?rev=774ffadf5a0120a74d70d281974d079ccd58c600#774ffadf5a0120a74d70d281974d079ccd58c600" dependencies = [ "base64", "bech32", "bls12_381", "bs58", + "byteorder", "crossbeam-channel", "ff", "group", @@ -2061,6 +2062,7 @@ dependencies = [ "hex", "jubjub", "log", + "memuse", "nom", "orchard", "percent-encoding", @@ -2084,7 +2086,7 @@ dependencies = [ [[package]] name = "zcash_client_sqlite" version = "0.3.0" -source = "git+https://github.com/zcash/librustzcash.git?rev=6cb0d212195a32b7456c866bcd367fc98967b668#6cb0d212195a32b7456c866bcd367fc98967b668" +source = "git+https://github.com/zcash/librustzcash.git?rev=774ffadf5a0120a74d70d281974d079ccd58c600#774ffadf5a0120a74d70d281974d079ccd58c600" dependencies = [ "bech32", "bs58", @@ -2108,7 +2110,7 @@ dependencies = [ [[package]] name = "zcash_encoding" version = "0.1.0" -source = "git+https://github.com/zcash/librustzcash.git?rev=6cb0d212195a32b7456c866bcd367fc98967b668#6cb0d212195a32b7456c866bcd367fc98967b668" +source = "git+https://github.com/zcash/librustzcash.git?rev=774ffadf5a0120a74d70d281974d079ccd58c600#774ffadf5a0120a74d70d281974d079ccd58c600" dependencies = [ "byteorder", "nonempty", @@ -2117,7 +2119,7 @@ dependencies = [ [[package]] name = "zcash_note_encryption" version = "0.1.0" -source = "git+https://github.com/zcash/librustzcash.git?rev=6cb0d212195a32b7456c866bcd367fc98967b668#6cb0d212195a32b7456c866bcd367fc98967b668" +source = "git+https://github.com/zcash/librustzcash.git?rev=774ffadf5a0120a74d70d281974d079ccd58c600#774ffadf5a0120a74d70d281974d079ccd58c600" dependencies = [ "chacha20 0.9.0", "chacha20poly1305 0.10.1", @@ -2130,7 +2132,7 @@ dependencies = [ [[package]] name = "zcash_primitives" version = "0.7.0" -source = "git+https://github.com/zcash/librustzcash.git?rev=6cb0d212195a32b7456c866bcd367fc98967b668#6cb0d212195a32b7456c866bcd367fc98967b668" +source = "git+https://github.com/zcash/librustzcash.git?rev=774ffadf5a0120a74d70d281974d079ccd58c600#774ffadf5a0120a74d70d281974d079ccd58c600" dependencies = [ "aes", "bip0039", @@ -2167,7 +2169,7 @@ dependencies = [ [[package]] name = "zcash_proofs" version = "0.7.1" -source = "git+https://github.com/zcash/librustzcash.git?rev=6cb0d212195a32b7456c866bcd367fc98967b668#6cb0d212195a32b7456c866bcd367fc98967b668" +source = "git+https://github.com/zcash/librustzcash.git?rev=774ffadf5a0120a74d70d281974d079ccd58c600#774ffadf5a0120a74d70d281974d079ccd58c600" dependencies = [ "bellman", "blake2b_simd", diff --git a/sdk-lib/Cargo.toml b/sdk-lib/Cargo.toml index bfa1dd8a..fb17148d 100644 --- a/sdk-lib/Cargo.toml +++ b/sdk-lib/Cargo.toml @@ -21,8 +21,8 @@ log-panics = "2.0.0" schemer = "0.2" secp256k1 = "0.21" secrecy = "0.8" -zcash_client_backend = { version = "0.5", features = ["transparent-inputs"] } -zcash_client_sqlite = { version = "0.3", features = ["transparent-inputs"] } +zcash_client_backend = { version = "0.5", features = ["transparent-inputs", "unstable"] } +zcash_client_sqlite = { version = "0.3", features = ["transparent-inputs", "unstable"] } zcash_primitives = "0.7" zcash_proofs = "0.7" @@ -30,11 +30,11 @@ zcash_proofs = "0.7" [patch.crates-io] group = { git = "https://github.com/zkcrypto/group.git", rev = "a7f3ceb2373e9fe536996f7b4d55c797f3e667f0" } orchard = { git = 'https://github.com/zcash/orchard.git', rev='f206b3f5d4e31bba75d03d9d03d5fa25825a9384' } -zcash_client_backend = { git = 'https://github.com/zcash/librustzcash.git', rev='6cb0d212195a32b7456c866bcd367fc98967b668' } -zcash_client_sqlite = { git = 'https://github.com/zcash/librustzcash.git', rev='6cb0d212195a32b7456c866bcd367fc98967b668' } -zcash_note_encryption = { git = 'https://github.com/zcash/librustzcash.git', rev='6cb0d212195a32b7456c866bcd367fc98967b668' } -zcash_primitives = { git = 'https://github.com/zcash/librustzcash.git', rev='6cb0d212195a32b7456c866bcd367fc98967b668' } -zcash_proofs = { git = 'https://github.com/zcash/librustzcash.git', rev='6cb0d212195a32b7456c866bcd367fc98967b668' } +zcash_client_backend = { git = 'https://github.com/zcash/librustzcash.git', rev='774ffadf5a0120a74d70d281974d079ccd58c600' } +zcash_client_sqlite = { git = 'https://github.com/zcash/librustzcash.git', rev='774ffadf5a0120a74d70d281974d079ccd58c600' } +zcash_note_encryption = { git = 'https://github.com/zcash/librustzcash.git', rev='774ffadf5a0120a74d70d281974d079ccd58c600' } +zcash_primitives = { git = 'https://github.com/zcash/librustzcash.git', rev='774ffadf5a0120a74d70d281974d079ccd58c600' } +zcash_proofs = { git = 'https://github.com/zcash/librustzcash.git', rev='774ffadf5a0120a74d70d281974d079ccd58c600' } ## Uncomment this to test librustzcash changes locally #[patch.crates-io] diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt index 0cde3692..e23124fc 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt @@ -49,6 +49,7 @@ import cash.z.ecc.android.sdk.internal.transaction.WalletTransactionEncoder import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twigTask import cash.z.ecc.android.sdk.model.BlockHeight +import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.WalletBalance import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.ZcashNetwork @@ -622,6 +623,14 @@ class SdkSynchronizer internal constructor( } } + // + // Account management + // + + // Not ready to be a public API; internal for testing only + internal suspend fun createAccount(seed: ByteArray): UnifiedSpendingKey = + processor.createAccount(seed) + // // Send / Receive // diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt index d3862471..d3f8b07b 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt @@ -5,6 +5,7 @@ import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction import cash.z.ecc.android.sdk.db.entity.PendingTransaction import cash.z.ecc.android.sdk.ext.ZcashSdk import cash.z.ecc.android.sdk.model.BlockHeight +import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.WalletBalance import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.ZcashNetwork @@ -159,6 +160,33 @@ interface Synchronizer { // Operations // + /** + * Adds the next available account-level spend authority, given the current set of + * [ZIP 316](https://zips.z.cash/zip-0316) account identifiers known, to the wallet + * database. + * + * The caller should store the byte encoding of the returned spending key in a secure + * fashion. This encoding **MUST NOT** be exposed to users. It is an internal encoding + * that is inherently unstable, and only intended to be passed between the SDK and the + * storage backend. The caller **MUST NOT** allow this encoding to be exported or + * imported. + * + * If `seed` was imported from a backup and this method is being used to restore a + * previous wallet state, you should use this method to add all of the desired + * accounts before scanning the chain from the seed's birthday height. + * + * By convention, wallets should only allow a new account to be generated after funds + * have been received by the currently-available account (in order to enable + * automated account recovery). + * + * @param seed the wallet's seed phrase. + * + * @return the newly created ZIP 316 account identifier, along with the binary + * encoding of the `UnifiedSpendingKey` for the newly created account. + */ + // This is not yet ready to be a public API + // suspend fun createAccount(seed: ByteArray): UnifiedSpendingKey + /** * Gets the shielded address for the given account. This is syntactic sugar for * [getShieldedAddress] because we use z-addrs by default. diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/CompactBlockProcessor.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/CompactBlockProcessor.kt index 25df0a16..e143c967 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/CompactBlockProcessor.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/CompactBlockProcessor.kt @@ -41,6 +41,7 @@ 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.BlockHeight +import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.WalletBalance import cash.z.wallet.sdk.rpc.Service import io.grpc.StatusRuntimeException @@ -1016,6 +1017,11 @@ class CompactBlockProcessor internal constructor( suspend fun getLastScannedHeight() = repository.lastScannedHeight() + // TODO(str4d): CompactBlockProcessor is the wrong place for this, but it's where all the other APIs that need + // access to the RustBackend live. This should be refactored. + internal suspend fun createAccount(seed: ByteArray): UnifiedSpendingKey = + rustBackend.createAccount(seed) + /** * Get address corresponding to the given account for this wallet. * diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt index 30ef210d..d195a0d0 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt @@ -7,6 +7,7 @@ import cash.z.ecc.android.sdk.internal.ext.deleteSuspend import cash.z.ecc.android.sdk.internal.model.Checkpoint 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.WalletBalance import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.ZcashNetwork @@ -52,6 +53,16 @@ internal class RustBackend private constructor( ) } + override suspend fun createAccount(seed: ByteArray): UnifiedSpendingKey { + return withContext(SdkDispatchers.DATABASE_IO) { + createAccount( + dataDbFile.absolutePath, + seed, + networkId = network.id + ) + } + } + override suspend fun initAccountsTable(vararg keys: UnifiedFullViewingKey): Boolean { val ufvks = Array(keys.size) { keys[it].encoding } @@ -405,6 +416,9 @@ internal class RustBackend private constructor( networkId: Int ): Boolean + @JvmStatic + private external fun createAccount(dbDataPath: String, seed: ByteArray, networkId: Int): UnifiedSpendingKey + @JvmStatic private external fun getCurrentAddress( dbDataPath: String, diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt index f3fcaf93..7bcc13d7 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt @@ -2,6 +2,7 @@ package cash.z.ecc.android.sdk.jni import cash.z.ecc.android.sdk.internal.model.Checkpoint import cash.z.ecc.android.sdk.model.BlockHeight +import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.WalletBalance import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.ZcashNetwork @@ -42,6 +43,8 @@ internal interface RustBackendWelding { suspend fun initDataDb(seed: ByteArray?): Int + suspend fun createAccount(seed: ByteArray): UnifiedSpendingKey + fun isValidShieldedAddr(addr: String): Boolean fun isValidTransparentAddr(addr: String): Boolean diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/FirstClassByteArray.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/FirstClassByteArray.kt new file mode 100644 index 00000000..8af8e003 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/FirstClassByteArray.kt @@ -0,0 +1,16 @@ +package cash.z.ecc.android.sdk.model + +class FirstClassByteArray(val byteArray: ByteArray) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as FirstClassByteArray + + if (!byteArray.contentEquals(other.byteArray)) return false + + return true + } + + override fun hashCode() = byteArray.contentHashCode() +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/UnifiedSpendingKey.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/UnifiedSpendingKey.kt new file mode 100644 index 00000000..d2a37b43 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/UnifiedSpendingKey.kt @@ -0,0 +1,32 @@ +package cash.z.ecc.android.sdk.model + +/** + * A [ZIP 316](https://zips.z.cash/zip-0316) Unified Spending Key. + * + * This is the spend authority for an account under the wallet's seed. + * + * An instance of this class contains all of the per-pool spending keys that could be + * derived at the time of its creation. As such, it is not suitable for long-term storage, + * export/import, or backup purposes. + */ +data class UnifiedSpendingKey internal constructor( + /** + * A [ZIP 316](https://zips.z.cash/zip-0316) account identifier. + */ + val account: Int, + + /** + * The binary encoding of the [ZIP 316](https://zips.z.cash/zip-0316) Unified Spending + * Key for [account]. + * + * This encoding **MUST NOT** be exposed to users. It is an internal encoding that is + * inherently unstable, and only intended to be passed between the SDK and the storage + * backend. Wallets **MUST NOT** allow this encoding to be exported or imported. + */ + internal val bytes: FirstClassByteArray +) { + // Override to prevent leaking key to logs + override fun toString() = "UnifiedSpendingKey(account=$account)" + + fun copyBytes() = bytes.byteArray.copyOf() +} diff --git a/sdk-lib/src/main/rust/lib.rs b/sdk-lib/src/main/rust/lib.rs index d0e870f3..61f74823 100644 --- a/sdk-lib/src/main/rust/lib.rs +++ b/sdk-lib/src/main/rust/lib.rs @@ -11,9 +11,10 @@ use std::str::FromStr; use android_logger::Config; use failure::format_err; use hdwallet::traits::{Deserialize, Serialize}; +use jni::objects::JValue; use jni::{ objects::{JClass, JString}, - sys::{jboolean, jbyteArray, jint, jlong, jobjectArray, jstring, JNI_FALSE, JNI_TRUE}, + sys::{jboolean, jbyteArray, jint, jlong, jobject, jobjectArray, jstring, JNI_FALSE, JNI_TRUE}, JNIEnv, }; use log::Level; @@ -35,7 +36,7 @@ use zcash_client_backend::{ decode_extended_spending_key, encode_extended_full_viewing_key, encode_extended_spending_key, AddressCodec, }, - keys::{sapling, UnifiedFullViewingKey}, + keys::{sapling, Era, UnifiedFullViewingKey}, wallet::{OvkPolicy, WalletTransparentOutput}, }; use zcash_client_sqlite::wallet::init::WalletMigrationError; @@ -145,6 +146,58 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initDataDb( unwrap_exc_or(&env, res, -1) } +/// Adds the next available account-level spend authority, given the current set of +/// [ZIP 316] account identifiers known, to the wallet database. +/// +/// 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. +/// +/// If `seed` was imported from a backup and this method is being used to restore a +/// previous wallet state, you should use this method to add all of the desired +/// accounts before scanning the chain from the seed's birthday height. +/// +/// By convention, wallets should only allow a new account to be generated after funds +/// have been received by the currently-available account (in order to enable +/// automated account recovery). +/// +/// [ZIP 316]: https://zips.z.cash/zip-0316 +#[no_mangle] +pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_createAccount( + env: JNIEnv<'_>, + _: JClass<'_>, + db_data: JString<'_>, + seed: jbyteArray, + network_id: jint, +) -> jobject { + let res = panic::catch_unwind(|| { + let network = parse_network(network_id as u32)?; + let db_data = wallet_db(&env, network, db_data)?; + let seed = SecretVec::new(env.convert_byte_array(seed).unwrap()); + + let mut db_ops = db_data.get_update_ops()?; + let (account, usk) = db_ops + .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()) + }); + unwrap_exc_or(&env, res, ptr::null_mut()) +} + +/// Initialises the data database with the given set of unified full viewing keys. +/// +/// This should only be used in special cases for implementing wallet recovery; prefer +/// `RustBackend.createAccount` for normal account creation purposes. #[no_mangle] pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initAccountsTableWithKeys( env: JNIEnv<'_>,