[#1188] Working prototype of SbS

- cleaned up the code
- ScanAlgorithm enum added to the SDK
- preferred sync algorithm set to .linear as default but can be changed to Spend before Sync as the Initializer.init parameter

[#1188] Working prototype of SbS

- error codes for failure states in the SbS State Machine changes added

[#1188] Working prototype of SbS (#1192)

- offline tests fixed
This commit is contained in:
Lukas Korba 2023-08-09 10:03:36 +02:00
parent 458aeeea4c
commit ff3af58a81
28 changed files with 197 additions and 159 deletions

View File

@ -53,6 +53,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
spendParamsURL: try! spendParamsURLHelper(),
outputParamsURL: try! outputParamsURLHelper(),
saplingParamsSourceURL: SaplingParamsSourceURL.default,
syncAlgorithm: .spendBeforeSync,
enableBackendTracing: true
)

View File

@ -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] = [

View File

@ -112,6 +112,7 @@ class SyncBlocksListViewController: UIViewController {
outputParamsURL: try! outputParamsURLHelper(),
saplingParamsSourceURL: SaplingParamsSourceURL.default,
alias: data.alias,
syncAlgorithm: .spendBeforeSync,
loggingPolicy: .default(.debug),
enableBackendTracing: true
)

View File

@ -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

View File

@ -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)

View File

@ -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
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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 { }
}

View File

@ -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
}

View File

@ -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)

View File

@ -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
}

View File

@ -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)

View File

@ -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 { }
}

View File

@ -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()
}

View File

@ -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 {

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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<InternalSyncStatus>
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 {

View File

@ -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,

View File

@ -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.")

View File

@ -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
}

View File

@ -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)")

View File

@ -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 {

View File

@ -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 }
}