diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample/AppDelegate.swift b/Example/ZcashLightClientSample/ZcashLightClientSample/AppDelegate.swift index 60c652fa..4eb4793c 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample/AppDelegate.swift +++ b/Example/ZcashLightClientSample/ZcashLightClientSample/AppDelegate.swift @@ -53,6 +53,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { spendParamsURL: try! spendParamsURLHelper(), outputParamsURL: try! outputParamsURLHelper(), saplingParamsSourceURL: SaplingParamsSourceURL.default, + syncAlgorithm: .spendBeforeSync, enableBackendTracing: true ) diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample/DemoAppConfig.swift b/Example/ZcashLightClientSample/ZcashLightClientSample/DemoAppConfig.swift index 2d2e47e0..fc736427 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample/DemoAppConfig.swift +++ b/Example/ZcashLightClientSample/ZcashLightClientSample/DemoAppConfig.swift @@ -28,7 +28,7 @@ enum DemoAppConfig { static let defaultBirthdayHeight: BlockHeight = ZcashSDK.isMainnet ? 1935000 : 2170000 static let defaultSeed = try! Mnemonic.deterministicSeedBytes(from: """ - kitchen renew wide common vague fold vacuum tilt amazing pear square gossip jewel month tree shock scan alpha just spot fluid toilet view dinner + kitchen renew wide common vague fold vacuum tilt amazing pear square gossip jewel month tree shock scan alpha just spot fluid toilet view dinner """) static let otherSynchronizers: [SynchronizerInitData] = [ diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample/Sync Blocks/SyncBlocksListViewController.swift b/Example/ZcashLightClientSample/ZcashLightClientSample/Sync Blocks/SyncBlocksListViewController.swift index a0d78ce2..46de3bf8 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample/Sync Blocks/SyncBlocksListViewController.swift +++ b/Example/ZcashLightClientSample/ZcashLightClientSample/Sync Blocks/SyncBlocksListViewController.swift @@ -112,6 +112,7 @@ class SyncBlocksListViewController: UIViewController { outputParamsURL: try! outputParamsURLHelper(), saplingParamsSourceURL: SaplingParamsSourceURL.default, alias: data.alias, + syncAlgorithm: .spendBeforeSync, loggingPolicy: .default(.debug), enableBackendTracing: true ) diff --git a/Sources/ZcashLightClientKit/Block/Actions/Action.swift b/Sources/ZcashLightClientKit/Block/Actions/Action.swift index 6be190c3..397930d6 100644 --- a/Sources/ZcashLightClientKit/Block/Actions/Action.swift +++ b/Sources/ZcashLightClientKit/Block/Actions/Action.swift @@ -11,13 +11,16 @@ actor ActionContext { var state: CBPState var prevState: CBPState? var syncControlData: SyncControlData + let preferredSyncAlgorithm: SyncAlgorithm + var supportedSyncAlgorithm: SyncAlgorithm? var totalProgressRange: CompactBlockRange = 0...0 var lastScannedHeight: BlockHeight? var lastDownloadedHeight: BlockHeight? var lastEnhancedHeight: BlockHeight? - init(state: CBPState) { + init(state: CBPState, preferredSyncAlgorithm: SyncAlgorithm = .linear) { self.state = state + self.preferredSyncAlgorithm = preferredSyncAlgorithm syncControlData = SyncControlData.empty } @@ -30,6 +33,7 @@ actor ActionContext { func update(lastScannedHeight: BlockHeight) async { self.lastScannedHeight = lastScannedHeight } func update(lastDownloadedHeight: BlockHeight) async { self.lastDownloadedHeight = lastDownloadedHeight } func update(lastEnhancedHeight: BlockHeight?) async { self.lastEnhancedHeight = lastEnhancedHeight } + func update(supportedSyncAlgorithm: SyncAlgorithm) async { self.supportedSyncAlgorithm = supportedSyncAlgorithm } } enum CBPState: CaseIterable { @@ -38,7 +42,7 @@ enum CBPState: CaseIterable { case validateServer case updateSubtreeRoots case updateChainTip - case validatePreviousWalletSession + case processSuggestedScanRanges case computeSyncControlData case download case scan diff --git a/Sources/ZcashLightClientKit/Block/Actions/ClearAlreadyScannedBlocksAction.swift b/Sources/ZcashLightClientKit/Block/Actions/ClearAlreadyScannedBlocksAction.swift index 475ac7d3..eb460f0d 100644 --- a/Sources/ZcashLightClientKit/Block/Actions/ClearAlreadyScannedBlocksAction.swift +++ b/Sources/ZcashLightClientKit/Block/Actions/ClearAlreadyScannedBlocksAction.swift @@ -22,11 +22,9 @@ extension ClearAlreadyScannedBlocksAction: Action { func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext { guard let lastScannedHeight = await context.lastScannedHeight else { - fatalError("it must be valid") - return context + throw ZcashError.compactBlockProcessorLastScannedHeight } - //let lastScannedHeight = //try await transactionRepository.lastScannedHeight() try await storage.clear(upTo: lastScannedHeight) await context.update(state: .enhance) diff --git a/Sources/ZcashLightClientKit/Block/Actions/ClearCacheAction.swift b/Sources/ZcashLightClientKit/Block/Actions/ClearCacheAction.swift index 792595b8..d43f8155 100644 --- a/Sources/ZcashLightClientKit/Block/Actions/ClearCacheAction.swift +++ b/Sources/ZcashLightClientKit/Block/Actions/ClearCacheAction.swift @@ -23,8 +23,19 @@ extension ClearCacheAction: Action { if await context.prevState == .idle { await context.update(state: .migrateLegacyCacheDB) } else { - //await context.update(state: .finished) // Linear - await context.update(state: .validatePreviousWalletSession) + if context.preferredSyncAlgorithm == .linear { + await context.update(state: .finished) + } else { + if let supportedSyncAlgorithm = await context.supportedSyncAlgorithm { + if supportedSyncAlgorithm == .linear { + await context.update(state: .finished) + } else { + await context.update(state: .processSuggestedScanRanges) + } + } else { + throw ZcashError.compactBlockProcessorSupportedSyncAlgorithm + } + } } return context } diff --git a/Sources/ZcashLightClientKit/Block/Actions/DownloadAction.swift b/Sources/ZcashLightClientKit/Block/Actions/DownloadAction.swift index cda8c433..2caface9 100644 --- a/Sources/ZcashLightClientKit/Block/Actions/DownloadAction.swift +++ b/Sources/ZcashLightClientKit/Block/Actions/DownloadAction.swift @@ -35,11 +35,10 @@ extension DownloadAction: Action { } let config = await configProvider.config -// let lastScannedHeightDB = try await transactionRepository.lastScannedHeight() let latestBlockHeight = await context.syncControlData.latestBlockHeight // This action is executed for each batch (batch size is 100 blocks by default) until all the blocks in whole `downloadRange` are downloaded. // So the right range for this batch must be computed. - let batchRangeStart = lastScannedHeight//max(lastScannedHeightDB, lastScannedHeight) + let batchRangeStart = lastScannedHeight let batchRangeEnd = min(latestBlockHeight, batchRangeStart + config.batchSize) guard batchRangeStart <= batchRangeEnd else { diff --git a/Sources/ZcashLightClientKit/Block/Actions/EnhanceAction.swift b/Sources/ZcashLightClientKit/Block/Actions/EnhanceAction.swift index 0fd16148..0b8d5069 100644 --- a/Sources/ZcashLightClientKit/Block/Actions/EnhanceAction.swift +++ b/Sources/ZcashLightClientKit/Block/Actions/EnhanceAction.swift @@ -22,18 +22,15 @@ final class EnhanceAction { func decideWhatToDoNext(context: ActionContext, lastScannedHeight: BlockHeight) async -> ActionContext { guard await context.syncControlData.latestScannedHeight != nil else { - await context.update(state: .clearCache) // linear -// await context.update(state: .validatePreviousWalletSession) // SbS + await context.update(state: .clearCache) return context } let latestBlockHeight = await context.syncControlData.latestBlockHeight if lastScannedHeight >= latestBlockHeight { - await context.update(state: .clearCache) // linear -// await context.update(state: .validatePreviousWalletSession) // SbS + await context.update(state: .clearCache) } else { - await context.update(state: .download) // Linear -// await context.update(state: .validatePreviousWalletSession) // SbS + await context.update(state: .download) } return context @@ -52,10 +49,8 @@ extension EnhanceAction: Action { // download and scan. let config = await configProvider.config - //let lastScannedHeight = try await transactionRepository.lastScannedHeight() guard let lastScannedHeight = await context.lastScannedHeight else { - await context.update(state: .validatePreviousWalletSession) - return context + throw ZcashError.compactBlockProcessorLastScannedHeight } guard let firstUnenhancedHeight = await context.syncControlData.firstUnenhancedHeight else { diff --git a/Sources/ZcashLightClientKit/Block/Actions/ProcessSuggestedScanRangesAction.swift b/Sources/ZcashLightClientKit/Block/Actions/ProcessSuggestedScanRangesAction.swift new file mode 100644 index 00000000..6ec14221 --- /dev/null +++ b/Sources/ZcashLightClientKit/Block/Actions/ProcessSuggestedScanRangesAction.swift @@ -0,0 +1,67 @@ +// +// ProcessSuggestedScanRangesAction.swift +// +// +// Created by Lukáš Korba on 02.08.2023. +// + +import Foundation + +final class ProcessSuggestedScanRangesAction { + let rustBackend: ZcashRustBackendWelding + let service: LightWalletService + let logger: Logger + + init(container: DIContainer) { + service = container.resolve(LightWalletService.self) + rustBackend = container.resolve(ZcashRustBackendWelding.self) + logger = container.resolve(Logger.self) + } +} + +extension ProcessSuggestedScanRangesAction: Action { + var removeBlocksCacheWhenFailed: Bool { false } + + func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext { + logger.info("Getting the suggested scan ranges from the wallet database.") + let scanRanges = try await rustBackend.suggestScanRanges() + + if let firstRange = scanRanges.first { + // If there is a range of blocks that needs to be verified, it will always + // be returned as the first element of the vector of suggested ranges. + if firstRange.priority == .verify { + // TODO: [#1189] handle rewind, https://github.com/zcash/ZcashLightClientKit/issues/1189 + // REWIND to download.start height HERE + } + + let lowerBound = firstRange.range.lowerBound - 1 + let upperBound = firstRange.range.upperBound - 1 + + let syncControlData = SyncControlData( + latestBlockHeight: upperBound, + latestScannedHeight: lowerBound, + firstUnenhancedHeight: lowerBound + 1 + ) + + logger.debug(""" + Init numbers: + latestBlockHeight [BC]: \(upperBound) + latestScannedHeight [DB]: \(lowerBound) + firstUnenhancedHeight [DB]: \(lowerBound + 1) + """) + + await context.update(lastScannedHeight: lowerBound) + await context.update(lastDownloadedHeight: lowerBound) + await context.update(syncControlData: syncControlData) + await context.update(totalProgressRange: lowerBound...upperBound) + + await context.update(state: .download) + } else { + await context.update(state: .finished) + } + + return context + } + + func stop() async { } +} diff --git a/Sources/ZcashLightClientKit/Block/Actions/SaplingParamsAction.swift b/Sources/ZcashLightClientKit/Block/Actions/SaplingParamsAction.swift index 03812c35..5c1c77bb 100644 --- a/Sources/ZcashLightClientKit/Block/Actions/SaplingParamsAction.swift +++ b/Sources/ZcashLightClientKit/Block/Actions/SaplingParamsAction.swift @@ -23,8 +23,12 @@ extension SaplingParamsAction: Action { func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext { logger.debug("Fetching sapling parameters") try await saplingParametersHandler.handleIfNeeded() -// await context.update(state: .computeSyncControlData) // Linear - await context.update(state: .updateSubtreeRoots) // SbS + + if context.preferredSyncAlgorithm == .spendBeforeSync { + await context.update(state: .updateSubtreeRoots) + } else { + await context.update(state: .computeSyncControlData) + } return context } diff --git a/Sources/ZcashLightClientKit/Block/Actions/ScanAction.swift b/Sources/ZcashLightClientKit/Block/Actions/ScanAction.swift index 9173f47c..064b1d6f 100644 --- a/Sources/ZcashLightClientKit/Block/Actions/ScanAction.swift +++ b/Sources/ZcashLightClientKit/Block/Actions/ScanAction.swift @@ -35,11 +35,10 @@ extension ScanAction: Action { } let config = await configProvider.config - //let lastScannedHeightDB = try await transactionRepository.lastScannedHeight() let latestBlockHeight = await context.syncControlData.latestBlockHeight // This action is executed for each batch (batch size is 100 blocks by default) until all the blocks in whole `scanRange` are scanned. // So the right range for this batch must be computed. - let batchRangeStart = lastScannedHeight//max(lastScannedHeightDB, lastScannedHeight) + let batchRangeStart = lastScannedHeight let batchRangeEnd = min(latestBlockHeight, batchRangeStart + config.batchSize) guard batchRangeStart <= batchRangeEnd else { @@ -50,24 +49,24 @@ extension ScanAction: Action { logger.debug("Starting scan blocks with range: \(batchRange.lowerBound)...\(batchRange.upperBound)") let totalProgressRange = await context.totalProgressRange - try await blockScanner.scanBlocks(at: batchRange, totalProgressRange: totalProgressRange) { [weak self] lastScannedHeight in - let progress = BlockProgress( - startHeight: totalProgressRange.lowerBound, - targetHeight: totalProgressRange.upperBound, - progressHeight: lastScannedHeight - ) - self?.logger.debug("progress: \(progress)") - await didUpdate(.progressPartialUpdate(.syncing(progress))) - - // ScanAction is controlled locally so it must report back the updated scanned height - await context.update(lastScannedHeight: lastScannedHeight) -// let prevSyncControlData = await context.syncControlData -// let newSyncControlData = SyncControlData( -// latestBlockHeight: prevSyncControlData.latestBlockHeight, -// latestScannedHeight: lastScannedHeight, -// firstUnenhancedHeight: prevSyncControlData.firstUnenhancedHeight -// ) -// await context.update(syncControlData: newSyncControlData) + + do { + try await blockScanner.scanBlocks(at: batchRange, totalProgressRange: totalProgressRange) { [weak self] lastScannedHeight in + let progress = BlockProgress( + startHeight: totalProgressRange.lowerBound, + targetHeight: totalProgressRange.upperBound, + progressHeight: lastScannedHeight + ) + self?.logger.debug("progress: \(progress)") + await didUpdate(.progressPartialUpdate(.syncing(progress))) + + // ScanAction is controlled locally so it must report back the updated scanned height + await context.update(lastScannedHeight: lastScannedHeight) + } + } catch { + // TODO: [#1189] check isContinuityError, https://github.com/zcash/ZcashLightClientKit/issues/1189 + // if YES, REWIND to height at what error occured - at least 1 block + throw error } return await update(context: context) diff --git a/Sources/ZcashLightClientKit/Block/Actions/UpdateChainTipAction.swift b/Sources/ZcashLightClientKit/Block/Actions/UpdateChainTipAction.swift index fd8e914f..635279ab 100644 --- a/Sources/ZcashLightClientKit/Block/Actions/UpdateChainTipAction.swift +++ b/Sources/ZcashLightClientKit/Block/Actions/UpdateChainTipAction.swift @@ -28,7 +28,7 @@ extension UpdateChainTipAction: Action { logger.info("Latest block height is \(latestBlockHeight)") try await rustBackend.updateChainTip(height: Int32(latestBlockHeight)) - await context.update(state: .validatePreviousWalletSession) + await context.update(state: .processSuggestedScanRanges) return context } diff --git a/Sources/ZcashLightClientKit/Block/Actions/UpdateSubtreeRootsAction.swift b/Sources/ZcashLightClientKit/Block/Actions/UpdateSubtreeRootsAction.swift index 8bebf49b..35aa47f7 100644 --- a/Sources/ZcashLightClientKit/Block/Actions/UpdateSubtreeRootsAction.swift +++ b/Sources/ZcashLightClientKit/Block/Actions/UpdateSubtreeRootsAction.swift @@ -46,8 +46,10 @@ extension UpdateSubtreeRootsAction: Action { // Likewise, no subtree roots results in switching to linear sync. if err != nil || roots.isEmpty { logger.info("Spend before Sync is not possible, switching to linear sync.") + await context.update(supportedSyncAlgorithm: .linear) await context.update(state: .computeSyncControlData) } else { + await context.update(supportedSyncAlgorithm: .spendBeforeSync) logger.info("Sapling tree has \(roots.count) subtrees") do { try await rustBackend.putSaplingSubtreeRoots(startIndex: UInt64(request.startIndex), roots: roots) diff --git a/Sources/ZcashLightClientKit/Block/Actions/ValidatePreviousWalletSessionAction.swift b/Sources/ZcashLightClientKit/Block/Actions/ValidatePreviousWalletSessionAction.swift deleted file mode 100644 index fac42ee0..00000000 --- a/Sources/ZcashLightClientKit/Block/Actions/ValidatePreviousWalletSessionAction.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// ValidatePreviousWalletSessionAction.swift -// -// -// Created by Lukáš Korba on 02.08.2023. -// - -import Foundation - -final class ValidatePreviousWalletSessionAction { - let rustBackend: ZcashRustBackendWelding - let service: LightWalletService - let logger: Logger - - init(container: DIContainer) { - service = container.resolve(LightWalletService.self) - rustBackend = container.resolve(ZcashRustBackendWelding.self) - logger = container.resolve(Logger.self) - } -} - -extension ValidatePreviousWalletSessionAction: Action { - var removeBlocksCacheWhenFailed: Bool { false } - - func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext { - logger.info("Getting the suggested scan ranges from the wallet database.") - let scanRanges = try await rustBackend.suggestScanRanges() - - print("__LD count \(scanRanges.count) first range \(scanRanges.first)") - - // Run the following loop until the wallet's view of the chain tip - // as of the previous wallet session is valid. -// while true { - // If there is a range of blocks that needs to be verified, it will always - // be returned as the first element of the vector of suggested ranges. - if let firstRange = scanRanges.first { - //if firstRange.priority == .verify { - let lowerBound = firstRange.range.lowerBound - 1 - let upperBound = firstRange.range.upperBound - 1 - - let syncControlData = SyncControlData( - latestBlockHeight: upperBound, - latestScannedHeight: lowerBound, - firstUnenhancedHeight: lowerBound + 1 - ) - - logger.debug(""" - Init numbers: - latestBlockHeight [BC]: \(upperBound) - latestScannedHeight [DB]: \(lowerBound) - firstUnenhancedHeight [DB]: \(lowerBound + 1) - """) - - if scanRanges.count == 1 { - print("cool") - } - - - await context.update(lastScannedHeight: lowerBound) - await context.update(lastDownloadedHeight: lowerBound) - await context.update(syncControlData: syncControlData) - await context.update(totalProgressRange: lowerBound...upperBound) - - await context.update(state: .download) -// } else { -// print("cool") -// } - } else { - await context.update(state: .finished) - } -// } else { -// // Nothing to verify; break out of the loop -// break -// } -// } - - // TODO: [#1171] Switching back to linear sync for now before step 7 are implemented - // https://github.com/zcash/ZcashLightClientKit/issues/1171 -// await context.update(state: .computeSyncControlData) - - return context - } - - func stop() async { } -} diff --git a/Sources/ZcashLightClientKit/Block/CompactBlockProcessor.swift b/Sources/ZcashLightClientKit/Block/CompactBlockProcessor.swift index 179c3019..18c7b2ed 100644 --- a/Sources/ZcashLightClientKit/Block/CompactBlockProcessor.swift +++ b/Sources/ZcashLightClientKit/Block/CompactBlockProcessor.swift @@ -70,6 +70,7 @@ actor CompactBlockProcessor { let network: ZcashNetwork let saplingActivation: BlockHeight let cacheDbURL: URL? + let syncAlgorithm: SyncAlgorithm var blockPollInterval: TimeInterval { TimeInterval.random(in: ZcashSDK.defaultPollInterval / 2 ... ZcashSDK.defaultPollInterval * 1.5) } @@ -89,6 +90,7 @@ actor CompactBlockProcessor { rewindDistance: Int = ZcashSDK.defaultRewindDistance, walletBirthdayProvider: @escaping () -> BlockHeight, saplingActivation: BlockHeight, + syncAlgorithm: SyncAlgorithm = .linear, network: ZcashNetwork ) { self.alias = alias @@ -106,6 +108,7 @@ actor CompactBlockProcessor { self.walletBirthdayProvider = walletBirthdayProvider self.saplingActivation = saplingActivation self.cacheDbURL = cacheDbURL + self.syncAlgorithm = syncAlgorithm } init( @@ -121,6 +124,7 @@ actor CompactBlockProcessor { maxBackoffInterval: TimeInterval = ZcashSDK.defaultMaxBackOffInterval, rewindDistance: Int = ZcashSDK.defaultRewindDistance, walletBirthdayProvider: @escaping () -> BlockHeight, + syncAlgorithm: SyncAlgorithm = .linear, network: ZcashNetwork ) { self.alias = alias @@ -138,6 +142,7 @@ actor CompactBlockProcessor { self.retries = retries self.maxBackoffInterval = maxBackoffInterval self.rewindDistance = rewindDistance + self.syncAlgorithm = syncAlgorithm } } @@ -169,13 +174,18 @@ actor CompactBlockProcessor { outputParamsURL: initializer.outputParamsURL, saplingParamsSourceURL: initializer.saplingParamsSourceURL, walletBirthdayProvider: walletBirthdayProvider, + syncAlgorithm: initializer.syncAlgorithm, network: initializer.network ), accountRepository: initializer.accountRepository ) } - init(container: DIContainer, config: Configuration, accountRepository: AccountRepository) { + init( + container: DIContainer, + config: Configuration, + accountRepository: AccountRepository + ) { Dependencies.setupCompactBlockProcessor( in: container, config: config, @@ -183,7 +193,7 @@ actor CompactBlockProcessor { ) let configProvider = ConfigProvider(config: config) - context = ActionContext(state: .idle) + context = ActionContext(state: .idle, preferredSyncAlgorithm: config.syncAlgorithm) actions = Self.makeActions(container: container, configProvider: configProvider) self.metrics = container.resolve(SDKMetrics.self) @@ -218,8 +228,8 @@ actor CompactBlockProcessor { action = UpdateSubtreeRootsAction(container: container) case .updateChainTip: action = UpdateChainTipAction(container: container) - case .validatePreviousWalletSession: - action = ValidatePreviousWalletSessionAction(container: container) + case .processSuggestedScanRanges: + action = ProcessSuggestedScanRangesAction(container: container) case .computeSyncControlData: action = ComputeSyncControlDataAction(container: container, configProvider: configProvider) case .download: @@ -595,7 +605,7 @@ extension CompactBlockProcessor { break case .updateChainTip: break - case .validatePreviousWalletSession: + case .processSuggestedScanRanges: break case .computeSyncControlData: break @@ -624,7 +634,7 @@ extension CompactBlockProcessor { private func resetContext() async { let lastEnhancedheight = await context.lastEnhancedHeight - context = ActionContext(state: .idle) + context = ActionContext(state: .idle, preferredSyncAlgorithm: config.syncAlgorithm) await context.update(lastEnhancedHeight: lastEnhancedheight) await compactBlockProgress.reset() } diff --git a/Sources/ZcashLightClientKit/Block/Scan/BlockScanner.swift b/Sources/ZcashLightClientKit/Block/Scan/BlockScanner.swift index 5938c7a9..a1d220b3 100644 --- a/Sources/ZcashLightClientKit/Block/Scan/BlockScanner.swift +++ b/Sources/ZcashLightClientKit/Block/Scan/BlockScanner.swift @@ -40,7 +40,7 @@ extension BlockScannerImpl: BlockScanner { logger.debug("Going to scan blocks in range: \(range)") try Task.checkCancellation() - let scanStartHeight = range.lowerBound//try await transactionRepository.lastScannedHeight() + let scanStartHeight = range.lowerBound let targetScanHeight = range.upperBound var scannedNewBlocks = false @@ -65,13 +65,7 @@ extension BlockScannerImpl: BlockScanner { let scanFinishTime = Date() -// if let lastScannedBlock = try await transactionRepository.lastScannedBlock() { -// lastScannedHeight = lastScannedBlock.height lastScannedHeight = startHeight + Int(batchSize) - 1 - await latestBlocksDataProvider.updateLatestScannedHeight(lastScannedHeight) -// await latestBlocksDataProvider.updateLatestScannedTime(TimeInterval(lastScannedBlock.time)) -// } -// lastScannedHeight = targetScanHeight scannedNewBlocks = previousScannedHeight != lastScannedHeight if scannedNewBlocks { diff --git a/Sources/ZcashLightClientKit/Error/ZcashError.swift b/Sources/ZcashLightClientKit/Error/ZcashError.swift index a437dcd5..feef5580 100644 --- a/Sources/ZcashLightClientKit/Error/ZcashError.swift +++ b/Sources/ZcashLightClientKit/Error/ZcashError.swift @@ -540,6 +540,12 @@ public enum ZcashError: Equatable, Error { /// Put sapling subtree roots to the DB failed. /// ZCBPEO0019 case compactBlockProcessorPutSaplingSubtreeRoots(_ error: Error) + /// Getting the `lastScannedHeight` failed but it's supposed to always provide some value. + /// ZCBPEO0020 + case compactBlockProcessorLastScannedHeight + /// Getting the `supportedSyncAlgorithm` failed but it's supposed to always provide some value. + /// ZCBPEO0021 + case compactBlockProcessorSupportedSyncAlgorithm /// The synchronizer is unprepared. /// ZSYNCO0001 case synchronizerNotPrepared @@ -715,6 +721,8 @@ public enum ZcashError: Equatable, Error { case .compactBlockProcessorConsensusBranchID: return "Consensus BranchIDs don't match this is probably an API or programming error." case .compactBlockProcessorDownloadBlockActionRewind: return "Rewind of DownloadBlockAction failed as no action is possible to unwrapp." case .compactBlockProcessorPutSaplingSubtreeRoots: return "Put sapling subtree roots to the DB failed." + case .compactBlockProcessorLastScannedHeight: return "Getting the `lastScannedHeight` failed but it's supposed to always provide some value." + case .compactBlockProcessorSupportedSyncAlgorithm: return "Getting the `supportedSyncAlgorithm` failed but it's supposed to always provide some value." case .synchronizerNotPrepared: return "The synchronizer is unprepared." case .synchronizerSendMemoToTransparentAddress: return "Memos can't be sent to transparent addresses." case .synchronizerShieldFundsInsuficientTransparentFunds: return "There is not enough transparent funds to cover fee for the shielding." @@ -880,6 +888,8 @@ public enum ZcashError: Equatable, Error { case .compactBlockProcessorConsensusBranchID: return .compactBlockProcessorConsensusBranchID case .compactBlockProcessorDownloadBlockActionRewind: return .compactBlockProcessorDownloadBlockActionRewind case .compactBlockProcessorPutSaplingSubtreeRoots: return .compactBlockProcessorPutSaplingSubtreeRoots + case .compactBlockProcessorLastScannedHeight: return .compactBlockProcessorLastScannedHeight + case .compactBlockProcessorSupportedSyncAlgorithm: return .compactBlockProcessorSupportedSyncAlgorithm case .synchronizerNotPrepared: return .synchronizerNotPrepared case .synchronizerSendMemoToTransparentAddress: return .synchronizerSendMemoToTransparentAddress case .synchronizerShieldFundsInsuficientTransparentFunds: return .synchronizerShieldFundsInsuficientTransparentFunds diff --git a/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift b/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift index 60e85271..6c1a185d 100644 --- a/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift +++ b/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift @@ -317,6 +317,10 @@ public enum ZcashErrorCode: String { case compactBlockProcessorDownloadBlockActionRewind = "ZCBPEO0018" /// Put sapling subtree roots to the DB failed. case compactBlockProcessorPutSaplingSubtreeRoots = "ZCBPEO0019" + /// Getting the `lastScannedHeight` failed but it's supposed to always provide some value. + case compactBlockProcessorLastScannedHeight = "ZCBPEO0020" + /// Getting the `supportedSyncAlgorithm` failed but it's supposed to always provide some value. + case compactBlockProcessorSupportedSyncAlgorithm = "ZCBPEO0021" /// The synchronizer is unprepared. case synchronizerNotPrepared = "ZSYNCO0001" /// Memos can't be sent to transparent addresses. diff --git a/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift b/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift index 3eccac23..72c00577 100644 --- a/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift +++ b/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift @@ -613,6 +613,12 @@ enum ZcashErrorDefinition { /// Put sapling subtree roots to the DB failed. // sourcery: code="ZCBPEO0019" case compactBlockProcessorPutSaplingSubtreeRoots(_ error: Error) + /// Getting the `lastScannedHeight` failed but it's supposed to always provide some value. + // sourcery: code="ZCBPEO0020" + case compactBlockProcessorLastScannedHeight + /// Getting the `supportedSyncAlgorithm` failed but it's supposed to always provide some value. + // sourcery: code="ZCBPEO0021" + case compactBlockProcessorSupportedSyncAlgorithm // MARK: - SDKSynchronizer diff --git a/Sources/ZcashLightClientKit/Initializer.swift b/Sources/ZcashLightClientKit/Initializer.swift index 60b3c1f7..92bc7d55 100644 --- a/Sources/ZcashLightClientKit/Initializer.swift +++ b/Sources/ZcashLightClientKit/Initializer.swift @@ -126,6 +126,7 @@ public class Initializer { let network: ZcashNetwork let logger: Logger let rustBackend: ZcashRustBackendWelding + let syncAlgorithm: SyncAlgorithm /// The effective birthday of the wallet based on the height provided when initializing and the checkpoints available on this SDK. /// @@ -165,6 +166,7 @@ public class Initializer { outputParamsURL: URL, saplingParamsSourceURL: SaplingParamsSourceURL, alias: ZcashSynchronizerAlias = .default, + syncAlgorithm: SyncAlgorithm = .linear, loggingPolicy: LoggingPolicy = .default(.debug), enableBackendTracing: Bool = false ) { @@ -197,6 +199,7 @@ public class Initializer { saplingParamsSourceURL: saplingParamsSourceURL, alias: alias, urlsParsingError: parsingError, + syncAlgorithm: syncAlgorithm, loggingPolicy: loggingPolicy ) } @@ -257,6 +260,7 @@ public class Initializer { saplingParamsSourceURL: SaplingParamsSourceURL, alias: ZcashSynchronizerAlias, urlsParsingError: ZcashError?, + syncAlgorithm: SyncAlgorithm = .linear, loggingPolicy: LoggingPolicy = .default(.debug) ) { self.container = container @@ -284,6 +288,7 @@ public class Initializer { self.walletBirthday = Checkpoint.birthday(with: 0, network: network).height self.urlsParsingError = urlsParsingError self.logger = container.resolve(Logger.self) + self.syncAlgorithm = syncAlgorithm } private static func makeLightWalletServiceFactory(endpoint: LightWalletEndpoint) -> LightWalletServiceFactory { diff --git a/Sources/ZcashLightClientKit/Synchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer.swift index c422fc51..7e9b033a 100644 --- a/Sources/ZcashLightClientKit/Synchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer.swift @@ -120,6 +120,9 @@ public protocol Synchronizer: AnyObject { /// An object that when enabled collects mertrics from the synchronizer var metrics: SDKMetrics { get } + /// Default algorithm used to sync the stored wallet with the blockchain. + var syncAlgorithm: SyncAlgorithm { get } + /// Initialize the wallet. The ZIP-32 seed bytes can optionally be passed to perform /// database migrations. most of the times the seed won't be needed. If they do and are /// not provided this will fail with `InitializationResult.seedRequired`. It could @@ -427,6 +430,15 @@ enum InternalSyncStatus: Equatable { } } +/// Algorithm used to sync the sdk with the blockchain +public enum SyncAlgorithm: Equatable { + /// Linear sync processes the unsynced blocks in a linear way up to the chain tip + case linear + /// Spend before Sync processes the unsynced blocks non-lineary, in prioritised ranges relevant to the stored wallet. + /// Note: This feature is in development (alpha version) so use carefully. + case spendBeforeSync +} + /// Kind of transactions handled by a Synchronizer public enum TransactionKind { case sent diff --git a/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift index cb3cf57c..8632bfca 100644 --- a/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift @@ -24,7 +24,9 @@ public class SDKSynchronizer: Synchronizer { public let metrics: SDKMetrics public let logger: Logger - + public var syncAlgorithm: SyncAlgorithm = .linear + private var requestedSyncAlgorithm: SyncAlgorithm? + // Don't read this variable directly. Use `status` instead. And don't update this variable directly use `updateStatus()` methods instead. private var underlyingStatus: GenericActor var status: InternalSyncStatus { @@ -87,6 +89,7 @@ public class SDKSynchronizer: Synchronizer { self.syncSession = SyncSession(.nullID) self.syncSessionTicker = syncSessionTicker self.latestBlocksDataProvider = initializer.container.resolve(LatestBlocksDataProvider.self) + self.syncAlgorithm = initializer.syncAlgorithm initializer.lightWalletService.connectionStateChange = { [weak self] oldState, newState in self?.connectivityStateChanged(oldState: oldState, newState: newState) @@ -542,7 +545,7 @@ public class SDKSynchronizer: Synchronizer { return subject.eraseToAnyPublisher() } - + // MARK: notify state private func snapshotState(status: InternalSyncStatus) async -> SynchronizerState { diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/ClearAlreadyScannedBlocksActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/ClearAlreadyScannedBlocksActionTests.swift index 6bbb04b7..17475c37 100644 --- a/Tests/OfflineTests/CompactBlockProcessorActions/ClearAlreadyScannedBlocksActionTests.swift +++ b/Tests/OfflineTests/CompactBlockProcessorActions/ClearAlreadyScannedBlocksActionTests.swift @@ -25,9 +25,11 @@ final class ClearAlreadyScannedBlocksActionTests: ZcashTestCase { ) do { - let nextContext = try await clearAlreadyScannedBlocksAction.run(with: .init(state: .clearAlreadyScannedBlocks)) { _ in } + let context = ActionContext(state: .clearAlreadyScannedBlocks) + await context.update(lastScannedHeight: -1) + + let nextContext = try await clearAlreadyScannedBlocksAction.run(with: context) { _ in } XCTAssertTrue(compactBlockRepositoryMock.clearUpToCalled, "storage.clear(upTo:) is expected to be called.") - XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.") let nextState = await nextContext.state XCTAssertTrue( nextState == .enhance, diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/DownloadActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/DownloadActionTests.swift index 077f013f..777cd2ac 100644 --- a/Tests/OfflineTests/CompactBlockProcessorActions/DownloadActionTests.swift +++ b/Tests/OfflineTests/CompactBlockProcessorActions/DownloadActionTests.swift @@ -22,7 +22,7 @@ final class DownloadActionTests: ZcashTestCase { blockDownloaderMock.setDownloadLimitClosure = { _ in } blockDownloaderMock.startDownloadMaxBlockBufferSizeClosure = { _ in } blockDownloaderMock.waitUntilRequestedBlocksAreDownloadedInClosure = { _ in } - blockDownloaderMock.updateLatestDownloadedBlockHeightClosure = { _ in } + blockDownloaderMock.updateLatestDownloadedBlockHeightForceClosure = { _, _ in } let downloadAction = setupAction( blockDownloaderMock, @@ -33,11 +33,11 @@ final class DownloadActionTests: ZcashTestCase { underlyingScanRange = CompactBlockRange(uncheckedBounds: (1000, 2000)) let syncContext = await setupActionContext() + await syncContext.update(lastScannedHeight: 1000) do { let nextContext = try await downloadAction.run(with: syncContext) { _ in } - XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.") XCTAssertTrue(blockDownloaderMock.setSyncRangeBatchSizeCalled, "downloader.setSyncRange() is expected to be called.") XCTAssertTrue(blockDownloaderMock.setDownloadLimitCalled, "downloader.setDownloadLimit() is expected to be called.") XCTAssertTrue(blockDownloaderMock.startDownloadMaxBlockBufferSizeCalled, "downloader.startDownload() is expected to be called.") @@ -111,7 +111,6 @@ final class DownloadActionTests: ZcashTestCase { do { let nextContext = try await downloadAction.run(with: syncContext) { _ in } - XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.") XCTAssertFalse(blockDownloaderMock.setSyncRangeBatchSizeCalled, "downloader.setSyncRange() is not expected to be called.") XCTAssertFalse(blockDownloaderMock.setDownloadLimitCalled, "downloader.setDownloadLimit() is not expected to be called.") XCTAssertFalse(blockDownloaderMock.startDownloadMaxBlockBufferSizeCalled, "downloader.startDownload() is not expected to be called.") diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/EnhanceActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/EnhanceActionTests.swift index 155009b3..190405b2 100644 --- a/Tests/OfflineTests/CompactBlockProcessorActions/EnhanceActionTests.swift +++ b/Tests/OfflineTests/CompactBlockProcessorActions/EnhanceActionTests.swift @@ -80,7 +80,6 @@ final class EnhanceActionTests: ZcashTestCase { do { _ = try await enhanceAction.run(with: syncContext) { _ in } - XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.") XCTAssertFalse(blockEnhancerMock.enhanceAtDidEnhanceCalled, "blockEnhancer.enhance() is not expected to be called.") } catch { XCTFail("testEnhanceAction_NoEnhanceRange is not expected to fail. \(error)") @@ -104,7 +103,6 @@ final class EnhanceActionTests: ZcashTestCase { do { _ = try await enhanceAction.run(with: syncContext) { _ in } - XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.") XCTAssertFalse(blockEnhancerMock.enhanceAtDidEnhanceCalled, "blockEnhancer.enhance() is not expected to be called.") } catch { XCTFail("testEnhanceAction_1000BlocksConditionNotFulfilled is not expected to fail. \(error)") @@ -163,8 +161,6 @@ final class EnhanceActionTests: ZcashTestCase { XCTAssertEqual(receivedTransaction.expiryHeight, transaction.expiryHeight, "ReceivedTransaction differs from mocked one.") } - XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.") - XCTAssertTrue(blockEnhancerMock.enhanceAtDidEnhanceCalled, "blockEnhancer.enhance() is expected to be called.") } catch { XCTFail("testEnhanceAction_EnhancementOfBlocksCalled_FoundTransactions is not expected to fail. \(error)") } @@ -226,8 +222,6 @@ final class EnhanceActionTests: ZcashTestCase { } XCTAssertEqual(minedTransaction.expiryHeight, transaction.expiryHeight, "MinedTransaction differs from mocked one.") } - XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.") - XCTAssertTrue(blockEnhancerMock.enhanceAtDidEnhanceCalled, "blockEnhancer.enhance() is expected to be called.") } catch { XCTFail("testEnhanceAction_EnhancementOfBlocksCalled_minedTransaction is not expected to fail. \(error)") } @@ -289,8 +283,6 @@ final class EnhanceActionTests: ZcashTestCase { } XCTAssertEqual(minedTransaction.expiryHeight, transaction.expiryHeight, "MinedTransaction differs from mocked one.") } - XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.") - XCTAssertTrue(blockEnhancerMock.enhanceAtDidEnhanceCalled, "blockEnhancer.enhance() is expected to be called.") } catch { XCTFail("testEnhanceAction_EnhancementOfBlocksCalled_minedTransaction is not expected to fail. \(error)") } @@ -307,6 +299,7 @@ final class EnhanceActionTests: ZcashTestCase { await syncContext.update(syncControlData: syncControlData) await syncContext.update(totalProgressRange: CompactBlockRange(uncheckedBounds: (1000, 2000))) + await syncContext.update(lastScannedHeight: underlyingScanRange?.lowerBound ?? -1) return syncContext } diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/SaplingParamsActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/SaplingParamsActionTests.swift index 78e70043..107b8ea1 100644 --- a/Tests/OfflineTests/CompactBlockProcessorActions/SaplingParamsActionTests.swift +++ b/Tests/OfflineTests/CompactBlockProcessorActions/SaplingParamsActionTests.swift @@ -28,8 +28,8 @@ final class SaplingParamsActionTests: ZcashTestCase { XCTAssertTrue(saplingParametersHandlerMock.handleIfNeededCalled, "saplingParametersHandler.handleIfNeeded() is expected to be called.") let nextState = await nextContext.state XCTAssertTrue( - nextState == .updateSubtreeRoots, - "nextContext after .handleSaplingParams is expected to be .updateSubtreeRoots but received \(nextState)" + nextState == .computeSyncControlData, + "nextContext after .handleSaplingParams is expected to be .computeSyncControlData but received \(nextState)" ) } catch { XCTFail("testSaplingParamsAction_NextAction is not expected to fail. \(error)") diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/ScanActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/ScanActionTests.swift index 3bb75c15..c4459b0c 100644 --- a/Tests/OfflineTests/CompactBlockProcessorActions/ScanActionTests.swift +++ b/Tests/OfflineTests/CompactBlockProcessorActions/ScanActionTests.swift @@ -22,6 +22,8 @@ final class ScanActionTests: ZcashTestCase { let scanAction = setupAction(blockScannerMock, transactionRepositoryMock, loggerMock) let syncContext = await setupActionContext() + await syncContext.update(lastScannedHeight: 1500) + do { let nextContext = try await scanAction.run(with: syncContext) { event in guard case .progressPartialUpdate(.syncing(let progress)) = event else { @@ -32,7 +34,6 @@ final class ScanActionTests: ZcashTestCase { XCTAssertEqual(progress.targetHeight, BlockHeight(2000)) XCTAssertEqual(progress.progressHeight, BlockHeight(1500)) } - XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.") XCTAssertTrue(loggerMock.debugFileFunctionLineCalled, "logger.debug(...) is expected to be called.") XCTAssertTrue(blockScannerMock.scanBlocksAtTotalProgressRangeDidScanCalled, "blockScanner.scanBlocks(...) is expected to be called.") let nextState = await nextContext.state @@ -78,7 +79,6 @@ final class ScanActionTests: ZcashTestCase { do { _ = try await scanAction.run(with: syncContext) { _ in } - XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.") XCTAssertFalse(loggerMock.debugFileFunctionLineCalled, "logger.debug(...) is not expected to be called.") XCTAssertFalse(blockScannerMock.scanBlocksAtTotalProgressRangeDidScanCalled, "blockScanner.scanBlocks(...) is not expected to be called.") } catch { diff --git a/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift b/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift index 9ca2e5c3..b02ce34b 100644 --- a/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift +++ b/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift @@ -1043,6 +1043,10 @@ class SynchronizerMock: Synchronizer { get { return underlyingMetrics } } var underlyingMetrics: SDKMetrics! + var syncAlgorithm: SyncAlgorithm { + get { return underlyingSyncAlgorithm } + } + var underlyingSyncAlgorithm: SyncAlgorithm! var pendingTransactions: [ZcashTransaction.Overview] { get async { return underlyingPendingTransactions } }