Adoption of sync & recovery progresses

Sync and recovery progresses

- FFI bumped to preview 0.15.0
- SDK has been refactored to incorporate recoveryProgress alongside scanProgress

Non-optional syncProgress

0 Denominator fix

OfflineTests fixed

Sync progress merged

- the progress is merged now but boolean value whether funds are spendable or not has been added extra
This commit is contained in:
Lukas Korba 2025-04-09 09:27:06 +02:00
parent 04ca05428f
commit 1a2db6b167
17 changed files with 128 additions and 89 deletions

View File

@ -176,8 +176,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/Electric-Coin-Company/zcash-light-client-ffi",
"state" : {
"revision" : "78cc7388a2ba5530888a99e584823a7399631d48",
"version" : "0.14.2"
"branch" : "preview/release/0.15.0",
"revision" : "c336dfc88b81aa3c857bc35e2d7d5452ae816d6e"
}
}
],

View File

@ -21,14 +21,18 @@ enum DemoAppConfig {
static let host = ZcashSDK.isMainnet ? "zec.rocks" : "lightwalletd.testnet.electriccoin.co"
static let port: Int = 443
static let defaultBirthdayHeight: BlockHeight = ZcashSDK.isMainnet ? 935000 : 1386000
static let defaultBirthdayHeight: BlockHeight = 2832500//ZcashSDK.isMainnet ? 935000 : 1386000
// static let defaultSeed = try! Mnemonic.deterministicSeedBytes(from: """
// wish puppy smile loan doll curve hole maze file ginger hair nose key relax knife witness cannon grab despair throw review deal slush frame
// """)
// static let defaultSeed = try! Mnemonic.deterministicSeedBytes(from: """
// live combine flight accident slow soda mind bright absent bid hen shy decade biology amazing mix enlist ensure biology rhythm snap duty soap armor
// """)
static let defaultSeed = try! Mnemonic.deterministicSeedBytes(from: """
live combine flight accident slow soda mind bright absent bid hen shy decade biology amazing mix enlist ensure biology rhythm snap duty soap armor
wreck craft number between hard warfare wisdom leave radar host local crane float play logic whale clap parade dynamic cotton attitude people guard together
""")
static let otherSynchronizers: [SynchronizerInitData] = [

View File

@ -319,8 +319,8 @@ extension SendViewController: UITextViewDelegate {
extension SDKSynchronizer {
static func textFor(state: SyncStatus) -> String {
switch state {
case .syncing(let progress):
return "Syncing \(progress * 100.0)%"
case let .syncing(syncProgress, areFundsSpendable):
return "Syncing \(syncProgress * 100.0)% spendable: \(areFundsSpendable)"
case .upToDate:
return "Up to Date 😎"

View File

@ -172,8 +172,8 @@ extension SyncBlocksListViewController: UITableViewDataSource {
extension SyncStatus {
var text: String {
switch self {
case let .syncing(progress):
return "Syncing 🤖 \(floor(progress * 1000) / 10)%"
case let .syncing(syncProgress, areFundsSpendable):
return "Syncing 🤖 \(floor(syncProgress * 1000) / 10)% spendable: \(areFundsSpendable)"
case .upToDate:
return "Up to Date 😎"
case .unprepared:

View File

@ -68,11 +68,13 @@ class SyncBlocksViewController: UIViewController {
case .unprepared:
break
case let .syncing(progress):
case let .syncing(syncProgress, areFundsSpendable):
enhancingStarted = false
progressBar.progress = progress
progressLabel.text = "\(floor(progress * 1000) / 10)%"
print("__LD syncProgress \(syncProgress) areFundsSpendable \(areFundsSpendable)")
progressBar.progress = syncProgress
progressLabel.text = "\(floor(syncProgress * 1000) / 10)% spendable: \(areFundsSpendable)"
let progressText = """
latest block height \(state.latestBlockHeight)
"""
@ -87,19 +89,9 @@ class SyncBlocksViewController: UIViewController {
}
@IBAction func startStop() {
var components = DateComponents()
components.year = 2019
components.month = 11
components.day = 1
let calendar = Calendar.current
if let date = calendar.date(from: components) {
synchronizer.estimateBirthdayHeight(for: date)
Task { @MainActor in
await doStartStop()
}
// Task { @MainActor in
// await doStartStop()
// }
}
func doStartStop() async {

View File

@ -16,7 +16,8 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/grpc/grpc-swift.git", from: "1.24.2"),
.package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.15.3"),
.package(url: "https://github.com/Electric-Coin-Company/zcash-light-client-ffi", exact: "0.14.2")
// .package(url: "https://github.com/Electric-Coin-Company/zcash-light-client-ffi", exact: "0.14.2")
.package(url: "https://github.com/Electric-Coin-Company/zcash-light-client-ffi", branch: "preview/release/0.15.0")
],
targets: [
.target(

View File

@ -73,13 +73,35 @@ extension ScanAction: Action {
// Proper solution is handled in
// TODO: [#1353] Advanced progress reporting, https://github.com/Electric-Coin-Company/zcash-swift-wallet-sdk/issues/1353
if progressReportReducer == 0 {
let walletSummary = try? await rustBackend.getWalletSummary()
let recoveryProgress = walletSummary?.recoveryProgress
// report scan progress only if it's available
if let scanProgress = try? await rustBackend.getWalletSummary()?.scanProgress {
logger.debug("progress ratio: \(scanProgress.numerator)/\(scanProgress.denominator)")
let progress = try scanProgress.progress()
if let scanProgress = walletSummary?.scanProgress {
let composedNumerator: Float = Float(scanProgress.numerator) + Float(recoveryProgress?.numerator ?? 0)
let composedDenominator: Float = Float(scanProgress.denominator) + Float(recoveryProgress?.denominator ?? 0)
logger.debug("progress ratio: \(composedNumerator)/\(composedDenominator)")
let progress: Float
if composedDenominator == 0 {
progress = 1.0
} else {
progress = composedNumerator / composedDenominator
}
// this shouldn't happen but if it does, we need to get notified by clients and work on a fix
if progress > 1.0 {
throw ZcashError.rustScanProgressOutOfRange("\(progress)")
}
let scanProgress: Float = (try? scanProgress.progress()) ?? 0.0
let areFundsSpendable = scanProgress == 1.0
logger.debug("progress float: \(progress)")
await didUpdate(.syncProgress(progress))
await didUpdate(.syncProgress(progress, areFundsSpendable))
}
progressReportReducer = Constants.reportDelay
} else {
progressReportReducer -= 1

View File

@ -483,10 +483,10 @@ extension CompactBlockProcessor {
case handledReorg(_ reorgHeight: BlockHeight, _ rewindHeight: BlockHeight)
/// Event sent when progress of some specific action happened.
case syncProgress(Float)
case syncProgress(Float, Bool)
/// Event sent when progress of the sync process changes.
case progressUpdated(Float)
case progressUpdated(Float, Bool)
/// Event sent when the CompactBlockProcessor fetched utxos from lightwalletd attempted to store them.
case storedUTXOs((inserted: [UnspentTransactionOutputEntity], skipped: [UnspentTransactionOutputEntity]))
@ -569,7 +569,9 @@ extension CompactBlockProcessor {
await self?.send(event: event)
if let progressChanged = await self?.compactBlockProgress.hasProgressUpdated(event), progressChanged {
if let progress = await self?.compactBlockProgress.progress {
await self?.send(event: .progressUpdated(progress))
await self?.send(
event: .progressUpdated(progress, self?.compactBlockProgress.areFundsSpendable ?? false)
)
}
}
}
@ -713,7 +715,7 @@ extension CompactBlockProcessor {
let lastScannedHeight = await latestBlocksDataProvider.maxScannedHeight
// Some actions may not run. For example there are no transactions to enhance and therefore there is no enhance progress. And in
// cases like this computation of final progress won't work properly. So let's fake 100% progress at the end of the sync process.
await send(event: .progressUpdated(1))
await send(event: .progressUpdated(1, false))
await send(event: .finished(lastScannedHeight))
await context.update(state: .finished)

View File

@ -11,13 +11,15 @@ final actor CompactBlockProgress {
static let zero = CompactBlockProgress()
var progress: Float = 0.0
var areFundsSpendable: Bool = false
func hasProgressUpdated(_ event: CompactBlockProcessor.Event) -> Bool {
guard case .syncProgress(let update) = event else {
guard case let .syncProgress(progress, areFundsSpendable) = event else {
return false
}
progress = update
self.progress = progress
self.areFundsSpendable = areFundsSpendable
return true
}

View File

@ -33,8 +33,7 @@ struct ScanProgress: Equatable {
func progress() throws -> Float {
guard denominator != 0 else {
// this shouldn't happen but if it does, we need to get notified by clients and work on a fix
throw ZcashError.rustScanProgressOutOfRange("\(numerator)/\(denominator)")
return 1.0
}
let value = Float(numerator) / Float(denominator)
@ -52,6 +51,7 @@ struct WalletSummary: Equatable {
let accountBalances: [AccountUUID: AccountBalance]
let chainTipHeight: BlockHeight
let fullyScannedHeight: BlockHeight
let recoveryProgress: ScanProgress?
let scanProgress: ScanProgress?
let nextSaplingSubtreeIndex: UInt32
let nextOrchardSubtreeIndex: UInt32

View File

@ -909,6 +909,7 @@ struct ZcashRustBackend: ZcashRustBackendWelding {
accountBalances: accountBalances,
chainTipHeight: BlockHeight(summaryPtr.pointee.chain_tip_height),
fullyScannedHeight: BlockHeight(summaryPtr.pointee.fully_scanned_height),
recoveryProgress: summaryPtr.pointee.recovery_progress?.pointee.toScanProgress(),
scanProgress: summaryPtr.pointee.scan_progress?.pointee.toScanProgress(),
nextSaplingSubtreeIndex: UInt32(summaryPtr.pointee.next_sapling_subtree_index),
nextOrchardSubtreeIndex: UInt32(summaryPtr.pointee.next_orchard_subtree_index)

View File

@ -436,7 +436,8 @@ public enum SyncStatus: Equatable {
public static func == (lhs: SyncStatus, rhs: SyncStatus) -> Bool {
switch (lhs, rhs) {
case (.unprepared, .unprepared): return true
case let (.syncing(lhsProgress), .syncing(rhsProgress)): return lhsProgress == rhsProgress
case let (.syncing(lhsSyncProgress, lhsRecoveryPrgoress), .syncing(rhsSyncProgress, rhsRecoveryPrgoress)):
return lhsSyncProgress == rhsSyncProgress && lhsRecoveryPrgoress == rhsRecoveryPrgoress
case (.upToDate, .upToDate): return true
case (.error, .error): return true
default: return false
@ -448,7 +449,7 @@ public enum SyncStatus: Equatable {
/// taking other maintenance steps that need to occur after an upgrade.
case unprepared
case syncing(_ progress: Float)
case syncing(_ syncProgress: Float, _ areFundsSpendable: Bool)
/// Indicates that this Synchronizer is fully up to date and ready for all wallet functions.
/// When set, a UI element may want to turn green.
@ -501,7 +502,7 @@ enum InternalSyncStatus: Equatable {
case unprepared
/// Indicates that this Synchronizer is actively processing new blocks (consists of fetch, scan and enhance operations)
case syncing(Float)
case syncing(Float, Bool)
/// Indicates that this Synchronizer is fully up to date and ready for all wallet functions.
/// When set, a UI element may want to turn green.
@ -597,7 +598,8 @@ extension InternalSyncStatus {
public static func == (lhs: InternalSyncStatus, rhs: InternalSyncStatus) -> Bool {
switch (lhs, rhs) {
case (.unprepared, .unprepared): return true
case let (.syncing(lhsProgress), .syncing(rhsProgress)): return lhsProgress == rhsProgress
case let (.syncing(lhsSyncProgress, lhsRecoveryPrgoress), .syncing(rhsSyncProgress, rhsRecoveryPrgoress)):
return lhsSyncProgress == rhsSyncProgress && lhsRecoveryPrgoress == rhsRecoveryPrgoress
case (.synced, .synced): return true
case (.stopped, .stopped): return true
case (.disconnected, .disconnected): return true
@ -608,8 +610,8 @@ extension InternalSyncStatus {
}
extension InternalSyncStatus {
init(_ blockProcessorProgress: Float) {
self = .syncing(blockProcessorProgress)
init(_ syncProgress: Float, _ areFundsSpendable: Bool) {
self = .syncing(syncProgress, areFundsSpendable)
}
}
@ -618,8 +620,8 @@ extension InternalSyncStatus {
switch self {
case .unprepared:
return .unprepared
case .syncing(let progress):
return .syncing(progress)
case let .syncing(syncProgress, areFundsSpendable):
return .syncing(syncProgress, areFundsSpendable)
case .synced:
return .upToDate
case .stopped:

View File

@ -173,8 +173,34 @@ public class SDKSynchronizer: Synchronizer {
await blockProcessor.start(retry: retry)
case .stopped, .synced, .disconnected, .error:
let syncProgress = (try? await initializer.rustBackend.getWalletSummary()?.scanProgress?.progress()) ?? 0
await updateStatus(.syncing(syncProgress))
let walletSummary = try? await initializer.rustBackend.getWalletSummary()
let recoveryProgress = walletSummary?.recoveryProgress
var syncProgress: Float = 0.0
var areFundsSpendable = false
if let scanProgress = walletSummary?.scanProgress {
let composedNumerator: Float = Float(scanProgress.numerator) + Float(recoveryProgress?.numerator ?? 0)
let composedDenominator: Float = Float(scanProgress.denominator) + Float(recoveryProgress?.denominator ?? 0)
let progress: Float
if composedDenominator == 0 {
progress = 1.0
} else {
progress = composedNumerator / composedDenominator
}
// this shouldn't happen but if it does, we need to get notified by clients and work on a fix
if progress > 1.0 {
throw ZcashError.rustScanProgressOutOfRange("\(progress)")
}
let scanProgress: Float = (try? scanProgress.progress()) ?? 0.0
areFundsSpendable = scanProgress == 1.0
syncProgress = progress
}
await updateStatus(.syncing(syncProgress, areFundsSpendable))
await blockProcessor.start(retry: retry)
}
}
@ -240,8 +266,8 @@ public class SDKSynchronizer: Synchronizer {
// log reorg information
self?.logger.info("handling reorg at: \(reorgHeight) with rewind height: \(rewindHeight)")
case let .progressUpdated(progress):
await self?.progressUpdated(progress: progress)
case let .progressUpdated(syncProgress, areFundsSpendable):
await self?.progressUpdated(syncProgress, areFundsSpendable)
case .syncProgress:
break
@ -281,8 +307,8 @@ public class SDKSynchronizer: Synchronizer {
}
}
private func progressUpdated(progress: Float) async {
let newStatus = InternalSyncStatus(progress)
private func progressUpdated(_ syncProgress: Float, _ areFundsSpendable: Bool) async {
let newStatus = InternalSyncStatus(syncProgress, areFundsSpendable)
await updateStatus(newStatus)
}

View File

@ -197,19 +197,19 @@ class SynchronizerDarksideTests: ZcashTestCase {
SynchronizerState(
syncSessionID: uuids[0],
accountsBalances: [:],
internalSyncStatus: .syncing(0),
internalSyncStatus: .syncing(0, false),
latestBlockHeight: 0
),
SynchronizerState(
syncSessionID: uuids[0],
accountsBalances: [:],
internalSyncStatus: .syncing(0.9),
internalSyncStatus: .syncing(0.9, false),
latestBlockHeight: 663189
),
SynchronizerState(
syncSessionID: uuids[0],
accountsBalances: [:],
internalSyncStatus: .syncing(1.0),
internalSyncStatus: .syncing(1.0, false),
latestBlockHeight: 663189
),
SynchronizerState(
@ -269,19 +269,19 @@ class SynchronizerDarksideTests: ZcashTestCase {
SynchronizerState(
syncSessionID: uuids[0],
accountsBalances: [:],
internalSyncStatus: .syncing(0),
internalSyncStatus: .syncing(0, false),
latestBlockHeight: 0
),
SynchronizerState(
syncSessionID: uuids[0],
accountsBalances: [:],
internalSyncStatus: .syncing(0.9),
internalSyncStatus: .syncing(0.9, false),
latestBlockHeight: 663189
),
SynchronizerState(
syncSessionID: uuids[0],
accountsBalances: [:],
internalSyncStatus: .syncing(1.0),
internalSyncStatus: .syncing(1.0, false),
latestBlockHeight: 663189
),
SynchronizerState(
@ -320,19 +320,19 @@ class SynchronizerDarksideTests: ZcashTestCase {
SynchronizerState(
syncSessionID: uuids[1],
accountsBalances: [:],
internalSyncStatus: .syncing(0),
internalSyncStatus: .syncing(0, false),
latestBlockHeight: 663189
),
SynchronizerState(
syncSessionID: uuids[1],
accountsBalances: [:],
internalSyncStatus: .syncing(0.9),
internalSyncStatus: .syncing(0.9, false),
latestBlockHeight: 663200
),
SynchronizerState(
syncSessionID: uuids[1],
accountsBalances: [:],
internalSyncStatus: .syncing(1.0),
internalSyncStatus: .syncing(1.0, false),
latestBlockHeight: 663200
),
SynchronizerState(

View File

@ -333,7 +333,7 @@ class SynchronizerOfflineTests: ZcashTestCase {
}
func testIsNewSessionOnUnpreparedToValidTransition() {
XCTAssertTrue(SessionTicker.live.isNewSyncSession(.unprepared, .syncing(0)))
XCTAssertTrue(SessionTicker.live.isNewSyncSession(.unprepared, .syncing(0, false)))
}
func testIsNotNewSessionOnUnpreparedToStateThatWontSync() {
@ -348,12 +348,8 @@ class SynchronizerOfflineTests: ZcashTestCase {
func testIsNotNewSyncSessionOnSameSession() {
XCTAssertFalse(
SessionTicker.live.isNewSyncSession(
.syncing(
0.5
),
.syncing(
0.6
)
.syncing(0.5, false),
.syncing(0.6, false)
)
)
}
@ -362,9 +358,7 @@ class SynchronizerOfflineTests: ZcashTestCase {
XCTAssertTrue(
SessionTicker.live.isNewSyncSession(
.synced,
.syncing(
0.6
)
.syncing(0.6, false)
)
)
}
@ -373,9 +367,7 @@ class SynchronizerOfflineTests: ZcashTestCase {
XCTAssertTrue(
SessionTicker.live.isNewSyncSession(
.disconnected,
.syncing(
0.6
)
.syncing(0.6, false)
)
)
}
@ -384,16 +376,14 @@ class SynchronizerOfflineTests: ZcashTestCase {
XCTAssertTrue(
SessionTicker.live.isNewSyncSession(
.stopped,
.syncing(
0.6
)
.syncing(0.6, false)
)
)
}
func testInternalSyncStatusesDontDifferWhenOuterStatusIsTheSame() {
XCTAssertFalse(InternalSyncStatus.disconnected.isDifferent(from: .disconnected))
XCTAssertFalse(InternalSyncStatus.syncing(0).isDifferent(from: .syncing(0)))
XCTAssertFalse(InternalSyncStatus.syncing(0, false).isDifferent(from: .syncing(0, false)))
XCTAssertFalse(InternalSyncStatus.stopped.isDifferent(from: .stopped))
XCTAssertFalse(InternalSyncStatus.synced.isDifferent(from: .synced))
XCTAssertFalse(InternalSyncStatus.unprepared.isDifferent(from: .unprepared))
@ -402,10 +392,10 @@ class SynchronizerOfflineTests: ZcashTestCase {
func testInternalSyncStatusMap_SyncingLowerBound() {
let synchronizerState = synchronizerState(
for:
InternalSyncStatus.syncing(0)
InternalSyncStatus.syncing(0, false)
)
if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(0.0, data) {
if case let .syncing(data, false) = synchronizerState.syncStatus, data != nextafter(0.0, data) {
XCTFail("Syncing is expected to be 0% (0.0) but received \(data).")
}
}
@ -413,10 +403,10 @@ class SynchronizerOfflineTests: ZcashTestCase {
func testInternalSyncStatusMap_SyncingInTheMiddle() {
let synchronizerState = synchronizerState(
for:
InternalSyncStatus.syncing(0.45)
InternalSyncStatus.syncing(0.45, false)
)
if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(0.45, data) {
if case let .syncing(data, false) = synchronizerState.syncStatus, data != nextafter(0.45, data) {
XCTFail("Syncing is expected to be 45% (0.45) but received \(data).")
}
}
@ -424,18 +414,18 @@ class SynchronizerOfflineTests: ZcashTestCase {
func testInternalSyncStatusMap_SyncingUpperBound() {
let synchronizerState = synchronizerState(
for:
InternalSyncStatus.syncing(0.9)
InternalSyncStatus.syncing(0.9, false)
)
if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(0.9, data) {
if case let .syncing(data, false) = synchronizerState.syncStatus, data != nextafter(0.9, data) {
XCTFail("Syncing is expected to be 90% (0.9) but received \(data).")
}
}
func testInternalSyncStatusMap_FetchingUpperBound() {
let synchronizerState = synchronizerState(for: InternalSyncStatus.syncing(1))
let synchronizerState = synchronizerState(for: InternalSyncStatus.syncing(1, false))
if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(1.0, data) {
if case let .syncing(data, false) = synchronizerState.syncStatus, data != nextafter(1.0, data) {
XCTFail("Syncing is expected to be 100% (1.0) but received \(data).")
}
}

View File

@ -135,9 +135,6 @@ class ZcashRustBackendTests: XCTestCase {
}
func testScanProgressThrowsOnWrongValues() {
// Assert that throws on Zero denominator
XCTAssertThrowsError(try ScanProgress(numerator: 0, denominator: 0).progress())
// Assert that throws on numerator > denominator
XCTAssertThrowsError(try ScanProgress(numerator: 23, denominator: 2).progress())

View File

@ -146,7 +146,7 @@ extension SynchronizerState {
SynchronizerState(
syncSessionID: .nullID,
accountsBalances: [:],
internalSyncStatus: .syncing(0),
internalSyncStatus: .syncing(0, false),
latestBlockHeight: 222222
)
}