Feature branch for SDK 2.1.0 (#1411)

* CompactBlockProcessor: Remove `withDownload` parameter

* backend-lib: Migrate to latest in-progress revision of Rust crates

Includes changes to how accounts are stored and referenced. We now
need to remember and provide the seed fingerprint; for now, given
that we know we only create derived accounts from a single seed, we
search for an account with a matching ZIP 32 account index.

* backend-lib: Add `Backend.isSeedRelevantToWallet`

* Remove nullability of DownloadSuccess param

* Comment update

* Fix Detekt warnings

* backend-lib: Migrate to latest in-progress revision of Rust crates

Includes some renames, and a built-in seed relevancy API that we now
use.

* Separate tree state fetching

- Added continuable retry logic

* Integrate Orchard support

Closes Electric-Coin-Company/zcash-android-wallet-sdk#528.
Closes Electric-Coin-Company/zcash-android-wallet-sdk#761.

* Detekt warnings fix

* Fix unit tests

* Update `TxOutputsView` to use correct column names. (#1425)

* Return an error instead of a panic in the case of data corruption. (#1426)

This removes an `expect` call that risked crashing the app in the case of
database corruption, potentially hiding other bugs.

* Include `orchardSubtreeRootList` in final check

* Revert `orchardSubtreeRootList` check

Explanation comment added

* Changelog update

* Update to zcash_client_sqlite version 0.10.3 (#1428)

---------

Co-authored-by: Honza <rychnovsky.honza@gmail.com>
Co-authored-by: Kris Nuttycombe <kris@electriccoin.co>
This commit is contained in:
str4d 2024-04-09 12:49:52 +01:00 committed by GitHub
parent 698b7fe92f
commit 52ee2bc5bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 662 additions and 212 deletions

View File

@ -6,6 +6,10 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- The Orchard support has been finished, and the SDK now fully supports sending and receiving funds on the Orchard
addresses
### Fixed
- SDK release 1.11.0-beta01 documented that `Synchronizer.new` would throw an
exception indicating that an internal migration requires the wallet seed, if
@ -14,9 +18,12 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
added. The SDK now correctly throws `InitializeException.SeedRequired`.
### Changed
- `Synchronizer.refreshAllBalances` now refreshes the Orchard balances as well
- The SDK uses ZIP-317 fee system internally
- `ZcashSdk.MINERS_FEE` has been deprecated, and will be removed in 2.1.0
- `ZcashSdk.MINERS_FEE` has been deprecated, and will be removed in 2.1.x
- `ZecSend` data class now provides `Proposal?` object initiated using `Synchronizer.proposeTransfer`
- Wallet initialization using `Synchronizer.new` now could throw a new `SeedNotRelevant` exception when the provided
seed is not relevant to any of the derived accounts in the wallet database
- Checkpoints update
## [2.0.7] - 2024-03-08
@ -34,7 +41,7 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `WalletSnapshot.transparentBalance: WalletBalance` to `WalletSnapshot.transparentBalance: Zatoshi`
- `Memo.MAX_MEMO_LENGTH_BYTES` is now available in public API
- `Synchronizer.sendToAddress` and `Synchronizer.shieldFunds` have been
deprecated, and will be removed in 2.1.0 (which will create multiple
deprecated, and will be removed in 2.1.x (which will create multiple
transactions at once for some recipients).
### Added
@ -96,7 +103,7 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed
- `LightWalletEndpointExt` and its functions and variables were removed from the SDK's public APIs entirely. It's
preserved only for testing and wallet Demo app purposes. The calling wallet app should provide its own
`LightWalletEndpoint` instance within `PersistableWallet` or `SdkSynchornizer` APIs.
`LightWalletEndpoint` instance within `PersistableWallet` or `SdkSynchronizer` APIs.
### Changed
- Gradle 8.5

56
backend-lib/Cargo.lock generated
View File

@ -730,9 +730,9 @@ dependencies = [
[[package]]
name = "incrementalmerkletree"
version = "0.5.0"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "361c467824d4d9d4f284be4b2608800839419dccc4d4608f28345237fe354623"
checksum = "eb1872810fb725b06b8c153dde9e86f3ec26747b9b60096da7a869883b549cbe"
dependencies = [
"either",
]
@ -1033,9 +1033,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "orchard"
version = "0.7.1"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fb255c3ffdccd3c84fe9ebed72aef64fdc72e6a3e4180dd411002d47abaad42"
checksum = "0462569fc8b0d1b158e4d640571867a4e4319225ebee2ab6647e60c70af19ae3"
dependencies = [
"aes",
"bitvec",
@ -1444,9 +1444,9 @@ dependencies = [
[[package]]
name = "sapling-crypto"
version = "0.1.1"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d183012062dfdde85f7e3e758328fcf6e9846d8dd3fce35b04d0efcb6677b0e0"
checksum = "02f4270033afcb0c74c5c7d59c73cfd1040367f67f224fe7ed9a919ae618f1b7"
dependencies = [
"aes",
"bellman",
@ -1566,9 +1566,9 @@ dependencies = [
[[package]]
name = "shardtree"
version = "0.2.0"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbf20c7a2747d9083092e3a3eeb9a7ed75577ae364896bebbc5e0bdcd4e97735"
checksum = "d766257c56a1bdd75479c256b97c92e72788a9afb18b5199f58faf7188dc99d9"
dependencies = [
"bitflags",
"either",
@ -2159,6 +2159,7 @@ dependencies = [
"jni",
"libc",
"log-panics",
"orchard",
"paranoid-android",
"prost",
"rayon",
@ -2191,9 +2192,9 @@ dependencies = [
[[package]]
name = "zcash_client_backend"
version = "0.11.0"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "185913267d824529b9547c933674963fca2b5bd84ad377a59d0f8ab6159ce798"
checksum = "0364e69c446fcf96a1f73f342c6c3fa697ea65ae7eeeae7d76ca847b9c442e40"
dependencies = [
"base64",
"bech32",
@ -2209,6 +2210,7 @@ dependencies = [
"memuse",
"nom",
"nonempty",
"orchard",
"percent-encoding",
"prost",
"rand_core",
@ -2226,14 +2228,15 @@ dependencies = [
"zcash_keys",
"zcash_note_encryption",
"zcash_primitives",
"zcash_protocol",
"zip32",
]
[[package]]
name = "zcash_client_sqlite"
version = "0.9.0"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e883405989b8d7275a0e1180000b7568bb3fa33e36b4806c174eb802678e2dbf"
checksum = "d2f94a5b293baca4b99be38695caa65a5e6981e8068f0bb80a69d60f4ee78346"
dependencies = [
"bs58",
"byteorder",
@ -2243,6 +2246,8 @@ dependencies = [
"incrementalmerkletree",
"jubjub",
"maybe-rayon",
"nonempty",
"orchard",
"prost",
"rusqlite",
"sapling-crypto",
@ -2250,6 +2255,7 @@ dependencies = [
"schemer-rusqlite",
"secrecy",
"shardtree",
"subtle",
"time",
"tracing",
"uuid",
@ -2258,6 +2264,8 @@ dependencies = [
"zcash_encoding",
"zcash_keys",
"zcash_primitives",
"zcash_protocol",
"zip32",
]
[[package]]
@ -2272,11 +2280,12 @@ dependencies = [
[[package]]
name = "zcash_keys"
version = "0.1.1"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f22d3407fdd6992b49f037f23862ab376be6013be6f2d0bc85948a635edc1f5"
checksum = "663489ffb4e51bc4436ff8796832612a9ff3c6516f1c620b5a840cb5dcd7b866"
dependencies = [
"bech32",
"blake2b_simd",
"bls12_381",
"bs58",
"byteorder",
@ -2288,11 +2297,13 @@ dependencies = [
"orchard",
"rand_core",
"sapling-crypto",
"secrecy",
"subtle",
"tracing",
"zcash_address",
"zcash_encoding",
"zcash_primitives",
"zcash_protocol",
"zip32",
]
@ -2311,9 +2322,9 @@ dependencies = [
[[package]]
name = "zcash_primitives"
version = "0.14.0"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9070e084570bb78aed4f8d71fd6254492e62c87a5d01e084183980e98117092d"
checksum = "b5a8d812efec385ecbcefc862c0005bb1336474ea7dd9b671d5bbddaadd04be2"
dependencies = [
"aes",
"bip0039",
@ -2343,15 +2354,16 @@ dependencies = [
"zcash_address",
"zcash_encoding",
"zcash_note_encryption",
"zcash_protocol",
"zcash_spec",
"zip32",
]
[[package]]
name = "zcash_proofs"
version = "0.14.0"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8a02eb1f151d9b9a6e16408d2c55ff440bd2fb232b7377277146d0fa2df9bc8"
checksum = "5163a1110f4265cc5f2fdf87ac4497fd1e014b6ce0760ca8d16d8e3853a5c0f7"
dependencies = [
"bellman",
"blake2b_simd",
@ -2372,9 +2384,9 @@ dependencies = [
[[package]]
name = "zcash_protocol"
version = "0.1.0"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c651f9f95d319cd1e5211178108dcd3aa73063806d09af54b799e1a329a575bf"
checksum = "8f8189d4a304e8aa3aef3b75e89f3874bb0dc84b1cd623316a84e79e06cddabc"
dependencies = [
"document-features",
"memuse",
@ -2431,9 +2443,9 @@ dependencies = [
[[package]]
name = "zip32"
version = "0.1.0"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d724a63be4dfb50b7f3617e542984e22e4b4a5b8ca5de91f55613152885e6b22"
checksum = "4226d0aee9c9407c27064dfeec9d7b281c917de3374e1e5a2e2cfad9e09de19e"
dependencies = [
"blake2b_simd",
"memuse",

View File

@ -16,6 +16,7 @@ hdwallet = "0.4"
hdwallet-bitcoin = "0.4"
hex = "0.4"
jni = { version = "0.21", default-features = false }
orchard = "0.8"
prost = "0.12"
rusqlite = "0.29"
sapling = { package = "sapling-crypto", version = "0.1", default-features = false }
@ -23,10 +24,10 @@ schemer = "0.2"
secp256k1 = "0.26"
secrecy = "0.8"
zcash_address = "0.3"
zcash_client_backend = { version = "0.11", features = ["transparent-inputs", "unstable"] }
zcash_client_sqlite = { version = "0.9", features = ["transparent-inputs", "unstable"] }
zcash_primitives = "0.14"
zcash_proofs = "0.14"
zcash_client_backend = { version = "0.12.1", features = ["orchard", "transparent-inputs", "unstable"] }
zcash_client_sqlite = { version = "0.10.3", features = ["orchard", "transparent-inputs", "unstable"] }
zcash_primitives = "0.15"
zcash_proofs = "0.15"
# Initialization
rayon = "1.7"

View File

@ -48,7 +48,8 @@ interface Backend {
* If `seed` is `null`, database migrations will be attempted without it.
*
* @return 0 if successful, 1 if the seed must be provided in order to execute the
* requested migrations.
* requested migrations, 2 if the provided seed is not relevant to any of the
* derived accounts in the wallet.
*
* @throws RuntimeException as a common indicator of the operation failure
*/
@ -65,6 +66,12 @@ interface Backend {
recoverUntil: Long?
): JniUnifiedSpendingKey
/**
* @throws RuntimeException as a common indicator of the operation failure
*/
@Throws(RuntimeException::class)
suspend fun isSeedRelevantToAnyDerivedAccounts(seed: ByteArray): Boolean
fun isValidSaplingAddr(addr: String): Boolean
fun isValidTransparentAddr(addr: String): Boolean
@ -102,9 +109,11 @@ interface Backend {
* @throws RuntimeException as a common indicator of the operation failure
*/
@Throws(RuntimeException::class)
suspend fun putSaplingSubtreeRoots(
startIndex: Long,
roots: List<JniSubtreeRoot>,
suspend fun putSubtreeRoots(
saplingStartIndex: Long,
saplingRoots: List<JniSubtreeRoot>,
orchardStartIndex: Long,
orchardRoots: List<JniSubtreeRoot>,
)
/**
@ -154,6 +163,7 @@ interface Backend {
@Throws(RuntimeException::class)
suspend fun scanBlocks(
fromHeight: Long,
fromState: ByteArray,
limit: Long
): JniScanSummary

View File

@ -91,6 +91,15 @@ class RustBackend private constructor(
}
}
override suspend fun isSeedRelevantToAnyDerivedAccounts(seed: ByteArray): Boolean =
withContext(SdkDispatchers.DATABASE_IO) {
isSeedRelevantToAnyDerivedAccounts(
dataDbFile.absolutePath,
seed,
networkId = networkId
)
}
override suspend fun getCurrentAddress(account: Int) =
withContext(SdkDispatchers.DATABASE_IO) {
getCurrentAddress(
@ -193,14 +202,18 @@ class RustBackend private constructor(
)
}
override suspend fun putSaplingSubtreeRoots(
startIndex: Long,
roots: List<JniSubtreeRoot>,
override suspend fun putSubtreeRoots(
saplingStartIndex: Long,
saplingRoots: List<JniSubtreeRoot>,
orchardStartIndex: Long,
orchardRoots: List<JniSubtreeRoot>,
) = withContext(SdkDispatchers.DATABASE_IO) {
putSaplingSubtreeRoots(
putSubtreeRoots(
dataDbFile.absolutePath,
startIndex,
roots.toTypedArray(),
saplingStartIndex,
saplingRoots.toTypedArray(),
orchardStartIndex,
orchardRoots.toTypedArray(),
networkId = networkId
)
}
@ -263,6 +276,7 @@ class RustBackend private constructor(
override suspend fun scanBlocks(
fromHeight: Long,
fromState: ByteArray,
limit: Long
): JniScanSummary {
return withContext(SdkDispatchers.DATABASE_IO) {
@ -270,6 +284,7 @@ class RustBackend private constructor(
fsBlockDbRoot.absolutePath,
dataDbFile.absolutePath,
fromHeight,
fromState,
limit,
networkId = networkId
)
@ -432,6 +447,13 @@ class RustBackend private constructor(
networkId: Int
): JniUnifiedSpendingKey
@JvmStatic
private external fun isSeedRelevantToAnyDerivedAccounts(
dbDataPath: String,
seed: ByteArray,
networkId: Int
): Boolean
@JvmStatic
private external fun getCurrentAddress(
dbDataPath: String,
@ -519,10 +541,13 @@ class RustBackend private constructor(
)
@JvmStatic
private external fun putSaplingSubtreeRoots(
@Suppress("LongParameterList")
private external fun putSubtreeRoots(
dbDataPath: String,
startIndex: Long,
roots: Array<JniSubtreeRoot>,
saplingStartIndex: Long,
saplingRoots: Array<JniSubtreeRoot>,
orchardStartIndex: Long,
orchardRoots: Array<JniSubtreeRoot>,
networkId: Int
)
@ -558,10 +583,12 @@ class RustBackend private constructor(
): Array<JniScanRange>
@JvmStatic
@Suppress("LongParameterList")
private external fun scanBlocks(
dbCachePath: String,
dbDataPath: String,
fromHeight: Long,
fromState: ByteArray,
limit: Long,
networkId: Int
): JniScanSummary

View File

@ -14,11 +14,14 @@ import cash.z.ecc.android.sdk.internal.ext.isInUIntRange
* @param progressDenominator the denominator of the progress ratio
* @param nextSaplingSubtreeIndex the Sapling subtree index that should start
* the next range of subtree roots passed to `Backend.putSaplingSubtreeRoots`.
* @param nextOrchardSubtreeIndex the Orchard subtree index that should start
* the next range of subtree roots passed to `Backend.putOrchardSubtreeRoots`.
* @throws IllegalArgumentException unless (progressNumerator is nonnegative,
* progressDenominator is positive, and the represented ratio is in the
* range 0.0 to 1.0 inclusive).
*/
@Keep
@Suppress("LongParameterList")
class JniWalletSummary(
val accountBalances: Array<JniAccountBalance>,
val chainTipHeight: Long,
@ -26,6 +29,7 @@ class JniWalletSummary(
val progressNumerator: Long,
val progressDenominator: Long,
val nextSaplingSubtreeIndex: Long,
val nextOrchardSubtreeIndex: Long,
) {
init {
require(chainTipHeight.isInUIntRange()) {
@ -49,5 +53,8 @@ class JniWalletSummary(
require(nextSaplingSubtreeIndex.isInUIntRange()) {
"Height $nextSaplingSubtreeIndex is outside of allowed UInt range"
}
require(nextOrchardSubtreeIndex.isInUIntRange()) {
"Height $nextOrchardSubtreeIndex is outside of allowed UInt range"
}
}
}

View File

@ -1,4 +1,5 @@
use std::convert::{Infallible, TryFrom, TryInto};
use std::error::Error;
use std::num::NonZeroU32;
use std::panic;
use std::path::Path;
@ -12,7 +13,6 @@ use jni::{
JNIEnv,
};
use prost::Message;
use schemer::MigratorError;
use secrecy::{ExposeSecret, SecretVec};
use tracing::{debug, error};
use tracing_subscriber::prelude::*;
@ -27,8 +27,8 @@ use zcash_client_backend::{
create_proposed_transactions, decrypt_and_store_transaction,
input_selection::GreedyInputSelector, propose_shielding, propose_transfer,
},
AccountBalance, AccountBirthday, InputSource, WalletCommitmentTrees, WalletRead,
WalletSummary, WalletWrite,
Account, AccountBalance, AccountBirthday, AccountSource, InputSource, SeedRelevance,
WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite,
},
encoding::AddressCodec,
fees::{standard::SingleOutputChangeStrategy, DustOutputPolicy},
@ -41,7 +41,7 @@ use zcash_client_backend::{
use zcash_client_sqlite::{
chain::{init::init_blockmeta_db, BlockMeta},
wallet::init::{init_wallet_db, WalletMigrationError},
FsBlockDb, WalletDb,
AccountId, FsBlockDb, WalletDb,
};
use zcash_primitives::{
block::BlockHash,
@ -58,7 +58,7 @@ use zcash_primitives::{
fees::StandardFeeRule,
Transaction, TxId,
},
zip32::{AccountId, DiversifierIndex},
zip32::{self, DiversifierIndex},
};
use zcash_proofs::prover::LocalTxProver;
@ -71,7 +71,7 @@ const ANCHOR_OFFSET: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(ANCHOR_OFFS
// Do not generate Orchard receivers until we support receiving Orchard funds.
const DEFAULT_ADDRESS_REQUEST: UnifiedAddressRequest =
UnifiedAddressRequest::unsafe_new(false, true, true);
UnifiedAddressRequest::unsafe_new(true, true, true);
#[cfg(debug_assertions)]
fn print_debug_state() {
@ -97,13 +97,54 @@ fn block_db(env: &mut JNIEnv, fsblockdb_root: JString) -> Result<FsBlockDb, fail
.map_err(|e| format_err!("Error opening block source database connection: {:?}", e))
}
fn account_id_from_jint(account: jint) -> Result<AccountId, failure::Error> {
fn account_id_from_jint(account: jint) -> Result<zip32::AccountId, failure::Error> {
u32::try_from(account)
.map_err(|_| ())
.and_then(|id| AccountId::try_from(id).map_err(|_| ()))
.and_then(|id| zip32::AccountId::try_from(id).map_err(|_| ()))
.map_err(|_| format_err!("Invalid account ID"))
}
fn account_id_from_jni<'local, P: Parameters>(
db_data: &WalletDb<rusqlite::Connection, P>,
account_index: jint,
) -> Result<AccountId, failure::Error> {
let requested_account_index = account_id_from_jint(account_index)?;
// Find the single account matching the given ZIP 32 account index.
let mut accounts = db_data
.get_account_ids()?
.into_iter()
.filter_map(|account_id| {
db_data
.get_account(account_id)
.map_err(|e| {
format_err!(
"Database error encountered retrieving account {:?}: {}",
account_id,
e
)
})
.and_then(|acct_opt|
acct_opt.ok_or_else(||
format_err!(
"Wallet data corrupted: unable to retrieve account data for account {:?}",
account_id
)
).map(|account| match account.source() {
AccountSource::Derived { account_index, .. } if account_index == requested_account_index => Some(account),
_ => None
})
)
.transpose()
});
match (accounts.next(), accounts.next()) {
(Some(account), None) => Ok(account?.id()),
(None, None) => Err(format_err!("Account does not exist")),
(_, Some(_)) => Err(format_err!("Account index matches more than one account")),
}
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_initOnLoad<'local>(
_env: JNIEnv<'local>,
@ -173,8 +214,10 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_initBlock
///
/// 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.
/// Returns:
/// - 0 if successful.
/// - 1 if the seed must be provided in order to execute the requested migrations.
/// - 2 if the provided seed is not relevant to any of the derived accounts in the wallet.
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_initDataDb<'local>(
mut env: JNIEnv<'local>,
@ -192,11 +235,22 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_initDataD
match init_wallet_db(&mut db_data, seed) {
Ok(()) => Ok(0),
Err(MigratorError::Migration { error, .. })
if matches!(error, WalletMigrationError::SeedRequired) =>
Err(e)
if matches!(
e.source().and_then(|e| e.downcast_ref()),
Some(&WalletMigrationError::SeedRequired)
) =>
{
Ok(1)
}
Err(e)
if matches!(
e.source().and_then(|e| e.downcast_ref()),
Some(&WalletMigrationError::SeedNotRelevant)
) =>
{
Ok(2)
}
Err(e) => Err(format_err!("Error while initializing data DB: {}", e)),
}
});
@ -205,7 +259,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_initDataD
fn encode_usk<'a>(
env: &mut JNIEnv<'a>,
account: AccountId,
account: zip32::AccountId,
usk: UnifiedSpendingKey,
) -> jni::errors::Result<JObject<'a>> {
let encoded = SecretVec::new(usk.to_bytes(Era::Orchard));
@ -280,15 +334,46 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_createAcc
}
})?;
let (account, usk) = db_data
.create_account(&seed, birthday)
let (account_id, usk) = db_data
.create_account(&seed, &birthday)
.map_err(|e| format_err!("Error while initializing accounts: {}", e))?;
Ok(encode_usk(env, account, usk)?.into_raw())
let account = db_data.get_account(account_id)?.expect("just created");
let account_index = match account.source() {
AccountSource::Derived { account_index, .. } => account_index,
AccountSource::Imported => unreachable!("just created"),
};
Ok(encode_usk(env, account_index, usk)?.into_raw())
});
unwrap_exc_or(&mut env, res, ptr::null_mut())
}
/// Checks whether the given seed is relevant to any of the derived accounts in the wallet.
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_isSeedRelevantToAnyDerivedAccounts<
'local,
>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
db_data: JString<'local>,
seed: JByteArray<'local>,
network_id: jint,
) -> jboolean {
let res = catch_unwind(&mut env, |env| {
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());
// Replicate the logic from `initWalletDb`.
Ok(match db_data.seed_relevance_to_derived_accounts(&seed)? {
SeedRelevance::Relevant { .. } | SeedRelevance::NoAccounts => JNI_TRUE,
SeedRelevance::NotRelevant | SeedRelevance::NoDerivedAccounts => JNI_FALSE,
})
});
unwrap_exc_or(&mut env, res, JNI_FALSE)
}
/// Derives and returns a unified spending key from the given seed for the given account ID.
///
/// Returns the newly created [ZIP 316] account identifier, along with the binary encoding
@ -341,8 +426,8 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustDerivationTool_de
let ufvks: Vec<_> = (0..accounts)
.map(|account| {
let account_id =
AccountId::try_from(account).map_err(|_| format_err!("Invalid account ID"))?;
let account_id = zip32::AccountId::try_from(account)
.map_err(|_| format_err!("Invalid account ID"))?;
UnifiedSpendingKey::from_seed(&network, &seed, account_id)
.map_err(|e| {
format_err!("error generating unified spending key from seed: {:?}", e)
@ -422,7 +507,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustDerivationTool_de
// Derive the default Unified Address (containing the default Sapling payment
// address that older SDKs used).
let (ua, _) = ufvk.default_address(DEFAULT_ADDRESS_REQUEST);
let (ua, _) = ufvk.default_address(DEFAULT_ADDRESS_REQUEST)?;
let address_str = ua.encode(&network);
let output = env
.new_string(address_str)
@ -469,9 +554,9 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getCurren
let _span = tracing::info_span!("RustBackend.getCurrentAddress").entered();
let network = parse_network(network_id as u32)?;
let db_data = wallet_db(env, network, db_data)?;
let account = account_id_from_jint(account)?;
let account = account_id_from_jni(&db_data, account)?;
match (&db_data).get_current_address(account) {
match db_data.get_current_address(account) {
Ok(Some(addr)) => {
let addr_str = addr.encode(&network);
let output = env
@ -961,10 +1046,11 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_rewindToH
unwrap_exc_or(&mut env, res, JNI_FALSE)
}
fn decode_sapling_subtree_root(
fn decode_subtree_root<H>(
env: &mut JNIEnv,
obj: JObject,
) -> Result<CommitmentTreeRoot<sapling::Node>, failure::Error> {
node_parser: impl FnOnce(&[u8]) -> std::io::Result<H>,
) -> Result<CommitmentTreeRoot<H>, failure::Error> {
fn long_as_u32(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<u32, failure::Error> {
Ok(u32::try_from(env.get_field(obj, name, "J")?.j()?)?)
}
@ -976,48 +1062,68 @@ fn decode_sapling_subtree_root(
Ok(CommitmentTreeRoot::from_parts(
BlockHeight::from_u32(long_as_u32(env, &obj, "completingBlockHeight")?),
sapling::Node::read(&byte_array(env, &obj, "rootHash")?[..])?,
node_parser(&byte_array(env, &obj, "rootHash")?[..])?,
))
}
#[no_mangle]
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_putSaplingSubtreeRoots<
'local,
>(
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_putSubtreeRoots<'local>(
mut env: JNIEnv<'local>,
_: JClass<'local>,
db_data: JString<'local>,
start_index: jlong,
roots: JObjectArray<'local>,
sapling_start_index: jlong,
sapling_roots: JObjectArray<'local>,
orchard_start_index: jlong,
orchard_roots: JObjectArray<'local>,
network_id: jint,
) -> jboolean {
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustBackend.putSaplingSubtreeRoots").entered();
let _span = tracing::info_span!("RustBackend.putSubtreeRoots").entered();
let network = parse_network(network_id as u32)?;
let mut db_data = wallet_db(env, network, db_data)?;
let start_index = if start_index >= 0 {
start_index as u64
} else {
return Err(format_err!("Start index must be nonnegative."));
};
let roots = {
fn parse_roots<H>(
env: &mut JNIEnv,
roots: JObjectArray,
node_parser: impl Fn(&[u8]) -> std::io::Result<H>,
) -> Result<Vec<CommitmentTreeRoot<H>>, failure::Error> {
let count = env.get_array_length(&roots).unwrap();
(0..count)
.scan(env, |env, i| {
Some(
env.get_object_array_element(&roots, i)
.map_err(|e| e.into())
.and_then(|jobj| decode_sapling_subtree_root(env, jobj)),
.and_then(|jobj| decode_subtree_root(env, jobj, &node_parser)),
)
})
.collect::<Result<Vec<_>, _>>()?
.collect::<Result<Vec<_>, _>>()
}
let sapling_start_index = if sapling_start_index >= 0 {
sapling_start_index as u64
} else {
return Err(format_err!("Sapling start index must be nonnegative."));
};
let sapling_roots = parse_roots(env, sapling_roots, |n| sapling::Node::read(n))?;
let orchard_start_index = if orchard_start_index >= 0 {
orchard_start_index as u64
} else {
return Err(format_err!("Orchard start index must be nonnegative."));
};
let orchard_roots = parse_roots(env, orchard_roots, |n| {
orchard::tree::MerkleHashOrchard::read(n)
})?;
db_data
.put_sapling_subtree_roots(start_index, &roots)
.map(|()| JNI_TRUE)
.map_err(|e| format_err!("Error while storing Sapling subtree roots: {}", e))
.put_sapling_subtree_roots(sapling_start_index, &sapling_roots)
.map_err(|e| format_err!("Error while storing Sapling subtree roots: {}", e))?;
db_data
.put_orchard_subtree_roots(orchard_start_index, &orchard_roots)
.map_err(|e| format_err!("Error while storing Orchard subtree roots: {}", e))?;
Ok(JNI_TRUE)
});
unwrap_exc_or(&mut env, res, JNI_FALSE)
@ -1104,7 +1210,7 @@ const JNI_ACCOUNT_BALANCE: &str = "cash/z/ecc/android/sdk/internal/model/JniAcco
fn encode_account_balance<'a>(
env: &mut JNIEnv<'a>,
account: &AccountId,
account: &zip32::AccountId,
balance: &AccountBalance,
) -> jni::errors::Result<JObject<'a>> {
let sapling_verified_balance = Amount::from(balance.sapling_balance().spendable_value());
@ -1143,16 +1249,31 @@ fn encode_account_balance<'a>(
///
/// If these conditions are not met, this fails and leaves an `IllegalArgumentException`
/// pending.
fn encode_wallet_summary<'a>(
fn encode_wallet_summary<'a, P: Parameters>(
env: &mut JNIEnv<'a>,
db_data: &WalletDb<rusqlite::Connection, P>,
summary: WalletSummary<AccountId>,
) -> jni::errors::Result<JObject<'a>> {
) -> Result<JObject<'a>, failure::Error> {
let account_balances = utils::rust_vec_to_java(
env,
summary.account_balances().into_iter().collect(),
summary
.account_balances()
.into_iter()
.map(|(account_id, balance)| {
let account_index = match db_data
.get_account(*account_id)?
.expect("the account exists in the wallet")
.source()
{
AccountSource::Derived { account_index, .. } => account_index,
AccountSource::Imported => unreachable!("Imported accounts are unimplemented"),
};
Ok::<_, failure::Error>((account_index, balance))
})
.collect::<Result<_, _>>()?,
JNI_ACCOUNT_BALANCE,
|env, (account, balance)| encode_account_balance(env, account, balance),
|env| encode_account_balance(env, &AccountId::ZERO, &AccountBalance::ZERO),
|env, (account_index, balance)| encode_account_balance(env, &account_index, balance),
|env| encode_account_balance(env, &zip32::AccountId::ZERO, &AccountBalance::ZERO),
)?;
let (progress_numerator, progress_denominator) = summary
@ -1160,9 +1281,9 @@ fn encode_wallet_summary<'a>(
.map(|progress| (*progress.numerator(), *progress.denominator()))
.unwrap_or((0, 1));
env.new_object(
Ok(env.new_object(
"cash/z/ecc/android/sdk/internal/model/JniWalletSummary",
&format!("([L{};JJJJJ)V", JNI_ACCOUNT_BALANCE),
&format!("([L{};JJJJJJ)V", JNI_ACCOUNT_BALANCE),
&[
(&account_balances).into(),
JValue::Long(i64::from(u32::from(summary.chain_tip_height()))),
@ -1170,8 +1291,9 @@ fn encode_wallet_summary<'a>(
JValue::Long(progress_numerator as i64),
JValue::Long(progress_denominator as i64),
JValue::Long(summary.next_sapling_subtree_index() as i64),
JValue::Long(summary.next_orchard_subtree_index() as i64),
],
)
)?)
}
#[no_mangle]
@ -1195,7 +1317,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getWallet
.map(|r| r.denominator() > &0)
.unwrap_or(false)
}) {
Some(summary) => Ok(encode_wallet_summary(env, summary)?.into_raw()),
Some(summary) => Ok(encode_wallet_summary(env, &db_data, summary)?.into_raw()),
None => Ok(ptr::null_mut()),
}
});
@ -1283,6 +1405,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_scanBlock
db_cache: JString<'local>,
db_data: JString<'local>,
from_height: jlong,
from_state: JByteArray<'local>,
limit: jlong,
network_id: jint,
) -> jobject {
@ -1292,9 +1415,19 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_scanBlock
let db_cache = block_db(env, db_cache)?;
let mut db_data = wallet_db(env, network, db_data)?;
let from_height = BlockHeight::try_from(from_height)?;
let from_state = TreeState::decode(&env.convert_byte_array(from_state).unwrap()[..])
.map_err(|e| format_err!("Invalid TreeState: {}", e))?
.to_chain_state()?;
let limit = usize::try_from(limit)?;
match scan_cached_blocks(&network, &db_cache, &mut db_data, from_height, limit) {
match scan_cached_blocks(
&network,
&db_cache,
&mut db_data,
from_height,
&from_state,
limit,
) {
Ok(scan_summary) => Ok(encode_scan_summary(env, scan_summary)?.into_raw()),
Err(e) => Err(format_err!(
"Rust error while scanning blocks (limit {:?}): {}",
@ -1397,7 +1530,7 @@ fn zip317_helper<DbT>(
StandardFeeRule::PreZip313
};
GreedyInputSelector::new(
SingleOutputChangeStrategy::new(fee_rule, change_memo, ShieldedProtocol::Sapling),
SingleOutputChangeStrategy::new(fee_rule, change_memo, ShieldedProtocol::Orchard),
DustOutputPolicy::default(),
)
}
@ -1418,10 +1551,10 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_proposeTr
let _span = tracing::info_span!("RustBackend.proposeTransfer").entered();
let network = parse_network(network_id as u32)?;
let mut db_data = wallet_db(env, network, db_data)?;
let account = account_id_from_jint(account)?;
let account = account_id_from_jni(&db_data, account)?;
let to = utils::java_string_to_rust(env, &to);
let value = NonNegativeAmount::from_nonnegative_i64(value)
.map_err(|()| format_err!("Invalid amount, out of range"))?;
.map_err(|_| format_err!("Invalid amount, out of range"))?;
let memo_bytes = env.convert_byte_array(memo).unwrap();
let to = match Address::decode(&network, &to) {
@ -1490,9 +1623,9 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_proposeSh
let _span = tracing::info_span!("RustBackend.proposeShielding").entered();
let network = parse_network(network_id as u32)?;
let mut db_data = wallet_db(env, network, db_data)?;
let account = account_id_from_jint(account)?;
let account = account_id_from_jni(&db_data, account)?;
let shielding_threshold = NonNegativeAmount::from_nonnegative_i64(shielding_threshold)
.map_err(|()| format_err!("Invalid shielding threshold, out of range"))?;
.map_err(|_| format_err!("Invalid shielding threshold, out of range"))?;
let memo_bytes = env.convert_byte_array(memo).unwrap();
let transparent_receiver =
@ -1512,8 +1645,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_proposeSh
Ok(Some(addr))
} else {
Err(format_err!(
"Transparent receiver does not belong to account {}",
u32::from(account),
"Transparent receiver does not belong to account",
))
}
}
@ -1694,11 +1826,9 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_listTrans
let res = catch_unwind(&mut env, |env| {
let _span = tracing::info_span!("RustBackend.listTransparentReceivers").entered();
let network = parse_network(network_id as u32)?;
let zcash_network = network
.address_network()
.expect("network_id parsing should not have resulted in an unrecognized network type.");
let zcash_network = network.network_type();
let db_data = wallet_db(env, network, db_data)?;
let account = account_id_from_jint(account_id)?;
let account = account_id_from_jni(&db_data, account_id)?;
match db_data.get_transparent_receivers(account) {
Ok(receivers) => {

View File

@ -15,7 +15,8 @@ class JniWalletSummaryTest {
fullyScannedHeight = 0,
progressNumerator = 1L,
progressDenominator = 100L,
nextSaplingSubtreeIndex = 0
nextSaplingSubtreeIndex = 0L,
nextOrchardSubtreeIndex = 0L
)
assertIs<JniWalletSummary>(instance)
}
@ -29,7 +30,8 @@ class JniWalletSummaryTest {
fullyScannedHeight = 0,
progressNumerator = 1L,
progressDenominator = 100L,
nextSaplingSubtreeIndex = 0
nextSaplingSubtreeIndex = 0L,
nextOrchardSubtreeIndex = 0
)
}
}
@ -43,7 +45,8 @@ class JniWalletSummaryTest {
fullyScannedHeight = 0,
progressNumerator = -1L,
progressDenominator = 100L,
nextSaplingSubtreeIndex = 0
nextSaplingSubtreeIndex = 0L,
nextOrchardSubtreeIndex = 0L
)
}
}
@ -57,7 +60,8 @@ class JniWalletSummaryTest {
fullyScannedHeight = 0,
progressNumerator = 1L,
progressDenominator = 0L,
nextSaplingSubtreeIndex = 0
nextSaplingSubtreeIndex = 0L,
nextOrchardSubtreeIndex = 0L
)
}
}
@ -71,7 +75,8 @@ class JniWalletSummaryTest {
fullyScannedHeight = 0,
progressNumerator = 100L,
progressDenominator = 1L,
nextSaplingSubtreeIndex = 0
nextSaplingSubtreeIndex = 0L,
nextOrchardSubtreeIndex = 0L
)
}
}
@ -85,7 +90,8 @@ class JniWalletSummaryTest {
fullyScannedHeight = 0,
progressNumerator = 1L,
progressDenominator = 100L,
nextSaplingSubtreeIndex = -1
nextSaplingSubtreeIndex = -1L,
nextOrchardSubtreeIndex = -1L
)
}
}

View File

@ -13,6 +13,7 @@ import co.electriccoin.lightwallet.client.model.Response
import co.electriccoin.lightwallet.client.model.SendResponseUnsafe
import co.electriccoin.lightwallet.client.model.ShieldedProtocolEnum
import co.electriccoin.lightwallet.client.model.SubtreeRootUnsafe
import co.electriccoin.lightwallet.client.model.TreeStateUnsafe
import kotlinx.coroutines.flow.Flow
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
@ -20,6 +21,7 @@ import kotlin.time.Duration.Companion.seconds
/**
* Client for interacting with lightwalletd.
*/
@Suppress("TooManyFunctions")
interface LightWalletClient {
/**
* @return the full transaction info.
@ -90,6 +92,11 @@ interface LightWalletClient {
maxEntries: UInt
): Flow<Response<SubtreeRootUnsafe>>
/**
* @return the latest block height known to the service.
*/
suspend fun getTreeState(height: BlockHeightUnsafe): Response<TreeStateUnsafe>
/**
* Reconnect to the same or a different server. This is useful when the connection is
* unrecoverable. That might be time to switch to a mirror or just reconnect.

View File

@ -15,6 +15,7 @@ import co.electriccoin.lightwallet.client.model.Response
import co.electriccoin.lightwallet.client.model.SendResponseUnsafe
import co.electriccoin.lightwallet.client.model.ShieldedProtocolEnum
import co.electriccoin.lightwallet.client.model.SubtreeRootUnsafe
import co.electriccoin.lightwallet.client.model.TreeStateUnsafe
import com.google.protobuf.ByteString
import io.grpc.CallOptions
import io.grpc.Channel
@ -87,7 +88,6 @@ internal class LightWalletClientImpl private constructor(
}
}
@Suppress("SwallowedException")
override suspend fun getServerInfo(): Response<LightWalletEndpointInfoUnsafe> {
return try {
val lightdInfo =
@ -233,6 +233,18 @@ internal class LightWalletClientImpl private constructor(
}
}
override suspend fun getTreeState(height: BlockHeightUnsafe): Response<TreeStateUnsafe> {
return try {
val response =
requireChannel().createStub(singleRequestTimeout)
.getTreeState(height.toBlockHeight())
Response.Success(TreeStateUnsafe.new(response))
} catch (e: StatusException) {
GrpcStatusResolver.resolveFailureFromStatus(e)
}
}
override fun shutdown() {
channel.shutdown()
}

View File

@ -14,14 +14,16 @@ class TreeStateUnsafe(
height: Long,
hash: String,
time: Int,
tree: String
saplingTree: String,
orchardTree: String
): TreeStateUnsafe {
val treeState =
TreeState.newBuilder()
.setHeight(height)
.setHash(hash)
.setTime(time)
.setSaplingTree(tree)
.setSaplingTree(saplingTree)
.setOrchardTree(orchardTree)
.build()
return new(treeState)
}

View File

@ -8,10 +8,17 @@ import cash.z.ecc.android.sdk.model.ZcashNetwork
object WalletFixture {
val NETWORK = ZcashNetwork.Mainnet
// This is the "Ben" wallet phrase from sdk-incubator-lib.
const val SEED_PHRASE =
"kitchen renew wide common vague fold vacuum tilt amazing pear square gossip jewel month" +
" tree shock scan alpha just spot fluid toilet view dinner"
// This is the "Alice" wallet phrase from sdk-incubator-lib.
const val ALICE_SEED_PHRASE =
"wish puppy smile loan doll curve hole maze file ginger hair nose key relax knife witness" +
" cannon grab despair throw review deal slush frame"
suspend fun getUnifiedSpendingKey(
seed: String = SEED_PHRASE,
network: ZcashNetwork = NETWORK,

View File

@ -5,7 +5,8 @@ import cash.z.ecc.android.sdk.internal.model.Checkpoint
import cash.z.ecc.android.sdk.internal.model.ext.KEY_EPOCH_SECONDS
import cash.z.ecc.android.sdk.internal.model.ext.KEY_HASH
import cash.z.ecc.android.sdk.internal.model.ext.KEY_HEIGHT
import cash.z.ecc.android.sdk.internal.model.ext.KEY_TREE
import cash.z.ecc.android.sdk.internal.model.ext.KEY_ORCHARD_TREE
import cash.z.ecc.android.sdk.internal.model.ext.KEY_SAPLING_TREE
import cash.z.ecc.android.sdk.internal.model.ext.KEY_VERSION
import cash.z.ecc.android.sdk.internal.model.ext.VERSION_1
import cash.z.ecc.android.sdk.internal.model.ext.from
@ -45,7 +46,8 @@ class CheckpointTest {
put(Checkpoint.KEY_HEIGHT, UInt.MAX_VALUE.toLong())
put(Checkpoint.KEY_HASH, CheckpointFixture.HASH)
put(Checkpoint.KEY_EPOCH_SECONDS, CheckpointFixture.EPOCH_SECONDS)
put(Checkpoint.KEY_TREE, CheckpointFixture.TREE)
put(Checkpoint.KEY_SAPLING_TREE, CheckpointFixture.SAPLING_TREE)
put(Checkpoint.KEY_ORCHARD_TREE, CheckpointFixture.ORCHARD_TREE)
}.toString()
Checkpoint.from(CheckpointFixture.NETWORK, jsonString).also {

View File

@ -5,12 +5,15 @@ import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.WalletInitMode
import cash.z.ecc.android.sdk.exception.InitializeException
import cash.z.ecc.android.sdk.fixture.LightWalletEndpointFixture
import cash.z.ecc.android.sdk.fixture.WalletFixture
import cash.z.ecc.android.sdk.model.Account
import cash.z.ecc.android.sdk.model.ZcashNetwork
import kotlinx.coroutines.test.runTest
import java.util.UUID
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class SdkSynchronizerTest {
@ -82,4 +85,45 @@ class SdkSynchronizerTest {
walletInitMode = WalletInitMode.ExistingWallet
).use {}
}
@Test
@SmallTest
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
fun detects_irrelevant_seeds() =
runTest {
// Random alias so that repeated invocations of this test will have a clean starting state
val alias = UUID.randomUUID().toString()
// TODO [#1094]: Consider fake SDK sync related components
// TODO [#1094]: https://github.com/zcash/zcash-android-wallet-sdk/issues/1094
// In the future, inject fake networking component so that it doesn't require hitting the network
Synchronizer.new(
InstrumentationRegistry.getInstrumentation().context,
ZcashNetwork.Mainnet,
alias,
LightWalletEndpointFixture.newEndpointForNetwork(ZcashNetwork.Mainnet),
Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy(),
birthday = null,
// Using existing wallet init mode as simplification for the test
walletInitMode = WalletInitMode.ExistingWallet
).use {
it.getSaplingAddress(Account.DEFAULT)
}
// Second instance should fail because the seed is not relevant to the wallet.
val error =
assertFailsWith<InitializeException> {
Synchronizer.new(
InstrumentationRegistry.getInstrumentation().context,
ZcashNetwork.Mainnet,
alias,
LightWalletEndpointFixture.newEndpointForNetwork(ZcashNetwork.Mainnet),
Mnemonics.MnemonicCode(WalletFixture.ALICE_SEED_PHRASE).toEntropy(),
birthday = null,
// Using existing wallet init mode as simplification for the test
walletInitMode = WalletInitMode.ExistingWallet
).use {}
}
assertEquals(InitializeException.SeedNotRelevant, error)
}
}

View File

@ -53,7 +53,7 @@ class TransparentTest(val expected: Expected, val network: ZcashNetwork) {
object ExpectedMainnet : Expected {
override val tAddr = "t1PKtYdJJHhc3Pxowmznkg7vdTwnhEsCvR4"
override val zAddr = "zs1yc4sgtfwwzz6xfsy2xsradzr6m4aypgxhfw2vcn3hatrh5ryqsr08sgpemlg39vdh9kfupx20py"
override val uAddr = "u1607xqhx72u8x94xcg6kyt9sd83aw8zvys2vwlr5n956e5jfytcaaeuzrk938c03jv4t0kdk73yxz9yd8rdksutw68ycpy6yt9vzhu28z58rh89gtt653cspr0c50ev4av0ddzj5vrrh"
override val uAddr = "u1t23erzgkn7c6c2jn66rspl4m45lg8rn3f7mn7le4yxk7693wr7sgx472jn95s00x8kx3hct5ej4tf76k59dfhsd809t7mzt9ldzw8f5083fw4xqvxfshl9u7ed2wyv6ypmzny0px0nvszslr5kr7fgk2zgfnlycddzqak4adsqjdzp76y7fl0k4ygamjr43t6rpxsf6xql8g20rdk0h"
override val tAccountPrivKey = "xprv9z1aorRbyM5A6ok9QmdCUztMRRgthiNpus4u8Rgn9YeZEz1EVkLthFpJS1Y1FaXAvgNDPKTwxvshUMj7KJiGeNVhKL8RzDv14yHbUu3szy5"
override val tskCompressed = "L4BvDC33yLjMRxipZvdiUmdYeRfZmR8viziwsVwe72zJdGbiJPv2"
override val tpk = "03b1d7fb28d17c125b504d06b1530097e0a3c76ada184237e3bc0925041230a5af"
@ -63,7 +63,7 @@ class TransparentTest(val expected: Expected, val network: ZcashNetwork) {
object ExpectedTestnet : Expected {
override val tAddr = "tm9v3KTsjXK8XWSqiwFjic6Vda6eHY9Mjjq"
override val zAddr = "ztestsapling1wn3tw9w5rs55x5yl586gtk72e8hcfdq8zsnjzcu8p7ghm8lrx54axc74mvm335q7lmy3g0sqje6"
override val uAddr = "utest1cy80kzr6fj5vrrazldtcgmycs6rgu2x73pvwrjjmlwrwx343m06lxua5u36jdwyeckn4a6a0fkxm4y7t3lvhzscqrwg3gxpj4rgrgmf93m0cpm9ddkzn5qyzgadktuwza5d5kucewv3"
override val uAddr = "utest10prpna6ydq6042q7t9qqr66qg9xhj8gn7aesrxnxelp7sqh5lc56w9qzl4pydhjvt9v34cxp5krdracecwg3dpkvgv8fttvz4hcql2m35se6u2n8h9p86xc7c6wm8fj7p4r3kq0mvvl4g650s6xdkhkg9yhtnnne4vy9k3hw27m0y6ctlmgkeadvn38v6wp9fpdwwhwrgn52z8fp6pc"
override val tAccountPrivKey = "xprv9yUDoMsKVAQ8W8tf3VuPGyBKHuDPa4SkBXT7KHp4dfW7iBWKUEgAYG1g6ZpdotTWc4iMrj6vgaT8otHCWRj5SYtXkDcxkheFCp6QZEW9dPi"
override val tskCompressed = "KzVugoXxR7AtTMdR5sdJtHxCNvMzQ4H196k7ATv4nnjoummsRC9G"
override val tpk = "03b1d7fb28d17c125b504d06b1530097e0a3c76ada184237e3bc0925041230a5af"

View File

@ -4,7 +4,8 @@ import cash.z.ecc.android.sdk.internal.model.Checkpoint
import cash.z.ecc.android.sdk.internal.model.ext.KEY_EPOCH_SECONDS
import cash.z.ecc.android.sdk.internal.model.ext.KEY_HASH
import cash.z.ecc.android.sdk.internal.model.ext.KEY_HEIGHT
import cash.z.ecc.android.sdk.internal.model.ext.KEY_TREE
import cash.z.ecc.android.sdk.internal.model.ext.KEY_ORCHARD_TREE
import cash.z.ecc.android.sdk.internal.model.ext.KEY_SAPLING_TREE
import cash.z.ecc.android.sdk.internal.model.ext.KEY_VERSION
import cash.z.ecc.android.sdk.internal.model.ext.VERSION_1
import cash.z.ecc.android.sdk.model.BlockHeight
@ -14,20 +15,30 @@ import org.json.JSONObject
object CheckpointFixture {
val NETWORK = ZcashNetwork.Mainnet
// These came from the mainnet 1500000.json file
val HEIGHT = BlockHeight.new(ZcashNetwork.Mainnet, 1500000L)
const val HASH = "00000000019e5b25a95c7607e7789eb326fddd69736970ebbe1c7d00247ef902"
const val EPOCH_SECONDS = 1639913234L
// These came from the mainnet 1700000.json file
val HEIGHT = BlockHeight.new(ZcashNetwork.Mainnet, 1700000L)
const val HASH = "0000000000f430793e0c6381b40b47ed77b0ed76d21c2c667acdfe7747a8ed5b"
const val EPOCH_SECONDS = 1654991544L
@Suppress("MaxLineLength", "ktlint:standard:max-line-length")
const val TREE = "01ce183032b16ed87fcc5052a42d908376526126346567773f55bc58a63e4480160013000001bae5112769a07772345dd402039f2949c457478fe9327363ff631ea9d78fb80d0177c0b6c21aa9664dc255336ed450914088108c38a9171c85875b4e53d31b3e140171add6f9129e124651ca894aa842a3c71b1738f3ee2b7ba829106524ef51e62101f9cebe2141ee9d0a3f3a3e28bce07fa6b6e1c7b42c01cc4fe611269e9d52da540001d0adff06de48569129bd2a211e3253716362da97270d3504d9c1b694689ebe3c0122aaaea90a7fa2773b8166937310f79a4278b25d759128adf3138d052da3725b0137fb2cbc176075a45db2a3c32d3f78e669ff2258fd974e99ec9fb314d7fd90180165aaee3332ea432d13a9398c4863b38b8a7a491877a5c46b0802dcd88f7e324301a9a262f8b92efc2e0e3e4bd1207486a79d62e87b4ab9cc41814d62a23c4e28040001e3c4ee998682df5c5e230d6968e947f83d0c03682f0cfc85f1e6ec8e8552c95a000155989fed7a8cc7a0d479498d6881ca3bafbe05c7095110f85c64442d6a06c25c0185cd8c141e620eda0ca0516f42240aedfabdf9189c8c6ac834b7bdebc171331d01ecceb776c043662617d62646ee60985521b61c0b860f3a9731e66ef74ed8fb320118f64df255c9c43db708255e7bf6bffd481e5c2f38fe9ed8f3d189f7f9cf2644"
const val SAPLING_TREE = "01c3674a676a784f00d3383330124e7c5c5470a2603a41a57114c6aed647d9356f001401c33e33c5a1711c589fef4a8553b5f965e4e06b5126944576d90ff770c6718e1600013d4a7407f15c6d9640675ab0db03155d17b467a678b8bc2578be04a7568bef5100019d14969270212b3fe9df2307a404f646b16c9152364fc3127b01f68a70ccdd0f000000000001b8b49a61716ecee15ea34cb00bfcfdd498eefef7d99a1e99dea510873dc10c4f018dff53df886aa66e320c9cd060ab7b821aed21e4cac2a871f48c385ff20ecb1f01b66c7874f1b7721ff890be6e832804877b534ffcb2b768ce6514e7c1a523e8380134136b9f1f00c2e9d16dc7358ef920862511c8fc42fb7074cbc9016d8d4e8b4c015eddc191a81221b7900bbdcd8610e5df595e3cdc7fd5b3432e3825206ae35b05017eda713cd733ccc555123788692a1876f9ca292b0aa2ddf3f45ed2b47f027340000000015ec9e9b1295908beed437df4126032ca57ada8e3ebb67067cd22a73c79a84009"
@Suppress("MaxLineLength", "ktlint:standard:max-line-length")
const val ORCHARD_TREE = "01ffe841309dd0d9fd5073282a966b5daaf3a36834b62ac25e350dd581cfce6e2f01f6be2fb34b7ead63fcf256751c7839f138121c5237b745f6bd1bf17b4b16da1e1f0102c2cc2ee89c7561d05e34d642efa5eb991141579cca7b0ff2c7faf7a253501d013490d36beed18879794594a1b9bf0def458e30cd99ddc5ae716c2eb121ccce37000001941b26a7f09a7a3887aec0879dfc1275225b83efbcef54674930c3c2dfe3322200000123f80f8c4446da4a147c92340b492788dca810ce0a997860f151a86927e86f390000000000000000000000000000000000000000000000"
internal fun new(
height: BlockHeight = HEIGHT,
hash: String = HASH,
time: Long = EPOCH_SECONDS,
tree: String = TREE
) = Checkpoint(height = height, hash = hash, epochSeconds = time, tree = tree)
saplingTree: String = SAPLING_TREE,
orchardTree: String = ORCHARD_TREE
) = Checkpoint(
height = height,
hash = hash,
epochSeconds = time,
saplingTree = saplingTree,
orchardTree = orchardTree
)
}
internal fun Checkpoint.toJson() =
@ -36,5 +47,6 @@ internal fun Checkpoint.toJson() =
put(Checkpoint.KEY_HEIGHT, height.value)
put(Checkpoint.KEY_HASH, hash)
put(Checkpoint.KEY_EPOCH_SECONDS, epochSeconds)
put(Checkpoint.KEY_TREE, tree)
put(Checkpoint.KEY_SAPLING_TREE, saplingTree)
put(Checkpoint.KEY_ORCHARD_TREE, orchardTree)
}.toString()

View File

@ -20,9 +20,11 @@ internal class FakeRustBackend(
metadata.removeAll { it.height > height }
}
override suspend fun putSaplingSubtreeRoots(
startIndex: Long,
roots: List<JniSubtreeRoot>,
override suspend fun putSubtreeRoots(
saplingStartIndex: Long,
saplingRoots: List<JniSubtreeRoot>,
orchardStartIndex: Long,
orchardRoots: List<JniSubtreeRoot>,
) {
TODO("Not yet implemented")
}
@ -114,6 +116,9 @@ internal class FakeRustBackend(
recoverUntil: Long?
): JniUnifiedSpendingKey = error("Intentionally not implemented in mocked FakeRustBackend implementation.")
override suspend fun isSeedRelevantToAnyDerivedAccounts(seed: ByteArray): Boolean =
error("Intentionally not implemented in mocked FakeRustBackend implementation.")
override fun isValidSaplingAddr(addr: String): Boolean =
error("Intentionally not implemented in mocked FakeRustBackend implementation.")
@ -154,6 +159,7 @@ internal class FakeRustBackend(
override suspend fun scanBlocks(
fromHeight: Long,
fromState: ByteArray,
limit: Long
) = error("Intentionally not implemented in mocked FakeRustBackend implementation.")
}

View File

@ -365,14 +365,10 @@ class SdkSynchronizer private constructor(
/**
* Calculate the latest balance based on the blocks that have been scanned and transmit this information into the
* [transparentBalance] and [saplingBalances] flow. The [orchardBalances] flow is still not filled with proper data
* because of the current limited Orchard support.
* [transparentBalance], [saplingBalances], and [orchardBalances] flows.
*/
suspend fun refreshAllBalances() {
processor.refreshWalletSummary()
// TODO [#682]: refresh orchard balance
// TODO [#682]: https://github.com/zcash/zcash-android-wallet-sdk/issues/682
Twig.warn { "Warning: Orchard balance does not yet refresh. Only some of the plumbing is in place." }
}
suspend fun isValidAddress(address: String): Boolean {

View File

@ -42,6 +42,7 @@ import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
import cash.z.ecc.android.sdk.internal.model.ScanRange
import cash.z.ecc.android.sdk.internal.model.SubtreeRoot
import cash.z.ecc.android.sdk.internal.model.SuggestScanRangePriority
import cash.z.ecc.android.sdk.internal.model.TreeState
import cash.z.ecc.android.sdk.internal.model.WalletSummary
import cash.z.ecc.android.sdk.internal.model.ext.from
import cash.z.ecc.android.sdk.internal.model.ext.toBlockHeight
@ -207,7 +208,7 @@ class CompactBlockProcessor internal constructor(
suspend fun start() {
val traceScope = TraceScope("CompactBlockProcessor.start")
val startIndex = refreshWalletSummary()
val (saplingStartIndex, orchardStartIndex) = refreshWalletSummary()
verifySetup()
@ -226,7 +227,7 @@ class CompactBlockProcessor internal constructor(
// Download note commitment tree data from lightwalletd to decide if we communicate with linear
// or spend-before-sync node.
var subTreeRootResult = getSubtreeRoots(downloader, network, startIndex)
var subTreeRootResult = getSubtreeRoots(downloader, network, saplingStartIndex, orchardStartIndex)
Twig.info { "Fetched SubTreeRoot result: $subTreeRootResult" }
Twig.debug { "Setup verified. Processor starting..." }
@ -251,10 +252,14 @@ class CompactBlockProcessor internal constructor(
val result =
putSaplingSubtreeRoots(
backend = backend,
startIndex = startIndex,
subTreeRootList =
saplingStartIndex = saplingStartIndex,
saplingSubtreeRootList =
(subTreeRootResult as GetSubtreeRootsResult.SpendBeforeSync)
.subTreeRootList,
.saplingSubtreeRootList,
orchardStartIndex = orchardStartIndex,
orchardSubtreeRootList =
(subTreeRootResult as GetSubtreeRootsResult.SpendBeforeSync)
.orchardSubtreeRootList,
lastValidHeight = lowerBoundHeight
)
) {
@ -290,7 +295,8 @@ class CompactBlockProcessor internal constructor(
}
GetSubtreeRootsResult.FailureConnection -> {
// SubtreeRoot fetching retry
subTreeRootResult = getSubtreeRoots(downloader, network, startIndex)
subTreeRootResult =
getSubtreeRoots(downloader, network, saplingStartIndex, orchardStartIndex)
BlockProcessingResult.Reconnecting
}
}
@ -464,7 +470,6 @@ class CompactBlockProcessor internal constructor(
repository = repository,
network = network,
syncRange = verifyRangeResult.scanRange.range,
withDownload = true,
enhanceStartHeight = firstUnenhancedHeight
).collect { batchSyncProgress ->
// Update sync progress and wallet balance
@ -561,7 +566,6 @@ class CompactBlockProcessor internal constructor(
repository = repository,
network = network,
syncRange = scanRange.range,
withDownload = true,
enhanceStartHeight = firstUnenhancedHeight
).map { batchSyncProgress ->
// Update sync progress and wallet balance
@ -728,15 +732,13 @@ class CompactBlockProcessor internal constructor(
/**
* Update the latest balances using the given wallet summary, and transmit this information
* into the related internal flows. Note that the Orchard balance is not supported.
* into the related internal flows.
*/
internal suspend fun updateAllBalances(summary: WalletSummary) {
summary.accountBalances[Account.DEFAULT]?.let {
Twig.debug { "Updating Sapling balance" }
Twig.debug { "Updating balances" }
saplingBalances.value = it.sapling
// TODO [#682]: Uncomment this once we have Orchard support.
// TODO [#682]: https://github.com/zcash/zcash-android-wallet-sdk/issues/682
// orchardBalances.value = it.orchard
orchardBalances.value = it.orchard
// We only allow stored transparent balance to be shielded, and we do so with
// a zero-conf transaction, so treat all unshielded balance as available.
transparentBalance.value = it.unshielded
@ -745,23 +747,26 @@ class CompactBlockProcessor internal constructor(
/**
* Refreshes the SDK's wallet summary from the Rust backend, and transmits this information
* into the related internal flows. Note that the Orchard balance is not yet supported.
* into the related internal flows.
*
* @return the next subtree index to fetch.
*/
internal suspend fun refreshWalletSummary(): UInt {
internal suspend fun refreshWalletSummary(): Pair<UInt, UInt> {
when (val result = getWalletSummary(backend)) {
is GetWalletSummaryResult.Success -> {
val resultProgress = result.scanProgressPercentDecimal()
Twig.info { "Progress from rust: ${resultProgress.decimal}" }
setProgress(resultProgress)
updateAllBalances(result.walletSummary)
return result.walletSummary.nextSaplingSubtreeIndex
return Pair(
result.walletSummary.nextSaplingSubtreeIndex,
result.walletSummary.nextOrchardSubtreeIndex
)
}
else -> {
// Do not report the progress and balances in case of any error, and
// tell the caller to fetch all subtree roots.
return UInt.MIN_VALUE
return Pair(UInt.MIN_VALUE, UInt.MIN_VALUE)
}
}
}
@ -1122,26 +1127,31 @@ class CompactBlockProcessor internal constructor(
* @return GetSubtreeRootsResult as a wrapper for the lightwalletd response result
*/
@VisibleForTesting
@Suppress("LongMethod")
internal suspend fun getSubtreeRoots(
downloader: CompactBlockDownloader,
network: ZcashNetwork,
startIndex: UInt
saplingStartIndex: UInt,
orchardStartIndex: UInt
): GetSubtreeRootsResult {
Twig.debug { "Fetching SubtreeRoots..." }
val traceScope = TraceScope("CompactBlockProcessor.getSubtreeRoots")
var result: GetSubtreeRootsResult = GetSubtreeRootsResult.Linear
var saplingSubtreeRootList: List<SubtreeRoot> = emptyList()
var orchardSubtreeRootList: List<SubtreeRoot> = emptyList()
retryUpToAndContinue(GET_SUBTREE_ROOTS_RETRIES) {
downloader.getSubtreeRoots(
startIndex,
saplingStartIndex,
maxEntries = UInt.MIN_VALUE,
shieldedProtocol = ShieldedProtocolEnum.SAPLING
).onEach { response ->
when (response) {
is Response.Success -> {
Twig.verbose {
"SubtreeRoot fetched successfully: its completingHeight is: ${response.result
"Sapling SubtreeRoot fetched successfully: its completingHeight is: ${response.result
.completingBlockHeight}"
}
}
@ -1154,12 +1164,14 @@ class CompactBlockProcessor internal constructor(
)
if (response is Response.Failure.Server.Unavailable) {
Twig.error {
"Fetching SubtreeRoot failed due to server communication problem with " +
"Fetching Sapling SubtreeRoot failed due to server communication problem with " +
"failure: ${response.toThrowable()}"
}
result = GetSubtreeRootsResult.FailureConnection
} else {
Twig.error { "Fetching SubtreeRoot failed with failure: ${response.toThrowable()}" }
Twig.error {
"Fetching Sapling SubtreeRoot failed with failure: ${response.toThrowable()}"
}
result = GetSubtreeRootsResult.OtherFailure(error)
}
traceScope.end()
@ -1175,15 +1187,74 @@ class CompactBlockProcessor internal constructor(
.map {
SubtreeRoot.new(it, network)
}.let {
result =
if (it.isEmpty()) {
GetSubtreeRootsResult.Linear
} else {
GetSubtreeRootsResult.SpendBeforeSync(startIndex, it)
}
saplingSubtreeRootList = it
}
}
retryUpToAndContinue(GET_SUBTREE_ROOTS_RETRIES) {
downloader.getSubtreeRoots(
orchardStartIndex,
maxEntries = UInt.MIN_VALUE,
shieldedProtocol = ShieldedProtocolEnum.ORCHARD
).onEach { response ->
when (response) {
is Response.Success -> {
Twig.verbose {
"Orchard SubtreeRoot fetched successfully: its completingHeight is: ${response.result
.completingBlockHeight}"
}
}
is Response.Failure -> {
val error =
LightWalletException.GetSubtreeRootsException(
response.code,
response.description,
response.toThrowable()
)
if (response is Response.Failure.Server.Unavailable) {
Twig.error {
"Fetching Orchard SubtreeRoot failed due to server communication problem with " +
"failure: ${response.toThrowable()}"
}
result = GetSubtreeRootsResult.FailureConnection
} else {
Twig.error {
"Fetching Orchard SubtreeRoot failed with failure: ${response.toThrowable()}"
}
result = GetSubtreeRootsResult.OtherFailure(error)
}
traceScope.end()
throw error
}
}
}
.filterIsInstance<Response.Success<SubtreeRootUnsafe>>()
.map { response ->
response.result
}
.toList()
.map {
SubtreeRoot.new(it, network)
}.let {
orchardSubtreeRootList = it
}
}
// Intentionally omitting [orchardSubtreeRootList], e.g., for Mainnet usage, we could check it, but on
// custom networks without NU5 activation, it wouldn't work. If the Orchard subtree roots are empty, it's
// technically still ok (as Orchard activates after Sapling, so on a network that doesn't have NU5
// activated, this would behave correctly). In contrast, if the Sapling subtree roots are empty, we
// cannot do SbS at all.
if (saplingSubtreeRootList.isNotEmpty()) {
result =
GetSubtreeRootsResult.SpendBeforeSync(
saplingStartIndex,
saplingSubtreeRootList,
orchardStartIndex,
orchardSubtreeRootList
)
}
traceScope.end()
return result
}
@ -1197,22 +1268,27 @@ class CompactBlockProcessor internal constructor(
* @return PutSaplingSubtreeRootsResult
*/
@VisibleForTesting
@Suppress("LongParameterList")
internal suspend fun putSaplingSubtreeRoots(
backend: TypesafeBackend,
startIndex: UInt,
subTreeRootList: List<SubtreeRoot>,
saplingStartIndex: UInt,
saplingSubtreeRootList: List<SubtreeRoot>,
orchardStartIndex: UInt,
orchardSubtreeRootList: List<SubtreeRoot>,
lastValidHeight: BlockHeight
): PutSaplingSubtreeRootsResult {
return runCatching {
backend.putSaplingSubtreeRoots(
startIndex = startIndex,
roots = subTreeRootList
backend.putSubtreeRoots(
saplingStartIndex = saplingStartIndex,
saplingRoots = saplingSubtreeRootList,
orchardStartIndex = orchardStartIndex,
orchardRoots = orchardSubtreeRootList,
)
}
.onSuccess {
Twig.info {
"Sapling subtree roots put successfully with startIndex: $startIndex and roots: " +
"${subTreeRootList.size}"
"Subtree roots put successfully with saplingStartIndex: $saplingStartIndex and " +
"orchardStartIndex: $orchardStartIndex"
}
}
.onFailure {
@ -1342,8 +1418,6 @@ class CompactBlockProcessor internal constructor(
* @param repository the derived data repository component
* @param network the network in which the sync mechanism operates
* @param syncRange the range of blocks to download
* @param withDownload the flag indicating whether the blocks should also be downloaded and processed, or
* processed existing blocks
* @param enhanceStartHeight the height in which the enhancing should start, or null in case of no previous
* transaction enhancing done yet
*
@ -1357,7 +1431,6 @@ class CompactBlockProcessor internal constructor(
repository: DerivedDataRepository,
network: ZcashNetwork,
syncRange: ClosedRange<BlockHeight>,
withDownload: Boolean,
enhanceStartHeight: BlockHeight?
): Flow<BatchSyncProgress> =
flow {
@ -1389,14 +1462,10 @@ class CompactBlockProcessor internal constructor(
SyncStageResult(
batch = it,
stageResult =
if (withDownload) {
downloadBatchOfBlocks(
downloader = downloader,
batch = it
)
} else {
SyncingResult.DownloadSuccess(null)
}
downloadBatchOfBlocks(
downloader = downloader,
batch = it
)
)
}.buffer(1).map { downloadStageResult ->
Twig.debug { "Download stage done with result: $downloadStageResult" }
@ -1413,7 +1482,8 @@ class CompactBlockProcessor internal constructor(
downloadStageResult.batch,
scanBatchOfBlocks(
backend = backend,
batch = downloadStageResult.batch
batch = downloadStageResult.batch,
fromState = downloadStageResult.stageResult.fromState
)
)
}
@ -1605,6 +1675,16 @@ class CompactBlockProcessor internal constructor(
batch: BlockBatch
): SyncingResult {
val traceScope = TraceScope("CompactBlockProcessor.downloadBatchOfBlocks")
val fromState =
fetchTreeStateForHeight(
height = batch.range.start - 1,
downloader = downloader
) ?: return SyncingResult.DownloadFailed(
batch.range.start,
CompactBlockProcessorException.FailedDownloadException()
)
var downloadedBlocks = listOf<JniBlockMeta>()
var downloadException: CompactBlockProcessorException.FailedDownloadException? = null
@ -1626,7 +1706,7 @@ class CompactBlockProcessor internal constructor(
Twig.verbose { "Successfully downloaded batch: $batch of $downloadedBlocks blocks" }
return if (downloadedBlocks.isNotEmpty()) {
SyncingResult.DownloadSuccess(downloadedBlocks)
SyncingResult.DownloadSuccess(fromState, downloadedBlocks)
} else {
SyncingResult.DownloadFailed(
batch.range.start,
@ -1635,15 +1715,43 @@ class CompactBlockProcessor internal constructor(
}
}
@VisibleForTesting
internal suspend fun fetchTreeStateForHeight(
height: BlockHeight,
downloader: CompactBlockDownloader,
): TreeState? {
retryUpToAndContinue(retries = RETRIES) { failedAttempts ->
if (failedAttempts == 0) {
Twig.debug { "Starting to fetch tree state for height ${height.value}" }
} else {
Twig.warn {
"Retrying to fetch tree state for height ${height.value} after $failedAttempts failure(s)..."
}
}
when (val response = downloader.getTreeState(BlockHeightUnsafe(height.value))) {
is Response.Success -> {
return TreeState.new(response.result)
}
is Response.Failure -> {
Twig.error(response.toThrowable()) { "Tree state fetch failed" }
throw response.toThrowable()
}
}
}
return null
}
@VisibleForTesting
internal suspend fun scanBatchOfBlocks(
batch: BlockBatch,
fromState: TreeState,
backend: TypesafeBackend
): SyncingResult {
val traceScope = TraceScope("CompactBlockProcessor.scanBatchOfBlocks")
val result =
runCatching {
backend.scanBlocks(batch.range.start, batch.range.length())
backend.scanBlocks(batch.range.start, fromState, batch.range.length())
}.onSuccess {
Twig.verbose { "Successfully scanned batch $batch" }
}.onFailure {

View File

@ -7,8 +7,10 @@ import cash.z.ecc.android.sdk.internal.model.SubtreeRoot
*/
internal sealed class GetSubtreeRootsResult {
data class SpendBeforeSync(
val startIndex: UInt,
val subTreeRootList: List<SubtreeRoot>
val saplingStartIndex: UInt,
val saplingSubtreeRootList: List<SubtreeRoot>,
val orchardStartIndex: UInt,
val orchardSubtreeRootList: List<SubtreeRoot>
) : GetSubtreeRootsResult()
data object Linear : GetSubtreeRootsResult()

View File

@ -4,6 +4,7 @@ import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor
import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException
import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
import cash.z.ecc.android.sdk.internal.model.ScanSummary
import cash.z.ecc.android.sdk.internal.model.TreeState
import cash.z.ecc.android.sdk.model.BlockHeight
/**
@ -16,8 +17,11 @@ internal sealed class SyncingResult {
object RestartSynchronization : SyncingResult()
data class DownloadSuccess(val downloadedBlocks: List<JniBlockMeta>?) : SyncingResult() {
override fun toString() = "${this::class.java.simpleName} with ${downloadedBlocks?.size ?: "none"} blocks"
data class DownloadSuccess(
val fromState: TreeState,
val downloadedBlocks: List<JniBlockMeta>
) : SyncingResult() {
override fun toString() = "${this::class.java.simpleName} with ${downloadedBlocks.size} blocks"
}
interface Failure {

View File

@ -174,6 +174,12 @@ sealed class InitializeException(message: String, cause: Throwable? = null) : Sd
private fun readResolve(): Any = SeedRequired
}
data object SeedNotRelevant : InitializeException(
"The provided seed is not relevant to any of the derived accounts in the wallet database."
) {
private fun readResolve(): Any = SeedNotRelevant
}
class FalseStart(cause: Throwable?) : InitializeException("Failed to initialize accounts due to: $cause", cause)
class AlreadyInitializedException(cause: Throwable, dbPath: String) : InitializeException(

View File

@ -85,9 +85,11 @@ internal interface TypesafeBackend {
* @throws RuntimeException as a common indicator of the operation failure
*/
@Throws(RuntimeException::class)
suspend fun putSaplingSubtreeRoots(
startIndex: UInt,
roots: List<SubtreeRoot>,
suspend fun putSubtreeRoots(
saplingStartIndex: UInt,
saplingRoots: List<SubtreeRoot>,
orchardStartIndex: UInt,
orchardRoots: List<SubtreeRoot>,
)
/**
@ -125,6 +127,7 @@ internal interface TypesafeBackend {
@Throws(RuntimeException::class)
suspend fun scanBlocks(
fromHeight: BlockHeight,
fromState: TreeState,
limit: Long
): ScanSummary

View File

@ -153,6 +153,7 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke
override suspend fun initDataDb(seed: ByteArray?) {
val ret = backend.initDataDb(seed)
when (ret) {
2 -> throw InitializeException.SeedNotRelevant
1 -> throw InitializeException.SeedRequired
0 -> { /* Successful case - no action needed */ }
-1 -> error("Rust backend only uses -1 as an error sentinel")
@ -160,18 +161,28 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke
}
}
override suspend fun putSaplingSubtreeRoots(
startIndex: UInt,
roots: List<SubtreeRoot>
) = backend.putSaplingSubtreeRoots(
startIndex = startIndex.toLong(),
roots =
roots.map {
override suspend fun putSubtreeRoots(
saplingStartIndex: UInt,
saplingRoots: List<SubtreeRoot>,
orchardStartIndex: UInt,
orchardRoots: List<SubtreeRoot>
) = backend.putSubtreeRoots(
saplingStartIndex = saplingStartIndex.toLong(),
saplingRoots =
saplingRoots.map {
JniSubtreeRoot.new(
rootHash = it.rootHash,
completingBlockHeight = it.completingBlockHeight.value
)
}
},
orchardStartIndex = orchardStartIndex.toLong(),
orchardRoots =
orchardRoots.map {
JniSubtreeRoot.new(
rootHash = it.rootHash,
completingBlockHeight = it.completingBlockHeight.value
)
},
)
override suspend fun updateChainTip(height: BlockHeight) = backend.updateChainTip(height.value)
@ -196,8 +207,9 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke
override suspend fun scanBlocks(
fromHeight: BlockHeight,
fromState: TreeState,
limit: Long
): ScanSummary = ScanSummary.new(backend.scanBlocks(fromHeight.value, limit), network)
): ScanSummary = ScanSummary.new(backend.scanBlocks(fromHeight.value, fromState.encoded, limit), network)
override suspend fun getWalletSummary(): WalletSummary? =
backend.getWalletSummary()?.let { jniWalletSummary ->

View File

@ -29,6 +29,7 @@ import kotlinx.coroutines.withContext
* @property lightWalletClient the client used for requesting compact blocks
* @property compactBlockStore responsible for persisting the compact blocks that are received
*/
@Suppress("TooManyFunctions")
open class CompactBlockDownloader private constructor(val compactBlockRepository: CompactBlockRepository) {
private lateinit var lightWalletClient: LightWalletClient
@ -178,6 +179,13 @@ open class CompactBlockDownloader private constructor(val compactBlockRepository
maxEntries = maxEntries
)
/**
* Returns information about roots of subtrees of the Sapling and Orchard note commitment trees.
*
* @return information about roots of subtrees of the Sapling and Orchard note commitment trees.
*/
suspend fun getTreeState(height: BlockHeightUnsafe) = lightWalletClient.getTreeState(height = height)
companion object {
private const val GET_SERVER_INFO_RETRIES = 6
}

View File

@ -83,11 +83,11 @@ internal object TxOutputsViewDefinition {
const val COLUMN_INTEGER_OUTPUT_INDEX = "output_index" // $NON-NLS
const val COLUMN_INTEGER_FROM_ACCOUNT = "from_account" // $NON-NLS
const val COLUMN_INTEGER_FROM_ACCOUNT = "from_account_id" // $NON-NLS
const val COLUMN_STRING_TO_ADDRESS = "to_address" // $NON-NLS
const val COLUMN_INTEGER_TO_ACCOUNT = "to_account" // $NON-NLS
const val COLUMN_INTEGER_TO_ACCOUNT = "to_account_id" // $NON-NLS
const val COLUMN_INTEGER_VALUE = "value" // $NON-NLS

View File

@ -16,14 +16,14 @@ internal data class Checkpoint(
val hash: String,
// Note: this field does NOT match the name of the JSON, so will break with field-based JSON parsing
val epochSeconds: Long,
// Note: this field does NOT match the name of the JSON, so will break with field-based JSON parsing
val tree: String
val saplingTree: String,
val orchardTree: String
) {
fun treeState(): TreeState {
require(epochSeconds.isInUIntRange()) {
"epochSeconds $epochSeconds is outside of allowed UInt range"
}
return TreeState.fromParts(height.value, hash, epochSeconds.toInt(), tree)
return TreeState.fromParts(height.value, hash, epochSeconds.toInt(), saplingTree, orchardTree)
}
internal companion object

View File

@ -17,9 +17,10 @@ class TreeState(
height: Long,
hash: String,
time: Int,
tree: String
saplingTree: String,
orchardTree: String
): TreeState {
val unsafeTreeState = TreeStateUnsafe.fromParts(height, hash, time, tree)
val unsafeTreeState = TreeStateUnsafe.fromParts(height, hash, time, saplingTree, orchardTree)
return TreeState.new(unsafeTreeState)
}
}

View File

@ -8,7 +8,8 @@ internal data class WalletSummary(
val chainTipHeight: BlockHeight,
val fullyScannedHeight: BlockHeight,
val scanProgress: ScanProgress,
val nextSaplingSubtreeIndex: UInt
val nextSaplingSubtreeIndex: UInt,
val nextOrchardSubtreeIndex: UInt
) {
companion object {
fun new(jni: JniWalletSummary): WalletSummary {
@ -20,7 +21,8 @@ internal data class WalletSummary(
chainTipHeight = BlockHeight(jni.chainTipHeight),
fullyScannedHeight = BlockHeight(jni.fullyScannedHeight),
scanProgress = ScanProgress.new(jni),
nextSaplingSubtreeIndex = jni.nextSaplingSubtreeIndex.toUInt()
nextSaplingSubtreeIndex = jni.nextSaplingSubtreeIndex.toUInt(),
nextOrchardSubtreeIndex = jni.nextOrchardSubtreeIndex.toUInt()
)
}
}

View File

@ -1,8 +1,10 @@
package cash.z.ecc.android.sdk.internal.model.ext
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.model.Checkpoint
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.ZcashNetwork
import org.json.JSONException
import org.json.JSONObject
// Version is not returned from the server, so version 1 is implied. A version is declared here
@ -17,8 +19,10 @@ internal val Checkpoint.Companion.KEY_HASH
get() = "hash"
internal val Checkpoint.Companion.KEY_EPOCH_SECONDS
get() = "time"
internal val Checkpoint.Companion.KEY_TREE
internal val Checkpoint.Companion.KEY_SAPLING_TREE
get() = "saplingTree"
internal val Checkpoint.Companion.KEY_ORCHARD_TREE
get() = "orchardTree"
internal fun Checkpoint.Companion.from(
zcashNetwork: ZcashNetwork,
@ -38,9 +42,21 @@ private fun Checkpoint.Companion.from(
}
val hash = jsonObject.getString(Checkpoint.KEY_HASH)
val epochSeconds = jsonObject.getLong(Checkpoint.KEY_EPOCH_SECONDS)
val tree = jsonObject.getString(Checkpoint.KEY_TREE)
val saplingTree = jsonObject.getString(Checkpoint.KEY_SAPLING_TREE)
val orchardTree =
try {
jsonObject.getString(Checkpoint.KEY_ORCHARD_TREE)
} catch (e: JSONException) {
Twig.warn(e) { "This checkpoint does not contain an Orchard tree state" }
// For checkpoints that don't contain an Orchard tree state, we can use
// the empty Orchard tree state as long as the height is before NU5.
require(height < zcashNetwork.orchardActivationHeight) {
"Post-NU5 checkpoint at height $height missing orchardTree field"
}
"000000" // NON-NLS
}
return Checkpoint(height, hash, epochSeconds, tree)
return Checkpoint(height, hash, epochSeconds, saplingTree, orchardTree)
}
else -> {
throw IllegalArgumentException("Unsupported version $version")