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
This commit is contained in:
Lukas Korba 2025-04-09 09:27:06 +02:00
parent 04ca05428f
commit f56fde9cb9
16 changed files with 79 additions and 88 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

@ -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, recoveryProgress):
return "Syncing \(syncProgress * 100.0)% \((recoveryProgress ?? 0) * 100.0)%"
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, recoveryProgress):
return "Syncing 🤖 \(floor(syncProgress * 1000) / 10)% \(floor((recoveryProgress ?? 0) * 1000) / 10)%"
case .upToDate:
return "Up to Date 😎"
case .unprepared:

View File

@ -68,11 +68,11 @@ class SyncBlocksViewController: UIViewController {
case .unprepared:
break
case let .syncing(progress):
case let .syncing(syncProgress, recoveryProgress):
enhancingStarted = false
progressBar.progress = progress
progressLabel.text = "\(floor(progress * 1000) / 10)%"
progressBar.progress = syncProgress
progressLabel.text = "\(floor(syncProgress * 1000) / 10)% \(floor((recoveryProgress ?? 0) * 1000) / 10)%"
let progressText = """
latest block height \(state.latestBlockHeight)
"""
@ -87,19 +87,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,17 @@ 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 = try? walletSummary?.recoveryProgress?.progress()
// report scan progress only if it's available
if let scanProgress = try? await rustBackend.getWalletSummary()?.scanProgress {
if let scanProgress = walletSummary?.scanProgress {
logger.debug("progress ratio: \(scanProgress.numerator)/\(scanProgress.denominator)")
let progress = try scanProgress.progress()
logger.debug("progress float: \(progress)")
await didUpdate(.syncProgress(progress))
logger.debug("progress float: \(progress) \(String(describing: recoveryProgress))")
await didUpdate(.syncProgress(progress, recoveryProgress))
}
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, Float?)
/// Event sent when progress of the sync process changes.
case progressUpdated(Float)
case progressUpdated(Float, Float?)
/// Event sent when the CompactBlockProcessor fetched utxos from lightwalletd attempted to store them.
case storedUTXOs((inserted: [UnspentTransactionOutputEntity], skipped: [UnspentTransactionOutputEntity]))
@ -568,8 +568,10 @@ extension CompactBlockProcessor {
context = try await action.run(with: context) { [weak self] event in
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))
if let progress = await self?.compactBlockProgress.syncProgress {
await self?.send(
event: .progressUpdated(progress, self?.compactBlockProgress.recoveryProgress)
)
}
}
}
@ -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, 1))
await send(event: .finished(lastScannedHeight))
await context.update(state: .finished)

View File

