[#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

@ -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,6 +49,8 @@ extension ScanAction: Action {
logger.debug("Starting scan blocks with range: \(batchRange.lowerBound)...\(batchRange.upperBound)")
let totalProgressRange = await context.totalProgressRange
do {
try await blockScanner.scanBlocks(at: batchRange, totalProgressRange: totalProgressRange) { [weak self] lastScannedHeight in
let progress = BlockProgress(
startHeight: totalProgressRange.lowerBound,
@ -61,13 +62,11 @@ extension ScanAction: Action {
// 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)
}
} 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,6 +24,8 @@ 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>
@ -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)

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