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.setViewingKeys`
- `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
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 ->
synchronizer = Synchronizer.newBlocking(initializer)
synchronizer = Synchronizer.newBlocking(initializer, seed)
}
}

View File

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

View File

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

View File

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

2
sdk-lib/Cargo.lock generated
View File

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

View File

@ -18,7 +18,9 @@ hex = "0.4"
jni = { version = "0.17", default-features = false }
log = "0.4"
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_primitives = "0.7"

View File

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

View File

@ -444,11 +444,14 @@ interface Synchronizer {
* 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
* 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(
initializer: Initializer
initializer: Initializer,
seed: ByteArray,
): Synchronizer {
val repository = DefaultSynchronizerFactory.defaultTransactionRepository(initializer)
val repository = DefaultSynchronizerFactory.defaultTransactionRepository(initializer, seed)
val blockStore = DefaultSynchronizerFactory.defaultBlockStore(initializer)
val service = DefaultSynchronizerFactory.defaultService(initializer)
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.
*/
@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.
*/
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 AlreadyInitializedException(cause: Throwable, dbPath: String) : InitializerException(
"Failed to initialize the blocks table" +

View File

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

View File

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

View File

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

View File

@ -17,7 +17,9 @@ use jni::{
JNIEnv,
};
use log::Level;
use schemer::MigratorError;
use secp256k1::PublicKey;
use secrecy::SecretVec;
use zcash_client_backend::keys::UnifiedSpendingKey;
use zcash_client_backend::{
address::RecipientAddress,
@ -36,6 +38,7 @@ use zcash_client_backend::{
keys::{sapling, UnifiedFullViewingKey},
wallet::{OvkPolicy, WalletTransparentOutput},
};
use zcash_client_sqlite::wallet::init::WalletMigrationError;
#[allow(deprecated)]
use zcash_client_sqlite::wallet::{delete_utxos_above, get_rewind_height};
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()
}
/// 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]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initDataDb(
env: JNIEnv<'_>,
_: JClass<'_>,
db_data: JString<'_>,
seed: jbyteArray,
network_id: jint,
) -> jboolean {
) -> jint {
let res = panic::catch_unwind(|| {
let network = parse_network(network_id as u32)?;
let db_path = utils::java_string_to_rust(&env, db_data);
let seed = None; // TODO Update
WalletDb::for_path(db_path, network)
.map_err(|e| format_err!("Error while opening data DB: {}", e))
.and_then(|mut db| {
init_wallet_db(&mut db, seed)
.map_err(|e| format_err!("Error while initializing data DB: {}", e))
})
.map(|()| JNI_TRUE)
let mut db_data = WalletDb::for_path(db_path, network)
.map_err(|e| format_err!("Error while opening data DB: {}", e))?;
let seed = (!seed.is_null()).then(|| SecretVec::new(env.convert_byte_array(seed).unwrap()));
match init_wallet_db(&mut db_data, seed) {
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]