@ -10,15 +10,17 @@ import Foundation
final actor CompactBlockProgress {
static let zero = CompactBlockProgress()
var progress: Float = 0.0
var syncProgress: Float = 0.0
var recoveryProgress: Float?
func hasProgressUpdated(_ event: CompactBlockProcessor.Event) -> Bool {
guard case .syncProgress(let update) = event else {
guard case let .syncProgress(syncProgress, recoveryProgress) = event else {
return false
}
progress = update
self.syncProgress = syncProgress
self.recoveryProgress = recoveryProgress
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, _ recoveryProgress: Float?)
/// 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, Float?)
/// 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, _ recoveryProgress: Float?) {
self = .syncing(syncProgress, recoveryProgress)
}
}
@ -618,8 +620,8 @@ extension InternalSyncStatus {
switch self {
case .unprepared:
return .unprepared
case .syncing(let progress):
return .syncing(progress)
case let .syncing(syncProgress, recoveryProgress):
return .syncing(syncProgress, recoveryProgress)
case .synced:
return .upToDate
case .stopped:

View File

@ -173,8 +173,10 @@ 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: Float? = try? walletSummary?.recoveryProgress?.progress()
let syncProgress = (try? walletSummary?.scanProgress?.progress()) ?? 0
await updateStatus(.syncing(syncProgress, recoveryProgress))
await blockProcessor.start(retry: retry)
}
}
@ -240,8 +242,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, recoveryProgress):
await self?.progressUpdated(syncProgress, recoveryProgress)
case .syncProgress:
break
@ -281,8 +283,8 @@ public class SDKSynchronizer: Synchronizer {
}
}
private func progressUpdated(progress: Float) async {
let newStatus = InternalSyncStatus(progress)
private func progressUpdated(_ syncProgress: Float, _ recoveryProgress: Float?) async {
let newStatus = InternalSyncStatus(syncProgress, recoveryProgress)
await updateStatus(newStatus)
}

View File

@ -197,19 +197,19 @@ class SynchronizerDarksideTests: ZcashTestCase {
SynchronizerState(
syncSessionID: uuids[0],
accountsBalances: [:],
internalSyncStatus: .syncing(0),
internalSyncStatus: .syncing(0, 0),
latestBlockHeight: 0
),
SynchronizerState(
syncSessionID: uuids[0],
accountsBalances: [:],
internalSyncStatus: .syncing(0.9),
internalSyncStatus: .syncing(0.9, 0),
latestBlockHeight: 663189
),
SynchronizerState(
syncSessionID: uuids[0],
accountsBalances: [:],
internalSyncStatus: .syncing(1.0),
internalSyncStatus: .syncing(1.0, 0),
latestBlockHeight: 663189
),
SynchronizerState(
@ -269,19 +269,19 @@ class SynchronizerDarksideTests: ZcashTestCase {
SynchronizerState(
syncSessionID: uuids[0],
accountsBalances: [:],
internalSyncStatus: .syncing(0),
internalSyncStatus: .syncing(0, 0),
latestBlockHeight: 0
),
SynchronizerState(
syncSessionID: uuids[0],
accountsBalances: [:],
internalSyncStatus: .syncing(0.9),
internalSyncStatus: .syncing(0.9, 0),
latestBlockHeight: 663189
),
SynchronizerState(
syncSessionID: uuids[0],
accountsBalances: [:],
internalSyncStatus: .syncing(1.0),
internalSyncStatus: .syncing(1.0, 0),
latestBlockHeight: 663189
),
SynchronizerState(
@ -320,19 +320,19 @@ class SynchronizerDarksideTests: ZcashTestCase {
SynchronizerState(
syncSessionID: uuids[1],
accountsBalances: [:],
internalSyncStatus: .syncing(0),
internalSyncStatus: .syncing(0, 0),
latestBlockHeight: 663189
),
SynchronizerState(
syncSessionID: uuids[1],
accountsBalances: [:],
internalSyncStatus: .syncing(0.9),
internalSyncStatus: .syncing(0.9, 0),
latestBlockHeight: 663200
),
SynchronizerState(
syncSessionID: uuids[1],
accountsBalances: [:],
internalSyncStatus: .syncing(1.0),
internalSyncStatus: .syncing(1.0, 0),
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, 0)))
}
func testIsNotNewSessionOnUnpreparedToStateThatWontSync() {
@ -348,12 +348,8 @@ class SynchronizerOfflineTests: ZcashTestCase {
func testIsNotNewSyncSessionOnSameSession() {
XCTAssertFalse(
SessionTicker.live.isNewSyncSession(
.syncing(
0.5
),
.syncing(
0.6
)
.syncing(0.5, 0),
.syncing(0.6, 0)
)
)
}
@ -362,9 +358,7 @@ class SynchronizerOfflineTests: ZcashTestCase {
XCTAssertTrue(
SessionTicker.live.isNewSyncSession(
.synced,
.syncing(
0.6
)
.syncing(0.6, 0)
)
)
}
@ -373,9 +367,7 @@ class SynchronizerOfflineTests: ZcashTestCase {
XCTAssertTrue(
SessionTicker.live.isNewSyncSession(
.disconnected,
.syncing(
0.6
)
.syncing(0.6, 0)
)
)
}
@ -384,16 +376,14 @@ class SynchronizerOfflineTests: ZcashTestCase {
XCTAssertTrue(
SessionTicker.live.isNewSyncSession(
.stopped,
.syncing(
0.6
)
.syncing(0.6, 0)
)
)
}
func testInternalSyncStatusesDontDifferWhenOuterStatusIsTheSame() {
XCTAssertFalse(InternalSyncStatus.disconnected.isDifferent(from: .disconnected))
XCTAssertFalse(InternalSyncStatus.syncing(0).isDifferent(from: .syncing(0)))
XCTAssertFalse(InternalSyncStatus.syncing(0, 0).isDifferent(from: .syncing(0, 0)))
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, 0)
)
if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(0.0, data) {
if case let .syncing(data, 0) = 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, 0)
)
if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(0.45, data) {
if case let .syncing(data, 0) = 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, 0)
)
if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(0.9, data) {
if case let .syncing(data, 0) = 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, 0))
if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(1.0, data) {
if case let .syncing(data, 0) = 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, 0),
latestBlockHeight: 222222
)
}