Add a `seed` argument to `Synchronizer.Companion.new`

This enables callers to recover from an error that indicates the seed
is needed for a database migration.
This commit is contained in:
Jack Grigg 2022-09-21 01:41:43 +01:00 committed by Carter Jernigan
parent b19ee179c5
commit c6fd783317
14 changed files with 70 additions and 26 deletions

View File

@ -27,6 +27,9 @@ Change Log
- `Initializer.Config.newWallet` - `Initializer.Config.newWallet`
- `Initializer.Config.setViewingKeys` - `Initializer.Config.setViewingKeys`
- `cash.z.ecc.android.sdk`: - `cash.z.ecc.android.sdk`:
- `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 - `Synchronizer.shieldFunds` now takes a transparent account private key (representing
all transparent secret keys within an account) instead of a transparent secret key. all transparent secret keys within an account) instead of a transparent secret key.

View File

@ -65,7 +65,7 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
) )
} }
}.let { initializer -> }.let { initializer ->
synchronizer = Synchronizer.newBlocking(initializer) synchronizer = Synchronizer.newBlocking(initializer, seed)
} }
} }

View File

@ -72,7 +72,7 @@ class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBindin
ZcashNetwork.fromResources(requireApplicationContext()) ZcashNetwork.fromResources(requireApplicationContext())
) )
} }
synchronizer = Synchronizer.newBlocking(initializer) synchronizer = Synchronizer.newBlocking(initializer, seed)
} }
private fun initTransactionUI() { private fun initTransactionUI() {

View File

@ -79,7 +79,7 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
it.alias = "Demo_Utxos" it.alias = "Demo_Utxos"
} }
} }
synchronizer = runBlocking { Synchronizer.new(initializer) } synchronizer = runBlocking { Synchronizer.new(initializer, seed) }
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {

View File

@ -80,7 +80,7 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
} }
} }
}.let { initializer -> }.let { initializer ->
synchronizer = Synchronizer.newBlocking(initializer) synchronizer = Synchronizer.newBlocking(initializer, seed)
} }
spendingKey = runBlocking { spendingKey = runBlocking {
DerivationTool.deriveSpendingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first() DerivationTool.deriveSpendingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first()

2
sdk-lib/Cargo.lock generated
View File

@ -2025,7 +2025,9 @@ dependencies = [
"jni", "jni",
"log", "log",
"log-panics", "log-panics",
"schemer",
"secp256k1", "secp256k1",
"secrecy",
"zcash_client_backend", "zcash_client_backend",
"zcash_client_sqlite", "zcash_client_sqlite",
"zcash_primitives", "zcash_primitives",

View File

@ -18,7 +18,9 @@ hex = "0.4"
jni = { version = "0.17", default-features = false } jni = { version = "0.17", default-features = false }
log = "0.4" log = "0.4"
log-panics = "2.0.0" log-panics = "2.0.0"
schemer = "0.2"
secp256k1 = "0.21" secp256k1 = "0.21"
secrecy = "0.8"
zcash_client_backend = { version = "0.5", features = ["transparent-inputs"] } zcash_client_backend = { version = "0.5", features = ["transparent-inputs"] }
zcash_client_sqlite = { version = "0.3", features = ["transparent-inputs"] } zcash_client_sqlite = { version = "0.3", features = ["transparent-inputs"] }
zcash_primitives = "0.7" zcash_primitives = "0.7"

View File

@ -797,12 +797,13 @@ object DefaultSynchronizerFactory {
// can touch its views. and is probably related to FlowPagedList // can touch its views. and is probably related to FlowPagedList
// TODO [#242]: https://github.com/zcash/zcash-android-wallet-sdk/issues/242 // TODO [#242]: https://github.com/zcash/zcash-android-wallet-sdk/issues/242
private const val DEFAULT_PAGE_SIZE = 1000 private const val DEFAULT_PAGE_SIZE = 1000
suspend fun defaultTransactionRepository(initializer: Initializer): TransactionRepository = suspend fun defaultTransactionRepository(initializer: Initializer, seed: ByteArray?): TransactionRepository =
PagedTransactionRepository.new( PagedTransactionRepository.new(
initializer.context, initializer.context,
initializer.network, initializer.network,
DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE,
initializer.rustBackend, initializer.rustBackend,
seed,
initializer.checkpoint, initializer.checkpoint,
initializer.viewingKeys, initializer.viewingKeys,
initializer.overwriteVks initializer.overwriteVks

View File

@ -444,11 +444,14 @@ interface Synchronizer {
* Synchronizer requires. It contains all information necessary to build a synchronizer and it is * Synchronizer requires. It contains all information necessary to build a synchronizer and it is
* mainly responsible for initializing the databases associated with this synchronizer and loading * mainly responsible for initializing the databases associated with this synchronizer and loading
* the rust backend. * the rust backend.
* @param seed the wallet's seed phrase. This only needs to be provided if this method returns an
* error indicating that the seed phrase is required for a database migration.
*/ */
suspend fun new( suspend fun new(
initializer: Initializer initializer: Initializer,
seed: ByteArray,
): Synchronizer { ): Synchronizer {
val repository = DefaultSynchronizerFactory.defaultTransactionRepository(initializer) val repository = DefaultSynchronizerFactory.defaultTransactionRepository(initializer, seed)
val blockStore = DefaultSynchronizerFactory.defaultBlockStore(initializer) val blockStore = DefaultSynchronizerFactory.defaultBlockStore(initializer)
val service = DefaultSynchronizerFactory.defaultService(initializer) val service = DefaultSynchronizerFactory.defaultService(initializer)
val encoder = DefaultSynchronizerFactory.defaultEncoder(initializer, repository) val encoder = DefaultSynchronizerFactory.defaultEncoder(initializer, repository)
@ -472,6 +475,8 @@ interface Synchronizer {
* This is a blocking call, so it should not be called from the main thread. * This is a blocking call, so it should not be called from the main thread.
*/ */
@JvmStatic @JvmStatic
fun newBlocking(initializer: Initializer): Synchronizer = runBlocking { new(initializer) } fun newBlocking(initializer: Initializer, seed: ByteArray): Synchronizer = runBlocking {
new (initializer, seed)
}
} }
} }

View File

@ -175,6 +175,10 @@ sealed class BirthdayException(message: String, cause: Throwable? = null) : SdkE
* Exceptions thrown by the initializer. * Exceptions thrown by the initializer.
*/ */
sealed class InitializerException(message: String, cause: Throwable? = null) : SdkException(message, cause) { sealed class InitializerException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
object SeedRequired : InitializerException(
"A pending database migration requires the wallet's seed. Call this initialization " +
"method again with the seed."
)
class FalseStart(cause: Throwable?) : InitializerException("Failed to initialize accounts due to: $cause", cause) class FalseStart(cause: Throwable?) : InitializerException("Failed to initialize accounts due to: $cause", cause)
class AlreadyInitializedException(cause: Throwable, dbPath: String) : InitializerException( class AlreadyInitializedException(cause: Throwable, dbPath: String) : InitializerException(
"Failed to initialize the blocks table" + "Failed to initialize the blocks table" +

View File

@ -5,6 +5,7 @@ import androidx.paging.PagedList
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import cash.z.ecc.android.sdk.db.commonDatabaseBuilder import cash.z.ecc.android.sdk.db.commonDatabaseBuilder
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.exception.InitializerException.SeedRequired
import cash.z.ecc.android.sdk.ext.ZcashSdk import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.internal.SdkDispatchers import cash.z.ecc.android.sdk.internal.SdkDispatchers
import cash.z.ecc.android.sdk.internal.SdkExecutors import cash.z.ecc.android.sdk.internal.SdkExecutors
@ -122,11 +123,12 @@ internal class PagedTransactionRepository private constructor(
zcashNetwork: ZcashNetwork, zcashNetwork: ZcashNetwork,
pageSize: Int = 10, pageSize: Int = 10,
rustBackend: RustBackend, rustBackend: RustBackend,
seed: ByteArray?,
birthday: Checkpoint, birthday: Checkpoint,
viewingKeys: List<UnifiedFullViewingKey>, viewingKeys: List<UnifiedFullViewingKey>,
overwriteVks: Boolean = false overwriteVks: Boolean = false
): PagedTransactionRepository { ): PagedTransactionRepository {
initMissingDatabases(rustBackend, birthday, viewingKeys) initMissingDatabases(rustBackend, seed, birthday, viewingKeys)
val db = buildDatabase(appContext.applicationContext, rustBackend.dataDbFile) val db = buildDatabase(appContext.applicationContext, rustBackend.dataDbFile)
applyKeyMigrations(rustBackend, overwriteVks, viewingKeys) applyKeyMigrations(rustBackend, overwriteVks, viewingKeys)
@ -172,10 +174,11 @@ internal class PagedTransactionRepository private constructor(
*/ */
private suspend fun initMissingDatabases( private suspend fun initMissingDatabases(
rustBackend: RustBackend, rustBackend: RustBackend,
seed: ByteArray?,
birthday: Checkpoint, birthday: Checkpoint,
viewingKeys: List<UnifiedFullViewingKey> viewingKeys: List<UnifiedFullViewingKey>
) { ) {
maybeCreateDataDb(rustBackend) maybeCreateDataDb(rustBackend, seed)
maybeInitBlocksTable(rustBackend, birthday) maybeInitBlocksTable(rustBackend, birthday)
maybeInitAccountsTable(rustBackend, viewingKeys) maybeInitAccountsTable(rustBackend, viewingKeys)
} }
@ -183,9 +186,15 @@ internal class PagedTransactionRepository private constructor(
/** /**
* Create the dataDb and its table, if it doesn't exist. * Create the dataDb and its table, if it doesn't exist.
*/ */
private suspend fun maybeCreateDataDb(rustBackend: RustBackend) { private suspend fun maybeCreateDataDb(rustBackend: RustBackend, seed: ByteArray?) {
tryWarn("Warning: did not create dataDb. It probably already exists.") { tryWarn(
rustBackend.initDataDb() "Warning: did not create dataDb. It probably already exists.",
unlessContains = "requires the wallet's seed"
) {
val res = rustBackend.initDataDb(seed)
if (res == 1) {
throw SeedRequired
}
twig("Initialized wallet for first run file: ${rustBackend.dataDbFile}") twig("Initialized wallet for first run file: ${rustBackend.dataDbFile}")
} }
} }

View File

@ -44,9 +44,10 @@ internal class RustBackend private constructor(
// Wrapper Functions // Wrapper Functions
// //
override suspend fun initDataDb() = withContext(SdkDispatchers.DATABASE_IO) { override suspend fun initDataDb(seed: ByteArray?) = withContext(SdkDispatchers.DATABASE_IO) {
initDataDb( initDataDb(
dataDbFile.absolutePath, dataDbFile.absolutePath,
seed,
networkId = network.id networkId = network.id
) )
} }
@ -384,7 +385,7 @@ internal class RustBackend private constructor(
// //
@JvmStatic @JvmStatic
private external fun initDataDb(dbDataPath: String, networkId: Int): Boolean private external fun initDataDb(dbDataPath: String, seed: ByteArray?, networkId: Int): Int
@JvmStatic @JvmStatic
private external fun initAccountsTableWithKeys( private external fun initAccountsTableWithKeys(

View File

@ -40,7 +40,7 @@ internal interface RustBackendWelding {
suspend fun initBlocksTable(checkpoint: Checkpoint): Boolean suspend fun initBlocksTable(checkpoint: Checkpoint): Boolean
suspend fun initDataDb(): Boolean suspend fun initDataDb(seed: ByteArray?): Int
fun isValidShieldedAddr(addr: String): Boolean fun isValidShieldedAddr(addr: String): Boolean

View File

@ -17,7 +17,9 @@ use jni::{
JNIEnv, JNIEnv,
}; };
use log::Level; use log::Level;
use schemer::MigratorError;
use secp256k1::PublicKey; use secp256k1::PublicKey;
use secrecy::SecretVec;
use zcash_client_backend::keys::UnifiedSpendingKey; use zcash_client_backend::keys::UnifiedSpendingKey;
use zcash_client_backend::{ use zcash_client_backend::{
address::RecipientAddress, address::RecipientAddress,
@ -36,6 +38,7 @@ use zcash_client_backend::{
keys::{sapling, UnifiedFullViewingKey}, keys::{sapling, UnifiedFullViewingKey},
wallet::{OvkPolicy, WalletTransparentOutput}, wallet::{OvkPolicy, WalletTransparentOutput},
}; };
use zcash_client_sqlite::wallet::init::WalletMigrationError;
#[allow(deprecated)] #[allow(deprecated)]
use zcash_client_sqlite::wallet::{delete_utxos_above, get_rewind_height}; use zcash_client_sqlite::wallet::{delete_utxos_above, get_rewind_height};
use zcash_client_sqlite::{ use zcash_client_sqlite::{
@ -106,26 +109,40 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initLogs(
print_debug_state() print_debug_state()
} }
/// Sets up the internal structure of the data database.
///
/// If `seed` is `null`, database migrations will be attempted without it.
///
/// Returns 0 if successful, 1 if the seed must be provided in order to execute the requested
/// migrations, or -1 otherwise.
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initDataDb( pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initDataDb(
env: JNIEnv<'_>, env: JNIEnv<'_>,
_: JClass<'_>, _: JClass<'_>,
db_data: JString<'_>, db_data: JString<'_>,
seed: jbyteArray,
network_id: jint, network_id: jint,
) -> jboolean { ) -> jint {
let res = panic::catch_unwind(|| { let res = panic::catch_unwind(|| {
let network = parse_network(network_id as u32)?; let network = parse_network(network_id as u32)?;
let db_path = utils::java_string_to_rust(&env, db_data); let db_path = utils::java_string_to_rust(&env, db_data);
let seed = None; // TODO Update
WalletDb::for_path(db_path, network) let mut db_data = WalletDb::for_path(db_path, network)
.map_err(|e| format_err!("Error while opening data DB: {}", e)) .map_err(|e| format_err!("Error while opening data DB: {}", e))?;
.and_then(|mut db| {
init_wallet_db(&mut db, seed) let seed = (!seed.is_null()).then(|| SecretVec::new(env.convert_byte_array(seed).unwrap()));
.map_err(|e| format_err!("Error while initializing data DB: {}", e))
}) match init_wallet_db(&mut db_data, seed) {
.map(|()| JNI_TRUE) Ok(()) => Ok(0),
Err(MigratorError::Migration { error, .. })
if matches!(error, WalletMigrationError::SeedRequired) =>
{
Ok(1)
}
Err(e) => Err(format_err!("Error while initializing data DB: {}", e)),
}
}); });
unwrap_exc_or(&env, res, JNI_FALSE) unwrap_exc_or(&env, res, -1)
} }
#[no_mangle] #[no_mangle]