Merge pull request #1192 from LukasKorba/1188-Working-prototype-of-SbS

1188 working prototype of SbS
This commit is contained in:
Lukas Korba 2023-08-10 12:10:45 +02:00 committed by GitHub
commit bcecc91190
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 247 additions and 77 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,12 +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
}
@ -26,8 +30,10 @@ actor ActionContext {
}
func update(syncControlData: SyncControlData) async { self.syncControlData = syncControlData }
func update(totalProgressRange: CompactBlockRange) async { self.totalProgressRange = totalProgressRange }
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 {
@ -36,6 +42,7 @@ enum CBPState: CaseIterable {
case validateServer
case updateSubtreeRoots
case updateChainTip
case processSuggestedScanRanges
case computeSyncControlData
case download
case scan

View File

@ -21,7 +21,10 @@ extension ClearAlreadyScannedBlocksAction: Action {
var removeBlocksCacheWhenFailed: Bool { false }
func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext {
let lastScannedHeight = try await transactionRepository.lastScannedHeight()
guard let lastScannedHeight = await context.lastScannedHeight else {
throw ZcashError.compactBlockProcessorLastScannedHeight
}
try await storage.clear(upTo: lastScannedHeight)
await context.update(state: .enhance)

View File

@ -23,7 +23,19 @@ extension ClearCacheAction: Action {
if await context.prevState == .idle {
await context.update(state: .migrateLegacyCacheDB)
} else {
await context.update(state: .finished)
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

@ -64,6 +64,7 @@ extension ComputeSyncControlDataAction: Action {
firstUnenhancedHeight: enhanceStart
)
await context.update(lastScannedHeight: latestScannedHeight)
await context.update(lastDownloadedHeight: latestScannedHeight)
await context.update(syncControlData: syncControlData)
await context.update(totalProgressRange: latestScannedHeight...latestBlockHeight)
@ -72,7 +73,7 @@ extension ComputeSyncControlDataAction: Action {
if latestBlockHeight < latestScannedHeight || latestBlockHeight == latestScannedHeight {
await context.update(state: .finished)
} else {
await context.update(state: .fetchUTXO)
await context.update(state: .download)
}
return context

View File

@ -30,16 +30,15 @@ extension DownloadAction: Action {
var removeBlocksCacheWhenFailed: Bool { true }
func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext {
guard let lastScannedHeight = await context.syncControlData.latestScannedHeight else {
guard let lastScannedHeight = await context.lastScannedHeight else {
return await update(context: context)
}
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 = max(lastScannedHeightDB, lastScannedHeight)
let batchRangeStart = lastScannedHeight
let batchRangeEnd = min(latestBlockHeight, batchRangeStart + config.batchSize)
guard batchRangeStart <= batchRangeEnd else {
@ -47,10 +46,11 @@ extension DownloadAction: Action {
}
let batchRange = batchRangeStart...batchRangeEnd
let downloadLimit = batchRange.upperBound + (2 * config.batchSize)
let potentialDownloadLimit = batchRange.upperBound + (2 * config.batchSize)
let downloadLimit = await context.syncControlData.latestBlockHeight >= potentialDownloadLimit ? potentialDownloadLimit : batchRangeEnd
logger.debug("Starting download with range: \(batchRange.lowerBound)...\(batchRange.upperBound)")
await downloader.update(latestDownloadedBlockHeight: batchRange.lowerBound)
await downloader.update(latestDownloadedBlockHeight: batchRange.lowerBound, force: true) // SbS
try await downloader.setSyncRange(lastScannedHeight...latestBlockHeight, batchSize: config.batchSize)
await downloader.setDownloadLimit(downloadLimit)
await downloader.startDownload(maxBlockBufferSize: config.downloadBufferSize)

View File

@ -49,7 +49,9 @@ extension EnhanceAction: Action {
// download and scan.
let config = await configProvider.config
let lastScannedHeight = try await transactionRepository.lastScannedHeight()
guard let lastScannedHeight = await context.lastScannedHeight else {
throw ZcashError.compactBlockProcessorLastScannedHeight
}
guard let firstUnenhancedHeight = await context.syncControlData.firstUnenhancedHeight else {
return await decideWhatToDoNext(context: context, lastScannedHeight: lastScannedHeight)

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,7 +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: .download)
if context.preferredSyncAlgorithm == .spendBeforeSync {
await context.update(state: .updateSubtreeRoots)
} else {
await context.update(state: .computeSyncControlData)
}
return context
}

View File

@ -30,16 +30,15 @@ extension ScanAction: Action {
var removeBlocksCacheWhenFailed: Bool { true }
func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext {
guard let lastScannedHeight = await context.syncControlData.latestScannedHeight else {
guard let lastScannedHeight = await context.lastScannedHeight else {
return await update(context: context)
}
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 = max(lastScannedHeightDB, lastScannedHeight)
let batchRangeStart = lastScannedHeight
let batchRangeEnd = min(latestBlockHeight, batchRangeStart + config.batchSize)
guard batchRangeStart <= batchRangeEnd else {
@ -50,14 +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)))
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,9 +28,7 @@ extension UpdateChainTipAction: Action {
logger.info("Latest block height is \(latestBlockHeight)")
try await rustBackend.updateChainTip(height: Int32(latestBlockHeight))
// TODO: [#1169] Switching back to linear sync for now before step 5 & 6 are implemented
// https://github.com/zcash/ZcashLightClientKit/issues/1169
await context.update(state: .computeSyncControlData)
await context.update(state: .processSuggestedScanRanges)
return context
}

View File

@ -27,7 +27,7 @@ extension UpdateSubtreeRootsAction: Action {
request.shieldedProtocol = .sapling
request.maxEntries = 65536
logger.info("Attempt to get subtree roots, this may fail because lightwalletd may not support DAG sync.")
logger.info("Attempt to get subtree roots, this may fail because lightwalletd may not support Spend before Sync.")
let stream = service.getSubtreeRoots(request)
var roots: [SubtreeRoot] = []
@ -42,12 +42,14 @@ extension UpdateSubtreeRootsAction: Action {
err = error
}
// In case of error, the lightwalletd doesn't support DAG sync -> switching to linear sync.
// In case of error, the lightwalletd doesn't support Spend before Sync -> switching to linear sync.
// Likewise, no subtree roots results in switching to linear sync.
if err != nil || roots.isEmpty {
logger.info("DAG sync is not possible, switching to linear sync.")
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

@ -52,7 +52,7 @@ extension ValidateServerAction: Action {
throw ZcashError.compactBlockProcessorWrongConsensusBranchId(localBranch, remoteBranchID)
}
await context.update(state: .updateSubtreeRoots)
await context.update(state: .fetchUTXO)
return context
}

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,6 +228,8 @@ actor CompactBlockProcessor {
action = UpdateSubtreeRootsAction(container: container)
case .updateChainTip:
action = UpdateChainTipAction(container: container)
case .processSuggestedScanRanges:
action = ProcessSuggestedScanRangesAction(container: container)
case .computeSyncControlData:
action = ComputeSyncControlDataAction(container: container, configProvider: configProvider)
case .download:
@ -593,6 +605,8 @@ extension CompactBlockProcessor {
break
case .updateChainTip:
break
case .processSuggestedScanRanges:
break
case .computeSyncControlData:
break
case .download:
@ -620,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

@ -55,7 +55,7 @@ protocol BlockDownloader {
/// Updates the internal in memory value of latest downloaded block height. This way the `BlockDownloader` works with the current latest height and can
/// continue on parallel downloading of next batch.
func update(latestDownloadedBlockHeight: BlockHeight) async
func update(latestDownloadedBlockHeight: BlockHeight, force: Bool) async
/// Provides the value of latest downloaded height.
func latestDownloadedBlockHeight() async -> BlockHeight
/// In case rewind is needed, the latestDownloadedBlockHeight is rewritten forcefully.
@ -253,8 +253,8 @@ extension BlockDownloaderImpl: BlockDownloader {
self.latestDownloadedBlockHeight = latestDownloadedBlockHeight ?? -1
}
func update(latestDownloadedBlockHeight: BlockHeight) async {
if latestDownloadedBlockHeight >= self.latestDownloadedBlockHeight {
func update(latestDownloadedBlockHeight: BlockHeight, force: Bool = false) async {
if latestDownloadedBlockHeight >= self.latestDownloadedBlockHeight || force {
self.latestDownloadedBlockHeight = latestDownloadedBlockHeight
}
}

View File

@ -40,7 +40,7 @@ extension BlockScannerImpl: BlockScanner {
logger.debug("Going to scan blocks in range: \(range)")
try Task.checkCancellation()
let scanStartHeight = try await transactionRepository.lastScannedHeight()
let scanStartHeight = range.lowerBound
let targetScanHeight = range.upperBound
var scannedNewBlocks = false
@ -65,11 +65,7 @@ extension BlockScannerImpl: BlockScanner {
let scanFinishTime = Date()
if let lastScannedBlock = try await transactionRepository.lastScannedBlock() {
lastScannedHeight = lastScannedBlock.height
await latestBlocksDataProvider.updateLatestScannedHeight(lastScannedHeight)
await latestBlocksDataProvider.updateLatestScannedTime(TimeInterval(lastScannedBlock.time))
}
lastScannedHeight = startHeight + Int(batchSize) - 1
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

@ -8,6 +8,20 @@
import Foundation
struct ScanRange {
enum Priority: UInt8 {
case unknown = 0
case scanned = 10
case historic = 20
case openAdjacent = 30
case foundNote = 40
case chainTip = 50
case verify = 60
init(_ value: UInt8) {
self = Priority(rawValue: value) ?? .unknown
}
}
let range: Range<BlockHeight>
let priority: UInt8
let priority: Priority
}

View File

@ -50,7 +50,7 @@ actor LatestBlocksDataProviderImpl: LatestBlocksDataProvider {
latestScannedTime = TimeInterval(time)
}
}
func updateBlockData() async {
if let newLatestBlockHeight = try? await service.latestBlockHeight(),
latestBlockHeight < newLatestBlockHeight {

View File

@ -585,7 +585,7 @@ actor ZcashRustBackend: ZcashRustBackendWelding {
BlockHeight(scanRange.start),
BlockHeight(scanRange.end)
)),
priority: scanRange.priority
priority: ScanRange.Priority(scanRange.priority)
)
)
}

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

@ -81,8 +81,8 @@ final class ComputeSyncControlDataActionTests: ZcashTestCase {
let nextState = await nextContext.state
XCTAssertTrue(
nextState == .fetchUTXO,
"nextContext after .computeSyncControlData is expected to be .fetchUTXO but received \(nextState)"
nextState == .download,
"nextContext after .computeSyncControlData is expected to be .download but received \(nextState)"
)
} catch {
XCTFail("testComputeSyncControlDataAction_checksBeforeSyncCase is not expected to fail. \(error)")

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 == .download,
"nextContext after .handleSaplingParams is expected to be .download 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

@ -31,8 +31,8 @@ final class ValidateServerActionTests: ZcashTestCase {
let nextContext = try await validateServerAction.run(with: .init(state: .validateServer)) { _ in }
let nextState = await nextContext.state
XCTAssertTrue(
nextState == .updateSubtreeRoots,
"nextContext after .validateServer is expected to be .updateSubtreeRoots but received \(nextState)"
nextState == .fetchUTXO,
"nextContext after .validateServer is expected to be .fetchUTXO but received \(nextState)"
)
} catch {
XCTFail("testValidateServerAction_NextAction is not expected to fail. \(error)")

View File

@ -97,17 +97,17 @@ class BlockDownloaderMock: BlockDownloader {
// MARK: - update
var updateLatestDownloadedBlockHeightCallsCount = 0
var updateLatestDownloadedBlockHeightCalled: Bool {
return updateLatestDownloadedBlockHeightCallsCount > 0
var updateLatestDownloadedBlockHeightForceCallsCount = 0
var updateLatestDownloadedBlockHeightForceCalled: Bool {
return updateLatestDownloadedBlockHeightForceCallsCount > 0
}
var updateLatestDownloadedBlockHeightReceivedLatestDownloadedBlockHeight: BlockHeight?
var updateLatestDownloadedBlockHeightClosure: ((BlockHeight) async -> Void)?
var updateLatestDownloadedBlockHeightForceReceivedArguments: (latestDownloadedBlockHeight: BlockHeight, force: Bool)?
var updateLatestDownloadedBlockHeightForceClosure: ((BlockHeight, Bool) async -> Void)?
func update(latestDownloadedBlockHeight: BlockHeight) async {
updateLatestDownloadedBlockHeightCallsCount += 1
updateLatestDownloadedBlockHeightReceivedLatestDownloadedBlockHeight = latestDownloadedBlockHeight
await updateLatestDownloadedBlockHeightClosure!(latestDownloadedBlockHeight)
func update(latestDownloadedBlockHeight: BlockHeight, force: Bool) async {
updateLatestDownloadedBlockHeightForceCallsCount += 1
updateLatestDownloadedBlockHeightForceReceivedArguments = (latestDownloadedBlockHeight: latestDownloadedBlockHeight, force: force)
await updateLatestDownloadedBlockHeightForceClosure!(latestDownloadedBlockHeight, force)
}
// MARK: - latestDownloadedBlockHeight
@ -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 }
}