[#700] CompactBlockProcessor as state machine proof of concept

- For now I created `CompactBlockProcessorNG` where I started with
  implementation of the state machine. I did it to not break the rest of
  the SDK. This change will be merged to the feature branch. And before
  it is merged to `main` branch code from `CompactBlockProcessorNG` will
  be moved to `CompactBlockProcessor`.
- The new code is not used. It just shows and explains how it is done.
  It is proof of concept.
- I did put either commented current code or comment to some places to
  explain what should be done there.
- New important data types:
  - `ActionContext` is context that can hold any data that needs to be
    shared between actions. For example sync ranges or current state.
  - `CBPState` is state of the `CompactBlockProcessor`. Each state is
    handled by one action. This doesn't apply to terminal states like
    `finished` or `failed`.
  - `ActionProgress` is very similar to `CompactBlockProgress`.
    Different actions reports progress differently and `ActionProgress`
    represents this.
  - `Action` is protocol that defines API of an action. It has one run
    method that executes the code of the action
- CBP first creates actions for (almost) each state in `makeActions()`
  method. Then the "magic" is done in `CompactBlockProcessorNG.run()` method.
  Here is main loop which takes action for current state and execute it.
  It's expected that action does it's work and then updates the context
  with new state. And this happens until some terminal state
  (`finished`, `failed`, `stopped`) is reached.
- After the transition to state machine API of the
  `CompactBlockProcessor` should stay the same. No changes should be
  required in `SDKSynchronizer`.

[#700] Add documentation for CompactBlockProcessor state machine

- plantuml tool is used to generate diagram.

[#1054] Add config to state machine CBP

Closes #1054

[#1043] Implement DownloadAction

Closes #1043

[#1049] Implement ValidateAction

Closes #1049

[#1050] Implement ValidateServerAction

Closes #1050

[#1056] Add constructors to state machine CBP

Closes #1056

[#1061] Add failure methods for state machine CBP

Closes #1061

[#1055] Implement retry timer to state machine CBP

Closes #1055

[#1057] Implement start for state machine CBP

Closes #1057

[#1058] Implement stop for state machine CBP

Closes #1058

[#1052] Implement AfterSyncHooksManager when using state machine CBP

Closes #1052

[#1060] Implement wipe for state machine CBP

Closes #1060

[#1059] Implement rewind for state machine CBP

Closes #1059

[#700] Add idle state to CBP state machine

This is required so the CBP can detect start of the sync process.

[#700] Implement sending of some events from CompactBlockProcessorNG

[#700] Implement progress reporting in state machine CBP

[#1045] Implement FetchUTXOsAction

- draft of the fetching

[#1045] Implement FetchUTXOsAction

- updated the way Actions communicate data back to the CBP
- used this mechanism to pass result of utxos fetch so it's passed to the SDKSynchronizer as an Event

[#1042] Implement ComputeSyncRangesAction

Closes #1042

[#700] Implement cache clearing when some actions fail

[#1043] Fix batch range computation in DownloadAction

[#1046] Implement SaplingParamsAction

- action for sapling param files finished

[#1048] Implement ScanDownloadedButUnscannedAction

- scan downloaded but unscanned blocks

[#1047] Implement ScanAction

- scan action with the proper ranges computed

[#1047] Implement ScanAction (#1085)

- fixed logger message

[#1044] Implement EnhanceAction

Closes #1044

[#1041] Implement ClearCacheAction

Closes #1041

[#1040] Implement ClearAlreadyScannedBlocksAction

Closes #1040

[#1039] Implement ChecksBeforeSyncAction

Closes #1039

[#700] Make CBP state machine work

[#1050] Implement ValidateServerAction

- broken tests commented out and tracked in the tickets
- new test for ValidateServerAction

[#1051] Update how progress is computed after switch to state machine

Closes #1051

- new proposal for the progress computation
- OverallProgress value is passed to the Synchronizer as a Float
- OverallProgress is a result of fetch, scan and enhance operations
- Order of actions no longer influences the computation
- Actions report partial updates, CompactBlockProgress actor holds current state and computes the OverallProgress

[#1049] Implement ValidateAction

- synchronizer offline tests updated so it compiles, review is requested in a different ticket
- ValidateAction tests added
- BlockValidator mock generated

[#1047] Implement ScanAction

- ScanAction tests
- refactor of validateAction -> validateServerAction
- generated few more mocks for the DI

[#1045] Implement FetchUTXOsAction

- FetchUTXOsAction tests
- UTXOFetcher mocks

[#1045] Implement FetchUTXOsAction

- enhanced with mocked values and more checks

[#1046] Implement SaplingParamsAction

- SaplingParamsAction tests
- added TODO for TestCoordinator reset()
- SaplingParametersHandler mock added

[#1046] Implement SaplingParamsAction

- SaplingParamsAction tests
- added TODO for TestCoordinator reset()
- SaplingParametersHandler mock added

[#1046] Implement SaplingParamsAction

- rebased so I get functionality of improved mock checks
- enhanced SaplingParamsAction tests
- enhanced ValidateAction tests
- enhanced ScanAction tests

[#1046] Implement SaplingParamsAction

- scanAction tests more checks added

[#1044] Implement EnhanceAction

- EnhanceAction tests focused on 2 different methods:
- decideWhatToDoNext covered separately, decisions where the state machine goes next
- run tests for different cases
- new mocks generated for enhacer
- some typos fixed

[#1044] Implement EnhanceAction (#1107)

- empty assert messages fixed

[#700] Get rid of ScanDownloadedButUnscannedAction

Before the state machine download and scan was called in one loop. And
processing range for one batch was same for both of them. Therefore
there was code which scanned downloaded but not scanned blocks.

But now download and scan are independent. So it is possible to remove
`ScanDownloadedButUnscannedAction`.

[#700] Make NetworkTests compilable

Some tests are disabled for now (list is in #1115). And `NetworkTests`
can be compiled and all the enabled tests work.

[#1043] Implement DownloadAction

- DownloadAction tests
- BlockDownloader mock

[#1043] Implement DownloadAction (#1110)

- support functions set to private

[#1039] Implement ChecksBeforeSyncAction

- ChecksBeforeSyncAction tests
- all support functions in Action tests are set to private
- let _ = -> _ = refactor
- CompactBlockRepository mock added

[#1040] Implement ClearAlreadyScannedBlocksAction

- Implement ClearAlreadyScannedBlocksAction tests
- CompactBlockRepository mock added

[#1041] Implement ClearCacheAction

- ClearCacheAction tests
- CompactBlockRepository mock added

[#1042] Implement ComputeSyncRangesAction

- ComputeSyncRangesAction tests
- fixed all tests after merge of latest SDK changes related InternalSyncProgress
- all actions marked as final class

[#1042] Implement ComputeSyncRangesAction (#1120)

- Custom LatestBlocksDataProviderMock removed from the project

[#1122] Implement FileManager protocol and dependency

- ZcashFileManager implemented
- MigrateLegacyCacheDBAction refactored to be dependent on ZcashFileManager
- ZcashFileManager mock added

[#1122] Implement FileManager protocol and dependency (#1124)

- code cleanup

[#1121] Implement MigrateLegacyCacheDBAction

- MigrateLegacyCacheDBAction tests WIP
- tests naming cleanup

[#1121] Implement MigrateLegacyCacheDBAction

- MigrateLegacyCacheDBAction tests finished

[#700] Fix DarksideTests

Closes #1102

Some tests that can't be compiled are disabled for now. List is in #1126.

This PR contains multiple fixes. Most of the fixes is done in the code.
Not in the tests. That is good news.

Fixes:
- `config` inside `CompactBlockProcessor` can be updated during the
  tests. And it must be also updated inside the actions. So
  `ConfigProvider` is added and it is wrapper for config that is passed
  to any instance of `Action` and provides updated config.
- Fixed `EnhanceAction`. Now it should update all the blocks in the
  enhance range even when the remaining count of blocks is lower than
  1000.
- Fixed `fail()` and `validationFailed()`. These two were canceling
  `syncTask`. But that stopped run loop in a bad way.

[#1129] Final check of all State Machine Action tests

- XTCAsset messages checked
- test naming checked and fixed

[#1126] Fix DarksideTests in state machine branch

Closes #1126

Fix offline tests

Closes #1098
Closes #1095
Closes #1094

Most of the tests is removed. Either the code that was tested doesn't
exists. Or now tests for state machine actions do this work.

[#1115] Fix NetworkTests in state machine branch

Closes #1115
This commit is contained in:
Michal Fousek 2023-05-05 17:04:13 +02:00
parent e8d5e9cd43
commit 2797f7be2d
67 changed files with 5008 additions and 2645 deletions

View File

@ -150,7 +150,6 @@ class SyncBlocksViewController: UIViewController {
case .unprepared, .error:
do {
if syncStatus == .unprepared {
// swiftlint:disable:next force_try
do {
_ = try await synchronizer.prepare(
with: DemoAppConfig.defaultSeed,

View File

@ -0,0 +1,61 @@
//
// Action.swift
//
//
// Created by Michal Fousek on 05.05.2023.
//
import Foundation
actor ActionContext {
var state: CBPState
var syncRanges: SyncRanges
var totalProgressRange: CompactBlockRange = 0...0
init(state: CBPState) {
self.state = state
syncRanges = SyncRanges.empty
}
func update(state: CBPState) async { self.state = state }
func update(syncRanges: SyncRanges) async { self.syncRanges = syncRanges }
func update(totalProgressRange: CompactBlockRange) async { self.totalProgressRange = totalProgressRange }
}
enum CBPState: CaseIterable {
case idle
case migrateLegacyCacheDB
case validateServer
case computeSyncRanges
case checksBeforeSync
case download
case validate
case scan
case clearAlreadyScannedBlocks
case enhance
case fetchUTXO
case handleSaplingParams
case clearCache
case finished
case failed
case stopped
}
protocol Action {
/// If this is true and action fails with error then blocks cache is cleared.
var removeBlocksCacheWhenFailed: Bool { get }
// When any action is created it can get `DIContainer` and resolve any depedencies it requires.
// Every action uses `context` to get some informartion like download range.
//
// `didUpdate` is closure that action use to tell CBP that some part of the work is done. For example if download action would like to
// update progress on every block downloaded it can use this closure. Also if action doesn't need to update progress on partial work it doesn't
// need to use this closure at all.
//
// Each action updates context accordingly. It should at least set new state. Reason for this is that action can return different states for
// different conditions. And action is the thing that knows these conditions.
func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext
// Should be called on each existing action when processor wants to stop. Some actions may do it's own background work.
func stop() async
}

View File

@ -0,0 +1,62 @@
//
// ChecksBeforeSyncAction.swift
//
//
// Created by Michal Fousek on 05.05.2023.
//
import Foundation
final class ChecksBeforeSyncAction {
let internalSyncProgress: InternalSyncProgress
let storage: CompactBlockRepository
init(container: DIContainer) {
internalSyncProgress = container.resolve(InternalSyncProgress.self)
storage = container.resolve(CompactBlockRepository.self)
}
/// Tells whether the state represented by these sync ranges evidence some sort of
/// outdated state on the cache or the internal state of the compact block processor.
///
/// - Note: this can mean that the processor has synced over the height that the internal
/// state knows of because the sync process was interrupted before it could reflect
/// it in the internal state storage. This could happen because of many factors, the
/// most feasible being OS shutting down a background process or the user abruptly
/// exiting the app.
/// - Returns: an ``Optional<BlockHeight>`` where Some represents what's the
/// new state the internal state should reflect and indicating that the cache should be cleared
/// as well. `nil` means that no action is required.
func shouldClearBlockCacheAndUpdateInternalState(syncRange: SyncRanges) -> BlockHeight? {
guard syncRange.downloadRange != nil, syncRange.scanRange != nil else { return nil }
guard
let latestScannedHeight = syncRange.latestScannedHeight,
let latestDownloadedHeight = syncRange.latestDownloadedBlockHeight,
latestScannedHeight > latestDownloadedHeight
else { return nil }
return latestScannedHeight
}
}
extension ChecksBeforeSyncAction: Action {
var removeBlocksCacheWhenFailed: Bool { false }
func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext {
// clear any present cached state if needed.
// this checks if there was a sync in progress that was
// interrupted abruptly and cache was not able to be cleared
// properly and internal state set to the appropriate value
if let newLatestDownloadedHeight = shouldClearBlockCacheAndUpdateInternalState(syncRange: await context.syncRanges) {
try await storage.clear()
try await internalSyncProgress.set(newLatestDownloadedHeight, .latestDownloadedBlockHeight)
} else {
try await storage.create()
}
await context.update(state: .fetchUTXO)
return context
}
func stop() async { }
}

View File

@ -0,0 +1,31 @@
//
// ClearCacheForLastScannedBatch.swift
//
//
// Created by Michal Fousek on 08.05.2023.
//
import Foundation
final class ClearAlreadyScannedBlocksAction {
let storage: CompactBlockRepository
let transactionRepository: TransactionRepository
init(container: DIContainer) {
storage = container.resolve(CompactBlockRepository.self)
transactionRepository = container.resolve(TransactionRepository.self)
}
}
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()
try await storage.clear(upTo: lastScannedHeight)
await context.update(state: .enhance)
return context
}
func stop() async { }
}

View File

@ -0,0 +1,27 @@
//
// ClearCacheAction.swift
//
//
// Created by Michal Fousek on 05.05.2023.
//
import Foundation
final class ClearCacheAction {
let storage: CompactBlockRepository
init(container: DIContainer) {
storage = container.resolve(CompactBlockRepository.self)
}
}
extension ClearCacheAction: Action {
var removeBlocksCacheWhenFailed: Bool { false }
func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext {
try await storage.clear()
await context.update(state: .finished)
return context
}
func stop() async { }
}

View File

@ -0,0 +1,93 @@
//
// ComputeSyncRangesAction.swift
//
//
// Created by Michal Fousek on 05.05.2023.
//
import Foundation
final class ComputeSyncRangesAction {
let configProvider: CompactBlockProcessor.ConfigProvider
let downloaderService: BlockDownloaderService
let internalSyncProgress: InternalSyncProgress
let latestBlocksDataProvider: LatestBlocksDataProvider
let logger: Logger
init(container: DIContainer, configProvider: CompactBlockProcessor.ConfigProvider) {
self.configProvider = configProvider
downloaderService = container.resolve(BlockDownloaderService.self)
internalSyncProgress = container.resolve(InternalSyncProgress.self)
latestBlocksDataProvider = container.resolve(LatestBlocksDataProvider.self)
logger = container.resolve(Logger.self)
}
/// This method analyses what must be done and computes range that should be used to compute reported progress.
func computeTotalProgressRange(from syncRanges: SyncRanges) -> CompactBlockRange {
guard syncRanges.downloadRange != nil || syncRanges.scanRange != nil else {
// In this case we are sure that no downloading or scanning happens so this returned range won't be even used. And it's easier to return
// this "fake" range than to handle nil.
return 0...0
}
// Thanks to guard above we can be sure that one of these two ranges is not nil.
let lowerBound = syncRanges.scanRange?.lowerBound ?? syncRanges.downloadRange?.lowerBound ?? 0
let upperBound = syncRanges.scanRange?.upperBound ?? syncRanges.downloadRange?.upperBound ?? 0
return lowerBound...upperBound
}
}
extension ComputeSyncRangesAction: Action {
var removeBlocksCacheWhenFailed: Bool { false }
func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext {
// call internalSyncProgress and compute sync ranges and store them in context
// if there is nothing sync just switch to finished state
let config = await configProvider.config
let latestDownloadHeight = try await downloaderService.lastDownloadedBlockHeight()
try await internalSyncProgress.migrateIfNeeded(latestDownloadedBlockHeightFromCacheDB: latestDownloadHeight, alias: config.alias)
await latestBlocksDataProvider.updateScannedData()
await latestBlocksDataProvider.updateBlockData()
let nextState = try await internalSyncProgress.computeNextState(
latestBlockHeight: latestBlocksDataProvider.latestBlockHeight,
latestScannedHeight: latestBlocksDataProvider.latestScannedHeight,
walletBirthday: config.walletBirthday
)
switch nextState {
case .finishProcessing:
await context.update(state: .finished)
case .processNewBlocks(let ranges):
let totalProgressRange = computeTotalProgressRange(from: ranges)
await context.update(totalProgressRange: totalProgressRange)
await context.update(syncRanges: ranges)
await context.update(state: .checksBeforeSync)
logger.debug("""
Syncing with ranges:
download: \(ranges.downloadRange?.lowerBound ?? -1)...\(ranges.downloadRange?.upperBound ?? -1)
scan: \(ranges.scanRange?.lowerBound ?? -1)...\(ranges.scanRange?.upperBound ?? -1)
enhance range: \(ranges.enhanceRange?.lowerBound ?? -1)...\(ranges.enhanceRange?.upperBound ?? -1)
fetchUTXO range: \(ranges.fetchUTXORange?.lowerBound ?? -1)...\(ranges.fetchUTXORange?.upperBound ?? -1)
total progress range: \(totalProgressRange.lowerBound)...\(totalProgressRange.upperBound)
""")
case let .wait(latestHeight, latestDownloadHeight):
// Lightwalletd might be syncing
logger.info(
"Lightwalletd might be syncing: latest downloaded block height is: \(latestDownloadHeight) " +
"while latest blockheight is reported at: \(latestHeight)"
)
await context.update(state: .finished)
}
return context
}
func stop() async { }
}

View File

@ -0,0 +1,64 @@
//
// DownloadAction.swift
//
//
// Created by Michal Fousek on 05.05.2023.
//
import Foundation
final class DownloadAction {
let configProvider: CompactBlockProcessor.ConfigProvider
let downloader: BlockDownloader
let transactionRepository: TransactionRepository
let logger: Logger
init(container: DIContainer, configProvider: CompactBlockProcessor.ConfigProvider) {
self.configProvider = configProvider
downloader = container.resolve(BlockDownloader.self)
transactionRepository = container.resolve(TransactionRepository.self)
logger = container.resolve(Logger.self)
}
private func update(context: ActionContext) async -> ActionContext {
await context.update(state: .validate)
return context
}
}
extension DownloadAction: Action {
var removeBlocksCacheWhenFailed: Bool { true }
func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext {
guard let downloadRange = await context.syncRanges.downloadRange else {
return await update(context: context)
}
let config = await configProvider.config
let lastScannedHeight = try await transactionRepository.lastScannedHeight()
// 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(downloadRange.lowerBound, lastScannedHeight)
let batchRangeEnd = min(downloadRange.upperBound, batchRangeStart + config.batchSize)
guard batchRangeStart <= batchRangeEnd else {
return await update(context: context)
}
let batchRange = batchRangeStart...batchRangeEnd
let downloadLimit = batchRange.upperBound + (2 * config.batchSize)
logger.debug("Starting download with range: \(batchRange.lowerBound)...\(batchRange.upperBound)")
try await downloader.setSyncRange(downloadRange, batchSize: config.batchSize)
await downloader.setDownloadLimit(downloadLimit)
await downloader.startDownload(maxBlockBufferSize: config.downloadBufferSize)
try await downloader.waitUntilRequestedBlocksAreDownloaded(in: batchRange)
return await update(context: context)
}
func stop() async {
await downloader.stopDownload()
}
}

View File

@ -0,0 +1,90 @@
//
// EnhanceAction.swift
//
//
// Created by Michal Fousek on 05.05.2023.
//
import Foundation
final class EnhanceAction {
let blockEnhancer: BlockEnhancer
let configProvider: CompactBlockProcessor.ConfigProvider
let internalSyncProgress: InternalSyncProgress
let logger: Logger
let transactionRepository: TransactionRepository
init(container: DIContainer, configProvider: CompactBlockProcessor.ConfigProvider) {
blockEnhancer = container.resolve(BlockEnhancer.self)
self.configProvider = configProvider
internalSyncProgress = container.resolve(InternalSyncProgress.self)
logger = container.resolve(Logger.self)
transactionRepository = container.resolve(TransactionRepository.self)
}
func decideWhatToDoNext(context: ActionContext, lastScannedHeight: BlockHeight) async -> ActionContext {
guard let scanRange = await context.syncRanges.scanRange else {
await context.update(state: .clearCache)
return context
}
if lastScannedHeight >= scanRange.upperBound {
await context.update(state: .clearCache)
} else {
await context.update(state: .download)
}
return context
}
}
extension EnhanceAction: Action {
var removeBlocksCacheWhenFailed: Bool { false }
func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext {
// Use `BlockEnhancer` to enhance blocks.
// This action is executed on each downloaded and scanned batch (typically each 100 blocks). But we want to run enhancement each 1000 blocks.
// This action can use `InternalSyncProgress` and last scanned height to compute when it should do work.
// if latestScannedHeight >= context.scanRanges.scanRange.upperBound then everything is processed and sync process should continue to end.
// If latestScannedHeight < context.scanRanges.scanRange.upperBound then set state to `download` because there are blocks to
// download and scan.
let config = await configProvider.config
let lastScannedHeight = try await transactionRepository.lastScannedHeight()
guard let range = await context.syncRanges.enhanceRange else {
return await decideWhatToDoNext(context: context, lastScannedHeight: lastScannedHeight)
}
let lastEnhancedHeight = try await internalSyncProgress.load(.latestEnhancedHeight)
let enhanceRangeStart = max(range.lowerBound, lastEnhancedHeight)
let enhanceRangeEnd = min(range.upperBound, lastScannedHeight)
// This may happen:
// For example whole enhance range is 0...2100 Without this force enhance is done for ranges: 0...1000, 1001...2000. And that's it.
// Last 100 blocks isn't enhanced.
//
// This force makes sure that all the blocks are enhanced even when last enhance happened < 1000 blocks ago.
let forceEnhance = enhanceRangeEnd == range.upperBound && enhanceRangeEnd - enhanceRangeStart <= config.enhanceBatchSize
if forceEnhance || (enhanceRangeStart <= enhanceRangeEnd && lastScannedHeight - lastEnhancedHeight >= config.enhanceBatchSize) {
let enhanceRange = enhanceRangeStart...enhanceRangeEnd
let transactions = try await blockEnhancer.enhance(
at: enhanceRange,
didEnhance: { progress in
if let foundTx = progress.lastFoundTransaction, progress.newlyMined {
await didUpdate(.minedTransaction(foundTx))
}
}
)
if let transactions {
await didUpdate(.foundTransactions(transactions, enhanceRange))
}
}
return await decideWhatToDoNext(context: context, lastScannedHeight: lastScannedHeight)
}
func stop() async { }
}

View File

@ -0,0 +1,37 @@
//
// FetchUTXOsAction.swift
//
//
// Created by Michal Fousek on 05.05.2023.
//
import Foundation
final class FetchUTXOsAction {
let utxoFetcher: UTXOFetcher
let logger: Logger
init(container: DIContainer) {
utxoFetcher = container.resolve(UTXOFetcher.self)
logger = container.resolve(Logger.self)
}
}
extension FetchUTXOsAction: Action {
var removeBlocksCacheWhenFailed: Bool { false }
func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext {
if let range = await context.syncRanges.fetchUTXORange {
logger.debug("Fetching UTXO with range: \(range.lowerBound)...\(range.upperBound)")
let result = try await utxoFetcher.fetch(at: range) { fetchProgress in
await didUpdate(.progressPartialUpdate(.fetch(fetchProgress)))
}
await didUpdate(.storedUTXOs(result))
}
await context.update(state: .handleSaplingParams)
return context
}
func stop() async { }
}

View File

@ -0,0 +1,79 @@
//
// MigrateLegacyCacheDB.swift
//
//
// Created by Michal Fousek on 10.05.2023.
//
import Foundation
final class MigrateLegacyCacheDBAction {
private let configProvider: CompactBlockProcessor.ConfigProvider
private let internalSyncProgress: InternalSyncProgress
private let storage: CompactBlockRepository
private let transactionRepository: TransactionRepository
private let fileManager: ZcashFileManager
init(container: DIContainer, configProvider: CompactBlockProcessor.ConfigProvider) {
self.configProvider = configProvider
internalSyncProgress = container.resolve(InternalSyncProgress.self)
storage = container.resolve(CompactBlockRepository.self)
transactionRepository = container.resolve(TransactionRepository.self)
fileManager = container.resolve(ZcashFileManager.self)
}
private func updateState(_ context: ActionContext) async -> ActionContext {
await context.update(state: .validateServer)
return context
}
}
extension MigrateLegacyCacheDBAction: Action {
var removeBlocksCacheWhenFailed: Bool { false }
func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext {
let config = await configProvider.config
guard let legacyCacheDbURL = config.cacheDbURL else {
return await updateState(context)
}
guard legacyCacheDbURL != config.fsBlockCacheRoot else {
throw ZcashError.compactBlockProcessorCacheDbMigrationFsCacheMigrationFailedSameURL
}
// Instance with alias `default` is same as instance before the Alias was introduced. So it makes sense that only this instance handles
// legacy cache DB. Any instance with different than `default` alias was created after the Alias was introduced and at this point legacy
// cache DB is't anymore. So there is nothing to migrate for instances with not default Alias.
guard config.alias == .default else {
return await updateState(context)
}
// if the URL provided is not readable, it means that the client has a reference
// to the cacheDb file but it has been deleted in a prior sync cycle. there's
// nothing to do here.
guard fileManager.isReadableFile(atPath: legacyCacheDbURL.path) else {
return await updateState(context)
}
do {
// if there's a readable file at the provided URL, delete it.
try fileManager.removeItem(at: legacyCacheDbURL)
} catch {
throw ZcashError.compactBlockProcessorCacheDbMigrationFailedToDeleteLegacyDb(error)
}
// create the storage
try await self.storage.create()
// The database has been deleted, so we have adjust the internal state of the
// `CompactBlockProcessor` so that it doesn't rely on download heights set
// by a previous processing cycle.
let lastScannedHeight = try await transactionRepository.lastScannedHeight()
try await internalSyncProgress.set(lastScannedHeight, .latestDownloadedBlockHeight)
return await updateState(context)
}
func stop() { }
}

View File

@ -0,0 +1,31 @@
//
// SaplingParamsAction.swift
//
//
// Created by Michal Fousek on 05.05.2023.
//
import Foundation
final class SaplingParamsAction {
let saplingParametersHandler: SaplingParametersHandler
let logger: Logger
init(container: DIContainer) {
saplingParametersHandler = container.resolve(SaplingParametersHandler.self)
logger = container.resolve(Logger.self)
}
}
extension SaplingParamsAction: Action {
var removeBlocksCacheWhenFailed: Bool { false }
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)
return context
}
func stop() async { }
}

View File

@ -0,0 +1,66 @@
//
// ScanAction.swift
//
//
// Created by Michal Fousek on 05.05.2023.
//
import Foundation
final class ScanAction {
let configProvider: CompactBlockProcessor.ConfigProvider
let blockScanner: BlockScanner
let logger: Logger
let transactionRepository: TransactionRepository
init(container: DIContainer, configProvider: CompactBlockProcessor.ConfigProvider) {
self.configProvider = configProvider
blockScanner = container.resolve(BlockScanner.self)
transactionRepository = container.resolve(TransactionRepository.self)
logger = container.resolve(Logger.self)
}
private func update(context: ActionContext) async -> ActionContext {
await context.update(state: .clearAlreadyScannedBlocks)
return context
}
}
extension ScanAction: Action {
var removeBlocksCacheWhenFailed: Bool { true }
func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext {
guard let scanRange = await context.syncRanges.scanRange else {
return await update(context: context)
}
let config = await configProvider.config
let lastScannedHeight = try await transactionRepository.lastScannedHeight()
// 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(scanRange.lowerBound, lastScannedHeight)
let batchRangeEnd = min(scanRange.upperBound, batchRangeStart + config.batchSize)
guard batchRangeStart <= batchRangeEnd else {
return await update(context: context)
}
let batchRange = batchRangeStart...batchRangeStart + config.batchSize
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)))
}
return await update(context: context)
}
func stop() async { }
}

View File

@ -0,0 +1,28 @@
//
// ValidateAction.swift
//
//
// Created by Michal Fousek on 05.05.2023.
//
import Foundation
final class ValidateAction {
let validator: BlockValidator
init(container: DIContainer) {
validator = container.resolve(BlockValidator.self)
}
}
extension ValidateAction: Action {
var removeBlocksCacheWhenFailed: Bool { true }
func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext {
try await validator.validate()
await context.update(state: .scan)
return context
}
func stop() async { }
}

View File

@ -0,0 +1,60 @@
//
// ValidateServerAction.swift
//
//
// Created by Michal Fousek on 05.05.2023.
//
import Foundation
final class ValidateServerAction {
let configProvider: CompactBlockProcessor.ConfigProvider
let rustBackend: ZcashRustBackendWelding
let service: LightWalletService
init(container: DIContainer, configProvider: CompactBlockProcessor.ConfigProvider) {
self.configProvider = configProvider
rustBackend = container.resolve(ZcashRustBackendWelding.self)
service = container.resolve(LightWalletService.self)
}
}
extension ValidateServerAction: Action {
var removeBlocksCacheWhenFailed: Bool { false }
func run(with context: ActionContext, didUpdate: @escaping (CompactBlockProcessor.Event) async -> Void) async throws -> ActionContext {
let config = await configProvider.config
let info = try await service.getInfo()
let localNetwork = config.network
let saplingActivation = config.saplingActivation
// check network types
guard let remoteNetworkType = NetworkType.forChainName(info.chainName) else {
throw ZcashError.compactBlockProcessorChainName(info.chainName)
}
guard remoteNetworkType == localNetwork.networkType else {
throw ZcashError.compactBlockProcessorNetworkMismatch(localNetwork.networkType, remoteNetworkType)
}
guard saplingActivation == info.saplingActivationHeight else {
throw ZcashError.compactBlockProcessorSaplingActivationMismatch(saplingActivation, BlockHeight(info.saplingActivationHeight))
}
// check branch id
let localBranch = try rustBackend.consensusBranchIdFor(height: Int32(info.blockHeight))
guard let remoteBranchID = ConsensusBranchID.fromString(info.consensusBranchID) else {
throw ZcashError.compactBlockProcessorConsensusBranchID
}
guard remoteBranchID == localBranch else {
throw ZcashError.compactBlockProcessorWrongConsensusBranchId(localBranch, remoteBranchID)
}
await context.update(state: .computeSyncRanges)
return context
}
func stop() async { }
}

File diff suppressed because it is too large Load Diff

View File

@ -250,6 +250,7 @@ extension BlockDownloaderImpl: BlockDownloader {
}
func setSyncRange(_ range: CompactBlockRange, batchSize: Int) async throws {
guard range != syncRange else { return }
downloadStream = nil
self.batchSize = batchSize
syncRange = range

View File

@ -7,8 +7,52 @@
import Foundation
public struct EnhancementProgress: Equatable {
/// total transactions that were detected in the `range`
public let totalTransactions: Int
/// enhanced transactions so far
public let enhancedTransactions: Int
/// last found transaction
public let lastFoundTransaction: ZcashTransaction.Overview?
/// block range that's being enhanced
public let range: CompactBlockRange
/// whether this transaction can be considered `newly mined` and not part of the
/// wallet catching up to stale and uneventful blocks.
public let newlyMined: Bool
public init(
totalTransactions: Int,
enhancedTransactions: Int,
lastFoundTransaction: ZcashTransaction.Overview?,
range: CompactBlockRange,
newlyMined: Bool
) {
self.totalTransactions = totalTransactions
self.enhancedTransactions = enhancedTransactions
self.lastFoundTransaction = lastFoundTransaction
self.range = range
self.newlyMined = newlyMined
}
public var progress: Float {
totalTransactions > 0 ? Float(enhancedTransactions) / Float(totalTransactions) : 0
}
public static var zero: EnhancementProgress {
EnhancementProgress(totalTransactions: 0, enhancedTransactions: 0, lastFoundTransaction: nil, range: 0...0, newlyMined: false)
}
public static func == (lhs: EnhancementProgress, rhs: EnhancementProgress) -> Bool {
return
lhs.totalTransactions == rhs.totalTransactions &&
lhs.enhancedTransactions == rhs.enhancedTransactions &&
lhs.lastFoundTransaction?.id == rhs.lastFoundTransaction?.id &&
lhs.range == rhs.range
}
}
protocol BlockEnhancer {
func enhance(at range: CompactBlockRange, didEnhance: (EnhancementProgress) async -> Void) async throws -> [ZcashTransaction.Overview]?
func enhance(at range: CompactBlockRange, didEnhance: @escaping (EnhancementProgress) async -> Void) async throws -> [ZcashTransaction.Overview]?
}
struct BlockEnhancerImpl {
@ -38,7 +82,7 @@ struct BlockEnhancerImpl {
}
extension BlockEnhancerImpl: BlockEnhancer {
func enhance(at range: CompactBlockRange, didEnhance: (EnhancementProgress) async -> Void) async throws -> [ZcashTransaction.Overview]? {
func enhance(at range: CompactBlockRange, didEnhance: @escaping (EnhancementProgress) async -> Void) async throws -> [ZcashTransaction.Overview]? {
try Task.checkCancellation()
logger.debug("Started Enhancing range: \(range)")

View File

@ -19,7 +19,7 @@ struct UTXOFetcherConfig {
protocol UTXOFetcher {
func fetch(
at range: CompactBlockRange,
didFetch: (Float) async -> Void
didFetch: @escaping (Float) async -> Void
) async throws -> (inserted: [UnspentTransactionOutputEntity], skipped: [UnspentTransactionOutputEntity])
}
@ -36,7 +36,7 @@ struct UTXOFetcherImpl {
extension UTXOFetcherImpl: UTXOFetcher {
func fetch(
at range: CompactBlockRange,
didFetch: (Float) async -> Void
didFetch: @escaping (Float) async -> Void
) async throws -> (inserted: [UnspentTransactionOutputEntity], skipped: [UnspentTransactionOutputEntity]) {
try Task.checkCancellation()

View File

@ -0,0 +1,64 @@
//
// CompactBlockProgress.swift
//
//
// Created by Michal Fousek on 11.05.2023.
//
import Foundation
final actor CompactBlockProgress {
static let zero = CompactBlockProgress()
enum Action: Equatable {
case enhance
case fetch
case scan
func weight() -> Float {
switch self {
case .enhance: return 0.08
case .fetch: return 0.02
case .scan: return 0.9
}
}
}
var actionProgresses: [Action: Float] = [:]
var progress: Float {
var overallProgress = Float(0)
actionProgresses.forEach { key, value in
overallProgress += value * key.weight()
}
return overallProgress
}
func event(_ event: CompactBlockProcessor.Event) -> Bool {
guard case .progressPartialUpdate(let update) = event else {
return false
}
switch update {
case .syncing(let progress):
actionProgresses[.scan] = progress.progress
case .enhance(let progress):
actionProgresses[.enhance] = progress.progress
case .fetch(let progress):
actionProgresses[.fetch] = progress
}
return true
}
func reset() {
actionProgresses.removeAll()
}
}
enum CompactBlockProgressUpdate: Equatable {
case syncing(_ progress: BlockProgress)
case enhance(_ progress: EnhancementProgress)
case fetch(_ progress: Float)
}

View File

@ -9,20 +9,35 @@ import Foundation
struct SyncRanges: Equatable {
let latestBlockHeight: BlockHeight
/// The sync process can be interrupted in any phase. It may happen that it's interrupted while downloading blocks. In that case in next sync
/// process already downloaded blocks needs to be scanned before the sync process starts to download new blocks. And the range of blocks that are
/// already downloaded but not scanned is stored in this variable.
let downloadedButUnscannedRange: CompactBlockRange?
/// Range of blocks that are not yet downloaded and not yet scanned.
let downloadAndScanRange: CompactBlockRange?
// Range of blocks that are not yet downloaded
let downloadRange: CompactBlockRange?
/// Range of blocks that are not yet scanned.
let scanRange: CompactBlockRange?
/// Range of blocks that are not enhanced yet.
let enhanceRange: CompactBlockRange?
/// Range of blocks for which no UTXOs are fetched yet.
let fetchUTXORange: CompactBlockRange?
let latestScannedHeight: BlockHeight?
let latestDownloadedBlockHeight: BlockHeight?
static var empty: SyncRanges {
SyncRanges(
latestBlockHeight: 0,
downloadRange: nil,
scanRange: nil,
enhanceRange: nil,
fetchUTXORange: nil,
latestScannedHeight: nil,
latestDownloadedBlockHeight: nil
)
}
}
enum NextState: Equatable {
case finishProcessing(height: BlockHeight)
case processNewBlocks(ranges: SyncRanges)
case wait(latestHeight: BlockHeight, latestDownloadHeight: BlockHeight)
}
protocol InternalSyncProgressStorage {
@ -30,6 +45,7 @@ protocol InternalSyncProgressStorage {
func bool(for key: String) async throws -> Bool
func integer(for key: String) async throws -> Int
func set(_ value: Int, for key: String) async throws
// sourcery: mockedName="setBool"
func set(_ value: Bool, for key: String) async throws
}
@ -132,7 +148,7 @@ actor InternalSyncProgress {
latestBlockHeight: BlockHeight,
latestScannedHeight: BlockHeight,
walletBirthday: BlockHeight
) async throws -> CompactBlockProcessor.NextState {
) async throws -> NextState {
let latestDownloadedBlockHeight = try await self.latestDownloadedBlockHeight
let latestEnhancedHeight = try await self.latestEnhancedHeight
let latestUTXOFetchedHeight = try await self.latestUTXOFetchedHeight
@ -174,15 +190,6 @@ actor InternalSyncProgress {
let latestEnhancedHeight = try await self.latestEnhancedHeight
let latestUTXOFetchedHeight = try await self.latestUTXOFetchedHeight
// If there is more downloaded then scanned blocks we have to range for these blocks. The sync process will then start with scanning these
// blocks instead of downloading new ones.
let downloadedButUnscannedRange: CompactBlockRange?
if latestScannedHeight < latestDownloadedBlockHeight {
downloadedButUnscannedRange = latestScannedHeight + 1...latestDownloadedBlockHeight
} else {
downloadedButUnscannedRange = nil
}
if latestScannedHeight > latestDownloadedBlockHeight {
logger.warn("""
InternalSyncProgress found inconsistent state.
@ -191,24 +198,16 @@ actor InternalSyncProgress {
latestScannedHeight: \(latestScannedHeight)
latestEnhancedHeight: \(latestEnhancedHeight)
latestUTXOFetchedHeight: \(latestUTXOFetchedHeight)
latest downloaded height
""")
}
// compute the range that must be downloaded and scanned based on
// birthday, `latestDownloadedBlockHeight`, `latestScannedHeight` and
// latest block height fetched from the chain.
let downloadAndScanRange = computeRange(
latestHeight: max(latestDownloadedBlockHeight, latestScannedHeight),
birthday: birthday,
latestBlockHeight: latestBlockHeight
)
let downloadRange = computeRange(latestHeight: latestDownloadedBlockHeight, birthday: birthday, latestBlockHeight: latestBlockHeight)
let scanRange = computeRange(latestHeight: latestScannedHeight, birthday: birthday, latestBlockHeight: latestBlockHeight)
return SyncRanges(
latestBlockHeight: latestBlockHeight,
downloadedButUnscannedRange: downloadedButUnscannedRange,
downloadAndScanRange: downloadAndScanRange,
downloadRange: downloadRange,
scanRange: scanRange,
enhanceRange: computeRange(latestHeight: latestEnhancedHeight, birthday: birthday, latestBlockHeight: latestBlockHeight),
fetchUTXORange: computeRange(latestHeight: latestUTXOFetchedHeight, birthday: birthday, latestBlockHeight: latestBlockHeight),
latestScannedHeight: latestScannedHeight,

View File

@ -88,17 +88,10 @@ public enum ZcashSDK {
// MARK: Defaults
/// Default size of batches of blocks to request from the compact block service. Which was used both for scanning and downloading.
/// consider basing your code assumptions on `DefaultDownloadBatch` and `DefaultScanningBatch` instead.
@available(*, deprecated, message: "this value is being deprecated in favor of `DefaultDownloadBatch` and `DefaultScanningBatch`")
public static let DefaultBatchSize = 100
/// Default batch size for downloading blocks for the compact block processor. Be careful with this number. This amount of blocks is held in
/// memory at some point of the sync process.
/// This values can't be smaller than `DefaultScanningBatch`. Otherwise bad things will happen.
public static let DefaultDownloadBatch = 100
/// Default batch size for scanning blocks for the compact block processor
public static let DefaultScanningBatch = 100
/// Default batch size for enhancing transactions for the compact block processor
public static let DefaultEnhanceBatch = 1000
/// Default amount of time, in in seconds, to poll for new blocks. Typically, this should be about half the average
/// block time.

View File

@ -180,6 +180,7 @@ protocol LightWalletService: AnyObject {
func fetchTransaction(txId: Data) async throws -> ZcashTransaction.Fetched
/// - Throws: `serviceFetchUTXOsFailed` when GRPC call fails.
// sourcery: mockedName="fetchUTXOsSingle"
func fetchUTXOs(for tAddress: String, height: BlockHeight) -> AsyncThrowingStream<UnspentTransactionOutputEntity, Error>
/// - Throws: `serviceFetchUTXOsFailed` when GRPC call fails.

View File

@ -366,15 +366,9 @@ enum InternalSyncStatus: Equatable {
/// taking other maintenance steps that need to occur after an upgrade.
case unprepared
case syncing(_ progress: BlockProgress)
/// Indicates that this Synchronizer is actively enhancing newly scanned blocks
/// with additional transaction details, fetched from the server.
case enhancing(_ progress: EnhancementProgress)
/// fetches the transparent balance and stores it locally
case fetching(_ progress: Float)
/// Indicates that this Synchronizer is actively processing new blocks (consists of fetch, scan and enhance operations)
case syncing(Float)
/// Indicates that this Synchronizer is fully up to date and ready for all wallet functions.
/// When set, a UI element may want to turn green.
case synced
@ -390,7 +384,7 @@ enum InternalSyncStatus: Equatable {
public var isSyncing: Bool {
switch self {
case .syncing, .enhancing, .fetching:
case .syncing:
return true
default:
return false
@ -416,8 +410,6 @@ enum InternalSyncStatus: Equatable {
switch self {
case .unprepared: return "unprepared"
case .syncing: return "syncing"
case .enhancing: return "enhancing"
case .fetching: return "fetching"
case .synced: return "synced"
case .stopped: return "stopped"
case .disconnected: return "disconnected"
@ -449,8 +441,6 @@ extension InternalSyncStatus {
switch (lhs, rhs) {
case (.unprepared, .unprepared): return true
case let (.syncing(lhsProgress), .syncing(rhsProgress)): return lhsProgress == rhsProgress
case let (.enhancing(lhsProgress), .enhancing(rhsProgress)): return lhsProgress == rhsProgress
case (.fetching, .fetching): return true
case (.synced, .synced): return true
case (.stopped, .stopped): return true
case (.disconnected, .disconnected): return true
@ -461,15 +451,8 @@ extension InternalSyncStatus {
}
extension InternalSyncStatus {
init(_ blockProcessorProgress: CompactBlockProgress) {
switch blockProcessorProgress {
case .syncing(let progressReport):
self = .syncing(progressReport)
case .enhance(let enhancingReport):
self = .enhancing(enhancingReport)
case .fetch(let fetchingProgress):
self = .fetching(fetchingProgress)
}
init(_ blockProcessorProgress: Float) {
self = .syncing(blockProcessorProgress)
}
}
@ -479,11 +462,7 @@ extension InternalSyncStatus {
case .unprepared:
return .unprepared
case .syncing(let progress):
return .syncing(0.9 * progress.progress)
case .enhancing(let progress):
return .syncing(0.9 + 0.08 * progress.progress)
case .fetching(let progress):
return .syncing(0.98 + 0.02 * progress)
return .syncing(progress)
case .synced:
return .upToDate
case .stopped:

View File

@ -94,6 +94,10 @@ enum Dependencies {
let storage = InternalSyncProgressDiskStorage(storageURL: urls.generalStorageURL, logger: logger)
return InternalSyncProgress(alias: alias, storage: storage, logger: logger)
}
container.register(type: ZcashFileManager.self, isSingleton: true) { _ in
FileManager.default
}
}
static func setupCompactBlockProcessor(
@ -140,7 +144,7 @@ enum Dependencies {
let blockScannerConfig = BlockScannerConfig(
networkType: config.network.networkType,
scanningBatchSize: config.scanningBatchSize
scanningBatchSize: config.batchSize
)
return BlockScannerImpl(

View File

@ -160,14 +160,14 @@ public class SDKSynchronizer: Synchronizer {
case .unprepared:
throw ZcashError.synchronizerNotPrepared
case .syncing, .enhancing, .fetching:
case .syncing:
logger.warn("warning: Synchronizer started when already running. Next sync process will be started when the current one stops.")
/// This may look strange but `CompactBlockProcessor` has mechanisms which can handle this situation. So we are fine with calling
/// it's start here.
await blockProcessor.start(retry: retry)
case .stopped, .synced, .disconnected, .error:
await updateStatus(.syncing(.nullProgress))
await updateStatus(.syncing(0))
syncStartDate = Date()
await blockProcessor.start(retry: retry)
}
@ -200,15 +200,14 @@ public class SDKSynchronizer: Synchronizer {
// MARK: Handle CompactBlockProcessor.Flow
// swiftlint:disable:next cyclomatic_complexity
private func subscribeToProcessorEvents(_ processor: CompactBlockProcessor) async {
let eventClosure: CompactBlockProcessor.EventClosure = { [weak self] event in
switch event {
case let .failed(error):
await self?.failed(error: error)
case let .finished(height, foundBlocks):
await self?.finished(lastScannedHeight: height, foundBlocks: foundBlocks)
case let .finished(height):
await self?.finished(lastScannedHeight: height)
case let .foundTransactions(transactions, range):
self?.foundTransactions(transactions: transactions, in: range)
@ -220,17 +219,14 @@ public class SDKSynchronizer: Synchronizer {
case let .progressUpdated(progress):
await self?.progressUpdated(progress: progress)
case .progressPartialUpdate:
break
case let .storedUTXOs(utxos):
self?.storedUTXOs(utxos: utxos)
case .startedEnhancing:
await self?.updateStatus(.enhancing(.zero))
case .startedFetching:
await self?.updateStatus(.fetching(0))
case .startedSyncing:
await self?.updateStatus(.syncing(.nullProgress))
case .startedEnhancing, .startedFetching, .startedSyncing:
break
case .stopped:
await self?.updateStatus(.stopped)
@ -247,7 +243,7 @@ public class SDKSynchronizer: Synchronizer {
await updateStatus(.error(error))
}
private func finished(lastScannedHeight: BlockHeight, foundBlocks: Bool) async {
private func finished(lastScannedHeight: BlockHeight) async {
await latestBlocksDataProvider.updateScannedData()
await updateStatus(.synced)
@ -266,7 +262,7 @@ public class SDKSynchronizer: Synchronizer {
}
}
private func progressUpdated(progress: CompactBlockProgress) async {
private func progressUpdated(progress: Float) async {
let newStatus = InternalSyncStatus(progress)
await updateStatus(newStatus)
}
@ -407,7 +403,7 @@ public class SDKSynchronizer: Synchronizer {
}
public func latestHeight() async throws -> BlockHeight {
try await blockProcessor.blockDownloaderService.latestBlockHeight()
try await blockProcessor.latestHeight()
}
public func latestUTXOs(address: String) async throws -> [UnspentTransactionOutputEntity] {
@ -640,8 +636,6 @@ extension InternalSyncStatus {
switch (self, otherStatus) {
case (.unprepared, .unprepared): return false
case (.syncing, .syncing): return false
case (.enhancing, .enhancing): return false
case (.fetching, .fetching): return false
case (.synced, .synced): return false
case (.stopped, .stopped): return false
case (.disconnected, .disconnected): return false

View File

@ -0,0 +1,16 @@
//
// ZcashFileManager.swift
//
//
// Created by Lukáš Korba on 23.05.2023.
//
import Foundation
protocol ZcashFileManager {
func isReadableFile(atPath path: String) -> Bool
func removeItem(at URL: URL) throws
func isDeletableFile(atPath path: String) -> Bool
}
extension FileManager: ZcashFileManager { }

View File

@ -59,7 +59,7 @@ class SDKSynchronizerAliasDarksideTests: ZcashTestCase {
endpoint: endpoint
)
try coordinator.reset(saplingActivation: birthday, branchID: branchID, chainName: chainName)
try await coordinator.reset(saplingActivation: birthday, branchID: branchID, chainName: chainName)
coordinators.append(coordinator)
}

View File

@ -33,7 +33,7 @@ class AdvancedReOrgTests: ZcashTestCase {
walletBirthday: birthday + 50,
network: network
)
try coordinator.reset(saplingActivation: 663150, branchID: self.branchID, chainName: self.chainName)
try await coordinator.reset(saplingActivation: 663150, branchID: self.branchID, chainName: self.chainName)
}
override func tearDown() async throws {
@ -453,7 +453,7 @@ class AdvancedReOrgTests: ZcashTestCase {
await hookToReOrgNotification()
self.expectedReorgHeight = 663196
self.expectedRewindHeight = 663175
try coordinator.reset(saplingActivation: birthday, branchID: "2bb40e60", chainName: "main")
try await coordinator.reset(saplingActivation: birthday, branchID: "2bb40e60", chainName: "main")
try coordinator.resetBlocks(dataset: .predefined(dataset: .txIndexChangeBefore))
try coordinator.applyStaged(blockheight: 663195)
sleep(1)
@ -1031,7 +1031,7 @@ class AdvancedReOrgTests: ZcashTestCase {
/// 8. sync to latest height
/// 9. verify that the balance is equal to the one before the reorg
func testReOrgChangesInboundMinedHeight() async throws {
try coordinator.reset(saplingActivation: 663150, branchID: branchID, chainName: chainName)
try await coordinator.reset(saplingActivation: 663150, branchID: branchID, chainName: chainName)
sleep(2)
try coordinator.resetBlocks(dataset: .predefined(dataset: .txHeightReOrgBefore))
sleep(2)
@ -1096,7 +1096,7 @@ class AdvancedReOrgTests: ZcashTestCase {
// FIXME [#644]: Test works with lightwalletd v0.4.13 but is broken when using newer lightwalletd. More info is in #644.
func testReOrgRemovesIncomingTxForever() async throws {
await hookToReOrgNotification()
try coordinator.reset(saplingActivation: 663150, branchID: branchID, chainName: chainName)
try await coordinator.reset(saplingActivation: 663150, branchID: branchID, chainName: chainName)
try coordinator.resetBlocks(dataset: .predefined(dataset: .txReOrgRemovesInboundTxBefore))
@ -1332,7 +1332,7 @@ class AdvancedReOrgTests: ZcashTestCase {
XCTAssertEqual(expectedBalance, initialTotalBalance)
}
func testLongSync() async throws {
func asdastestLongSync() async throws {
await hookToReOrgNotification()
/*

View File

@ -30,7 +30,7 @@ class BalanceTests: ZcashTestCase {
walletBirthday: birthday,
network: network
)
try coordinator.reset(saplingActivation: 663150, branchID: "e9ff75a6", chainName: "main")
try await coordinator.reset(saplingActivation: 663150, branchID: "e9ff75a6", chainName: "main")
}
override func tearDown() async throws {

View File

@ -33,7 +33,7 @@ class DarksideSanityCheckTests: ZcashTestCase {
network: network
)
try self.coordinator.reset(saplingActivation: self.birthday, branchID: self.branchID, chainName: self.chainName)
try await coordinator.reset(saplingActivation: self.birthday, branchID: self.branchID, chainName: self.chainName)
try self.coordinator.resetBlocks(dataset: .default)
}

View File

@ -1,144 +0,0 @@
//
// InternalStateConsistencyTests.swift
// DarksideTests
//
// Created by Francisco Gindre on 1/26/23.
//
import Combine
import XCTest
@testable import TestUtils
@testable import ZcashLightClientKit
final class InternalStateConsistencyTests: ZcashTestCase {
let sendAmount = Zatoshi(1000)
var birthday: BlockHeight = 663150
let defaultLatestHeight: BlockHeight = 663175
var coordinator: TestCoordinator!
var firstSyncExpectation = XCTestExpectation(description: "first sync expectation")
var expectedReorgHeight: BlockHeight = 665188
var expectedRewindHeight: BlockHeight = 665188
var reorgExpectation = XCTestExpectation(description: "reorg")
let branchID = "2bb40e60"
let chainName = "main"
let network = DarksideWalletDNetwork()
var sdkSynchronizerInternalSyncStatusHandler: SDKSynchronizerInternalSyncStatusHandler! = SDKSynchronizerInternalSyncStatusHandler()
override func setUp() async throws {
try await super.setUp()
// don't use an exact birthday, users never do.
self.coordinator = try await TestCoordinator(
container: mockContainer,
walletBirthday: birthday + 50,
network: network
)
try coordinator.reset(saplingActivation: 663150, branchID: self.branchID, chainName: self.chainName)
}
override func tearDown() async throws {
try await super.tearDown()
let coordinator = self.coordinator!
self.coordinator = nil
sdkSynchronizerInternalSyncStatusHandler = nil
try await coordinator.stop()
try? FileManager.default.removeItem(at: coordinator.databases.fsCacheDbRoot)
try? FileManager.default.removeItem(at: coordinator.databases.dataDB)
}
func testInternalStateIsConsistentWhenMigrating() async throws {
sdkSynchronizerInternalSyncStatusHandler.subscribe(
to: coordinator.synchronizer.stateStream,
expectations: [.stopped: firstSyncExpectation]
)
let fullSyncLength = 10000
try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName, length: fullSyncLength)
sleep(1)
// apply the height
try coordinator.applyStaged(blockheight: 664150)
sleep(1)
try await coordinator.sync(
completion: { _ in
XCTFail("shouldn't have completed")
},
error: handleError
)
let coordinator = self.coordinator!
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
Task(priority: .userInitiated) {
coordinator.synchronizer.stop()
}
}
await fulfillment(of: [firstSyncExpectation], timeout: 2)
let isSyncing = await coordinator.synchronizer.status.isSyncing
let status = await coordinator.synchronizer.status
XCTAssertFalse(isSyncing, "SDKSynchronizer shouldn't be syncing")
XCTAssertEqual(status, .stopped)
let internalSyncState = coordinator.synchronizer.internalSyncProgress
let latestDownloadHeight = try await internalSyncState.latestDownloadedBlockHeight
let latestScanHeight = try await coordinator.synchronizer.initializer.transactionRepository.lastScannedHeight()
let dbHandle = TestDbHandle(originalDb: TestDbBuilder.prePopulatedDarksideCacheDb()!)
try dbHandle.setUp()
if latestDownloadHeight > latestScanHeight {
try await coordinator.synchronizer.blockProcessor.migrateCacheDb(dbHandle.readWriteDb)
let afterMigrationDownloadedHeight = try await internalSyncState.latestDownloadedBlockHeight
XCTAssertNotEqual(latestDownloadHeight, afterMigrationDownloadedHeight)
XCTAssertEqual(latestScanHeight, afterMigrationDownloadedHeight)
} else {
try await coordinator.synchronizer.blockProcessor.migrateCacheDb(dbHandle.readWriteDb)
let afterMigrationDownloadedHeight = try await internalSyncState.latestDownloadedBlockHeight
XCTAssertEqual(latestDownloadHeight, afterMigrationDownloadedHeight)
XCTAssertEqual(latestScanHeight, afterMigrationDownloadedHeight)
}
XCTAssertFalse(FileManager.default.isReadableFile(atPath: dbHandle.readWriteDb.path))
// clear to simulate a clean slate from the FsBlockDb
try await coordinator.synchronizer.blockProcessor.storage.clear()
// Now let's resume scanning and see how it goes.
let secondSyncAttemptExpectation = XCTestExpectation(description: "second sync attempt")
do {
try await coordinator.sync(
completion: { _ in
XCTAssertTrue(true)
secondSyncAttemptExpectation.fulfill()
},
error: { [weak self] error in
secondSyncAttemptExpectation.fulfill()
self?.handleError(error)
}
)
} catch {
handleError(error)
}
await fulfillment(of: [secondSyncAttemptExpectation], timeout: 10)
}
func handleError(_ error: Error?) {
guard let testError = error else {
XCTFail("failed with nil error")
return
}
XCTFail("Failed with error: \(testError)")
}
}

View File

@ -30,7 +30,7 @@ class PendingTransactionUpdatesTest: ZcashTestCase {
walletBirthday: birthday,
network: network
)
try self.coordinator.reset(saplingActivation: 663150, branchID: "e9ff75a6", chainName: "main")
try await coordinator.reset(saplingActivation: 663150, branchID: "e9ff75a6", chainName: "main")
}
override func tearDown() async throws {

View File

@ -50,7 +50,7 @@ class ReOrgTests: ZcashTestCase {
network: self.network
)
try self.coordinator.reset(saplingActivation: self.birthday, branchID: self.branchID, chainName: self.chainName)
try await coordinator.reset(saplingActivation: self.birthday, branchID: self.branchID, chainName: self.chainName)
try self.coordinator.resetBlocks(dataset: .default)
@ -128,7 +128,7 @@ class ReOrgTests: ZcashTestCase {
targetHeight: BlockHeight
) async throws {
do {
try coordinator.reset(saplingActivation: birthday, branchID: branchID, chainName: chainName)
try await coordinator.reset(saplingActivation: birthday, branchID: branchID, chainName: chainName)
try coordinator.resetBlocks(dataset: .predefined(dataset: .beforeReOrg))
try coordinator.applyStaged(blockheight: firstLatestHeight)
sleep(1)

View File

@ -35,7 +35,7 @@ class RewindRescanTests: ZcashTestCase {
walletBirthday: birthday,
network: network
)
try self.coordinator.reset(saplingActivation: 663150, branchID: "e9ff75a6", chainName: "main")
try await coordinator.reset(saplingActivation: 663150, branchID: "e9ff75a6", chainName: "main")
}
override func tearDown() async throws {

View File

@ -30,7 +30,7 @@ class ShieldFundsTests: ZcashTestCase {
walletBirthday: birthday,
network: network
)
try coordinator.reset(saplingActivation: birthday, branchID: self.branchID, chainName: self.chainName)
try await coordinator.reset(saplingActivation: birthday, branchID: self.branchID, chainName: self.chainName)
try coordinator.service.clearAddedUTXOs()
}

View File

@ -40,7 +40,7 @@ class SynchronizerDarksideTests: ZcashTestCase {
network: network
)
try self.coordinator.reset(saplingActivation: 663150, branchID: "e9ff75a6", chainName: "main")
try await coordinator.reset(saplingActivation: 663150, branchID: "e9ff75a6", chainName: "main")
}
override func tearDown() async throws {
@ -195,7 +195,7 @@ class SynchronizerDarksideTests: ZcashTestCase {
syncSessionID: uuids[0],
shieldedBalance: .zero,
transparentBalance: .zero,
internalSyncStatus: .syncing(BlockProgress(startHeight: 0, targetHeight: 0, progressHeight: 0)),
internalSyncStatus: .syncing(0),
latestScannedHeight: 663150,
latestBlockHeight: 0,
latestScannedTime: 1576821833
@ -204,93 +204,7 @@ class SynchronizerDarksideTests: ZcashTestCase {
syncSessionID: uuids[0],
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
transparentBalance: .zero,
internalSyncStatus: .syncing(BlockProgress(startHeight: 663150, targetHeight: 663189, progressHeight: 663189)),
latestScannedHeight: 663189,
latestBlockHeight: 663189,
latestScannedTime: 1
),
SynchronizerState(
syncSessionID: uuids[0],
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
transparentBalance: .zero,
internalSyncStatus: .enhancing(
EnhancementProgress(totalTransactions: 0, enhancedTransactions: 0, lastFoundTransaction: nil, range: 0...0, newlyMined: false)
),
latestScannedHeight: 663189,
latestBlockHeight: 663189,
latestScannedTime: 1
),
SynchronizerState(
syncSessionID: uuids[0],
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
transparentBalance: .zero,
internalSyncStatus: .enhancing(
EnhancementProgress(
totalTransactions: 2,
enhancedTransactions: 1,
lastFoundTransaction: ZcashTransaction.Overview(
accountId: 0,
blockTime: 1.0,
expiryHeight: 663206,
fee: Zatoshi(0),
id: 2,
index: 1,
hasChange: false,
memoCount: 1,
minedHeight: 663188,
raw: Data(),
rawID: Data(),
receivedNoteCount: 1,
sentNoteCount: 0,
value: Zatoshi(100000),
isExpiredUmined: false
),
range: 663150...663189,
newlyMined: true
)
),
latestScannedHeight: 663189,
latestBlockHeight: 663189,
latestScannedTime: 1
),
SynchronizerState(
syncSessionID: uuids[0],
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
transparentBalance: .zero,
internalSyncStatus: .enhancing(
EnhancementProgress(
totalTransactions: 2,
enhancedTransactions: 2,
lastFoundTransaction: ZcashTransaction.Overview(
accountId: 0,
blockTime: 1.0,
expiryHeight: 663192,
fee: Zatoshi(0),
id: 1,
index: 1,
hasChange: false,
memoCount: 1,
minedHeight: 663174,
raw: Data(),
rawID: Data(),
receivedNoteCount: 1,
sentNoteCount: 0,
value: Zatoshi(100000),
isExpiredUmined: false
),
range: 663150...663189,
newlyMined: true
)
),
latestScannedHeight: 663189,
latestBlockHeight: 663189,
latestScannedTime: 1
),
SynchronizerState(
syncSessionID: uuids[0],
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
transparentBalance: .zero,
internalSyncStatus: .fetching(0),
internalSyncStatus: .syncing(0.9),
latestScannedHeight: 663189,
latestBlockHeight: 663189,
latestScannedTime: 1
@ -359,7 +273,7 @@ class SynchronizerDarksideTests: ZcashTestCase {
syncSessionID: uuids[0],
shieldedBalance: .zero,
transparentBalance: .zero,
internalSyncStatus: .syncing(BlockProgress(startHeight: 0, targetHeight: 0, progressHeight: 0)),
internalSyncStatus: .syncing(0),
latestScannedHeight: 663150,
latestBlockHeight: 0,
latestScannedTime: 1576821833.0
@ -368,93 +282,7 @@ class SynchronizerDarksideTests: ZcashTestCase {
syncSessionID: uuids[0],
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
transparentBalance: .zero,
internalSyncStatus: .syncing(BlockProgress(startHeight: 663150, targetHeight: 663189, progressHeight: 663189)),
latestScannedHeight: 663189,
latestBlockHeight: 663189,
latestScannedTime: 1
),
SynchronizerState(
syncSessionID: uuids[0],
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
internalSyncStatus: .enhancing(
EnhancementProgress(totalTransactions: 0, enhancedTransactions: 0, lastFoundTransaction: nil, range: 0...0, newlyMined: false)
),
latestScannedHeight: 663189,
latestBlockHeight: 663189,
latestScannedTime: 1
),
SynchronizerState(
syncSessionID: uuids[0],
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
internalSyncStatus: .enhancing(
EnhancementProgress(
totalTransactions: 2,
enhancedTransactions: 1,
lastFoundTransaction: ZcashTransaction.Overview(
accountId: 0,
blockTime: 1.0,
expiryHeight: 663206,
fee: nil,
id: 2,
index: 1,
hasChange: false,
memoCount: 1,
minedHeight: 663188,
raw: Data(),
rawID: Data(),
receivedNoteCount: 1,
sentNoteCount: 0,
value: Zatoshi(100000),
isExpiredUmined: false
),
range: 663150...663189,
newlyMined: true
)
),
latestScannedHeight: 663189,
latestBlockHeight: 663189,
latestScannedTime: 1
),
SynchronizerState(
syncSessionID: uuids[0],
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
internalSyncStatus: .enhancing(
EnhancementProgress(
totalTransactions: 2,
enhancedTransactions: 2,
lastFoundTransaction: ZcashTransaction.Overview(
accountId: 0,
blockTime: 1.0,
expiryHeight: 663192,
fee: nil,
id: 1,
index: 1,
hasChange: false,
memoCount: 1,
minedHeight: 663174,
raw: Data(),
rawID: Data(),
receivedNoteCount: 1,
sentNoteCount: 0,
value: Zatoshi(100000),
isExpiredUmined: false
),
range: 663150...663189,
newlyMined: true
)
),
latestScannedHeight: 663189,
latestBlockHeight: 663189,
latestScannedTime: 1
),
SynchronizerState(
syncSessionID: uuids[0],
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
internalSyncStatus: .fetching(0),
internalSyncStatus: .syncing(0.9),
latestScannedHeight: 663189,
latestBlockHeight: 663189,
latestScannedTime: 1
@ -499,7 +327,7 @@ class SynchronizerDarksideTests: ZcashTestCase {
syncSessionID: uuids[1],
shieldedBalance: WalletBalance(verified: Zatoshi(100000), total: Zatoshi(200000)),
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
internalSyncStatus: .syncing(BlockProgress(startHeight: 0, targetHeight: 0, progressHeight: 0)),
internalSyncStatus: .syncing(0),
latestScannedHeight: 663189,
latestBlockHeight: 663189,
latestScannedTime: 1.0
@ -508,27 +336,7 @@ class SynchronizerDarksideTests: ZcashTestCase {
syncSessionID: uuids[1],
shieldedBalance: WalletBalance(verified: Zatoshi(200000), total: Zatoshi(200000)),
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
internalSyncStatus: .syncing(BlockProgress(startHeight: 663190, targetHeight: 663200, progressHeight: 663200)),
latestScannedHeight: 663200,
latestBlockHeight: 663200,
latestScannedTime: 1
),
SynchronizerState(
syncSessionID: uuids[1],
shieldedBalance: WalletBalance(verified: Zatoshi(200000), total: Zatoshi(200000)),
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
internalSyncStatus: .enhancing(
EnhancementProgress(totalTransactions: 0, enhancedTransactions: 0, lastFoundTransaction: nil, range: 0...0, newlyMined: true)
),
latestScannedHeight: 663200,
latestBlockHeight: 663200,
latestScannedTime: 1
),
SynchronizerState(
syncSessionID: uuids[1],
shieldedBalance: WalletBalance(verified: Zatoshi(200000), total: Zatoshi(200000)),
transparentBalance: WalletBalance(verified: Zatoshi(0), total: Zatoshi(0)),
internalSyncStatus: .fetching(0),
internalSyncStatus: .syncing(0.9),
latestScannedHeight: 663200,
latestBlockHeight: 663200,
latestScannedTime: 1

View File

@ -33,7 +33,7 @@ final class SynchronizerTests: ZcashTestCase {
walletBirthday: birthday + 50,
network: network
)
try coordinator.reset(saplingActivation: 663150, branchID: self.branchID, chainName: self.chainName)
try await coordinator.reset(saplingActivation: 663150, branchID: self.branchID, chainName: self.chainName)
let eventClosure: CompactBlockProcessor.EventClosure = { [weak self] event in
switch event {
@ -101,8 +101,6 @@ final class SynchronizerTests: ZcashTestCase {
let status = await coordinator.synchronizer.status
XCTAssertEqual(status, .stopped)
let state = await coordinator.synchronizer.blockProcessor.state
XCTAssertEqual(state, .stopped)
}
// MARK: Wipe tests
@ -188,8 +186,13 @@ final class SynchronizerTests: ZcashTestCase {
try await Task.sleep(nanoseconds: 2_000_000_000)
// Just to be sure that blockProcessor is still syncing and that this test does what it should.
let blockProcessorState = await coordinator.synchronizer.blockProcessor.state
XCTAssertEqual(blockProcessorState, .syncing)
let synchronizerState = coordinator.synchronizer.latestState.syncStatus
switch synchronizerState {
case .syncing:
break
default:
XCTFail("Synchornizer should be in syncing state.")
}
let wipeFinished = XCTestExpectation(description: "SynchronizerWipeFinished Expectation")
/*
@ -223,8 +226,7 @@ final class SynchronizerTests: ZcashTestCase {
private func checkThatWipeWorked() async throws {
let storage = await self.coordinator.synchronizer.blockProcessor.storage as! FSCompactBlockRepository
let fm = FileManager.default
print(coordinator.synchronizer.initializer.dataDbURL.path)
XCTAssertFalse(fm.fileExists(atPath: coordinator.synchronizer.initializer.dataDbURL.path), "Data DB should be deleted.")
XCTAssertTrue(fm.fileExists(atPath: storage.blocksDirectory.path), "FS Cache directory should exist")
XCTAssertEqual(try fm.contentsOfDirectory(atPath: storage.blocksDirectory.path), [], "FS Cache directory should be empty")
@ -239,9 +241,6 @@ final class SynchronizerTests: ZcashTestCase {
XCTAssertEqual(latestEnhancedHeight, 0, "internalSyncProgress latestEnhancedHeight should be 0")
XCTAssertEqual(latestUTXOFetchedHeight, 0, "internalSyncProgress latestUTXOFetchedHeight should be 0")
let blockProcessorState = await coordinator.synchronizer.blockProcessor.state
XCTAssertEqual(blockProcessorState, .stopped, "CompactBlockProcessor state should be stopped")
let status = await coordinator.synchronizer.status
XCTAssertEqual(status, .unprepared, "SDKSynchronizer state should be unprepared")
}

View File

@ -33,7 +33,7 @@ class Z2TReceiveTests: ZcashTestCase {
walletBirthday: birthday,
network: network
)
try coordinator.reset(saplingActivation: 663150, branchID: self.branchID, chainName: self.chainName)
try await coordinator.reset(saplingActivation: 663150, branchID: self.branchID, chainName: self.chainName)
}
override func tearDown() async throws {

View File

@ -1,223 +0,0 @@
//
// BlockScanTests.swift
// ZcashLightClientKitTests
//
// Created by Francisco Gindre on 10/17/19.
// Copyright © 2019 Electric Coin Company. All rights reserved.
//
import Combine
import XCTest
import SQLite
@testable import TestUtils
@testable import ZcashLightClientKit
class BlockScanTests: ZcashTestCase {
var cancelables: [AnyCancellable] = []
var dataDbURL: URL!
var spendParamsURL: URL!
var outputParamsURL: URL!
// swiftlint:disable:next line_length
var saplingExtendedKey = SaplingExtendedFullViewingKey(validatedEncoding: "zxviewtestsapling1qw88ayg8qqqqpqyhg7jnh9mlldejfqwu46pm40ruwstd8znq3v3l4hjf33qcu2a5e36katshcfhcxhzgyfugj2lkhmt40j45cv38rv3frnghzkxcx73k7m7afw9j7ujk7nm4dx5mv02r26umxqgar7v3x390w2h3crqqgjsjly7jy4vtwzrmustm5yudpgcydw7x78awca8wqjvkqj8p8e3ykt7lrgd7xf92fsfqjs5vegfsja4ekzpfh5vtccgvs5747xqm6qflmtqpr8s9u")
var walletBirthDay = Checkpoint.birthday(
with: 1386000,
network: ZcashNetworkBuilder.network(for: .testnet)
)
var rustBackend: ZcashRustBackendWelding!
var network = ZcashNetworkBuilder.network(for: .testnet)
var blockRepository: BlockRepository!
let testFileManager = FileManager()
override func setUp() async throws {
try await super.setUp()
logger = OSLogger(logLevel: .debug)
dataDbURL = try! __dataDbURL()
spendParamsURL = try! __spendParamsURL()
outputParamsURL = try! __outputParamsURL()
rustBackend = ZcashRustBackend.makeForTests(
dbData: dataDbURL,
fsBlockDbRoot: testTempDirectory,
networkType: network.networkType
)
deleteDBs()
Dependencies.setup(
in: mockContainer,
urls: Initializer.URLs(
fsBlockDbRoot: testTempDirectory,
dataDbURL: dataDbURL,
generalStorageURL: testGeneralStorageDirectory,
spendParamsURL: spendParamsURL,
outputParamsURL: outputParamsURL
),
alias: .default,
networkType: .testnet,
endpoint: LightWalletEndpointBuilder.default,
loggingPolicy: .default(.debug)
)
mockContainer.mock(type: LatestBlocksDataProvider.self, isSingleton: true) { _ in LatestBlocksDataProviderMock() }
mockContainer.mock(type: ZcashRustBackendWelding.self, isSingleton: true) { _ in self.rustBackend }
}
private func deleteDBs() {
try? FileManager.default.removeItem(at: dataDbURL)
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
try super.tearDownWithError()
try? testFileManager.removeItem(at: dataDbURL)
try? testFileManager.removeItem(at: spendParamsURL)
try? testFileManager.removeItem(at: outputParamsURL)
cancelables = []
blockRepository = nil
testTempDirectory = nil
}
func testSingleDownloadAndScan() async throws {
_ = try await rustBackend.initDataDb(seed: nil)
let endpoint = LightWalletEndpoint(address: "lightwalletd.testnet.electriccoin.co", port: 9067)
let blockCount = 100
let range = network.constants.saplingActivationHeight ... network.constants.saplingActivationHeight + blockCount
let processorConfig = CompactBlockProcessor.Configuration(
alias: .default,
fsBlockCacheRoot: testTempDirectory,
dataDb: dataDbURL,
spendParamsURL: spendParamsURL,
outputParamsURL: outputParamsURL,
saplingParamsSourceURL: SaplingParamsSourceURL.tests,
walletBirthdayProvider: { [weak self] in self?.walletBirthDay.height ?? .zero },
network: network
)
mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in
LightWalletServiceFactory(endpoint: endpoint).make()
}
try await mockContainer.resolve(CompactBlockRepository.self).create()
let compactBlockProcessor = CompactBlockProcessor(container: mockContainer, config: processorConfig)
let repository = BlockSQLDAO(dbProvider: SimpleConnectionProvider.init(path: self.dataDbURL.absoluteString, readonly: true))
var latestScannedheight = BlockHeight.empty()
try await compactBlockProcessor.blockDownloaderService.downloadBlockRange(range)
XCTAssertFalse(Task.isCancelled)
try await compactBlockProcessor.blockScanner.scanBlocks(at: range, totalProgressRange: range, didScan: { _ in })
latestScannedheight = repository.lastScannedBlockHeight()
XCTAssertEqual(latestScannedheight, range.upperBound)
await compactBlockProcessor.stop()
}
func observeBenchmark(_ metrics: SDKMetrics) {
let reports = metrics.popAllBlockReports(flush: true)
reports.forEach {
print("observed benchmark: \($0)")
}
}
func testScanValidateDownload() async throws {
let seed = "testreferencealicetestreferencealice"
let metrics = SDKMetrics()
metrics.enableMetrics()
guard try await rustBackend.initDataDb(seed: nil) == .success else {
XCTFail("Seed should not be required for this test")
return
}
let derivationTool = DerivationTool(networkType: .testnet)
let spendingKey = try derivationTool.deriveUnifiedSpendingKey(seed: Array(seed.utf8), accountIndex: 0)
let viewingKey = try derivationTool.deriveUnifiedFullViewingKey(from: spendingKey)
do {
try await rustBackend.initAccountsTable(ufvks: [viewingKey])
} catch {
XCTFail("failed to init account table. error: \(error)")
return
}
try await rustBackend.initBlocksTable(
height: Int32(walletBirthDay.height),
hash: walletBirthDay.hash,
time: walletBirthDay.time,
saplingTree: walletBirthDay.saplingTree
)
let processorConfig = CompactBlockProcessor.Configuration(
alias: .default,
fsBlockCacheRoot: testTempDirectory,
dataDb: dataDbURL,
spendParamsURL: spendParamsURL,
outputParamsURL: outputParamsURL,
saplingParamsSourceURL: SaplingParamsSourceURL.tests,
downloadBatchSize: 1000,
scanningBatchSize: 1000,
walletBirthdayProvider: { [weak self] in self?.network.constants.saplingActivationHeight ?? .zero },
network: network
)
mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in
LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.eccTestnet).make()
}
try await mockContainer.resolve(CompactBlockRepository.self).create()
let compactBlockProcessor = CompactBlockProcessor(container: mockContainer, config: processorConfig)
let eventClosure: CompactBlockProcessor.EventClosure = { [weak self] event in
switch event {
case .progressUpdated: self?.observeBenchmark(metrics)
default: break
}
}
await compactBlockProcessor.updateEventClosure(identifier: "tests", closure: eventClosure)
let range = CompactBlockRange(
uncheckedBounds: (walletBirthDay.height, walletBirthDay.height + 10000)
)
do {
let blockDownloader = await compactBlockProcessor.blockDownloader
await blockDownloader.setDownloadLimit(range.upperBound)
try await blockDownloader.setSyncRange(range, batchSize: 100)
await blockDownloader.startDownload(maxBlockBufferSize: 10)
try await blockDownloader.waitUntilRequestedBlocksAreDownloaded(in: range)
XCTAssertFalse(Task.isCancelled)
try await compactBlockProcessor.blockValidator.validate()
XCTAssertFalse(Task.isCancelled)
try await compactBlockProcessor.blockScanner.scanBlocks(at: range, totalProgressRange: range, didScan: { _ in })
XCTAssertFalse(Task.isCancelled)
} catch {
if let lwdError = error as? ZcashError {
switch lwdError {
case .serviceBlockStreamFailed:
XCTAssert(true)
default:
XCTFail("LWD Service error found, but should have been a timeLimit reached Error - \(lwdError)")
}
} else {
XCTFail("Error should have been a timeLimit reached Error - \(error)")
}
}
await compactBlockProcessor.stop()
metrics.disableMetrics()
}
}

View File

@ -12,6 +12,13 @@ import XCTest
class BlockStreamingTest: ZcashTestCase {
let testFileManager = FileManager()
var rustBackend: ZcashRustBackendWelding!
var endpoint: LightWalletEndpoint!
var service: LightWalletService!
var storage: FSCompactBlockRepository!
var internalSyncProgress: InternalSyncProgress!
var processorConfig: CompactBlockProcessor.Configuration!
var latestBlockHeight: BlockHeight!
var startHeight: BlockHeight!
override func setUp() async throws {
try await super.setUp()
@ -37,45 +44,7 @@ class BlockStreamingTest: ZcashTestCase {
mockContainer.mock(type: LatestBlocksDataProvider.self, isSingleton: true) { _ in LatestBlocksDataProviderMock() }
mockContainer.mock(type: ZcashRustBackendWelding.self, isSingleton: true) { _ in self.rustBackend }
}
override func tearDownWithError() throws {
try super.tearDownWithError()
rustBackend = nil
try? FileManager.default.removeItem(at: __dataDbURL())
testTempDirectory = nil
}
func testStream() async throws {
let endpoint = LightWalletEndpoint(
address: LightWalletEndpointBuilder.eccTestnet.host,
port: 9067,
secure: true,
singleCallTimeoutInMillis: 1000,
streamingCallTimeoutInMillis: 100000
)
let service = LightWalletServiceFactory(endpoint: endpoint).make()
let latestHeight = try await service.latestBlockHeight()
let startHeight = latestHeight - 100_000
var blocks: [ZcashCompactBlock] = []
let stream = service.blockStream(startHeight: startHeight, endHeight: latestHeight)
do {
for try await compactBlock in stream {
print("received block \(compactBlock.height)")
blocks.append(compactBlock)
print("progressHeight: \(compactBlock.height)")
print("startHeight: \(startHeight)")
print("targetHeight: \(latestHeight)")
}
} catch {
XCTFail("failed with error: \(error)")
}
}
func testStreamCancellation() async throws {
let endpoint = LightWalletEndpoint(
address: LightWalletEndpointBuilder.eccTestnet.host,
port: 9067,
@ -85,51 +54,49 @@ class BlockStreamingTest: ZcashTestCase {
)
let service = LightWalletServiceFactory(endpoint: endpoint).make()
let latestBlockHeight = try await service.latestBlockHeight()
let startHeight = latestBlockHeight - 100_000
let processorConfig = CompactBlockProcessor.Configuration.standard(
for: ZcashNetworkBuilder.network(for: .testnet),
walletBirthday: ZcashNetworkBuilder.network(for: .testnet).constants.saplingActivationHeight
)
mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in
LightWalletServiceFactory(endpoint: endpoint).make()
}
try await mockContainer.resolve(CompactBlockRepository.self).create()
let compactBlockProcessor = CompactBlockProcessor(container: mockContainer, config: processorConfig)
let cancelableTask = Task {
do {
let blockDownloader = await compactBlockProcessor.blockDownloader
await blockDownloader.setDownloadLimit(latestBlockHeight)
try await blockDownloader.setSyncRange(startHeight...latestBlockHeight, batchSize: 100)
await blockDownloader.startDownload(maxBlockBufferSize: 10)
try await blockDownloader.waitUntilRequestedBlocksAreDownloaded(in: startHeight...latestBlockHeight)
} catch {
XCTAssertTrue(Task.isCancelled)
}
}
cancelableTask.cancel()
await compactBlockProcessor.stop()
latestBlockHeight = try await service.latestBlockHeight()
startHeight = latestBlockHeight - 10_000
}
func testStreamTimeout() async throws {
override func tearDownWithError() throws {
try super.tearDownWithError()
rustBackend = nil
try? FileManager.default.removeItem(at: __dataDbURL())
endpoint = nil
service = nil
storage = nil
internalSyncProgress = nil
processorConfig = nil
}
private func makeDependencies(timeout: Int64) async throws {
let endpoint = LightWalletEndpoint(
address: LightWalletEndpointBuilder.eccTestnet.host,
port: 9067,
secure: true,
singleCallTimeoutInMillis: 1000,
streamingCallTimeoutInMillis: 1000
singleCallTimeoutInMillis: timeout,
streamingCallTimeoutInMillis: timeout
)
let service = LightWalletServiceFactory(endpoint: endpoint).make()
self.endpoint = endpoint
service = LightWalletServiceFactory(endpoint: endpoint).make()
storage = FSCompactBlockRepository(
fsBlockDbRoot: testTempDirectory,
metadataStore: FSMetadataStore.live(
fsBlockDbRoot: testTempDirectory,
rustBackend: rustBackend,
logger: logger
),
blockDescriptor: .live,
contentProvider: DirectoryListingProviders.defaultSorted,
logger: logger
)
try await storage.create()
let latestBlockHeight = try await service.latestBlockHeight()
let internalSyncProgressStorage = InternalSyncProgressMemoryStorage()
try await internalSyncProgressStorage.set(startHeight, for: InternalSyncProgress.Key.latestDownloadedBlockHeight.rawValue)
internalSyncProgress = InternalSyncProgress(alias: .default, storage: internalSyncProgressStorage, logger: logger)
let startHeight = latestBlockHeight - 100_000
let processorConfig = CompactBlockProcessor.Configuration.standard(
processorConfig = CompactBlockProcessor.Configuration.standard(
for: ZcashNetworkBuilder.network(for: .testnet),
walletBirthday: ZcashNetworkBuilder.network(for: .testnet).constants.saplingActivationHeight
)
@ -137,18 +104,99 @@ class BlockStreamingTest: ZcashTestCase {
mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in
LightWalletServiceFactory(endpoint: endpoint).make()
}
try await mockContainer.resolve(CompactBlockRepository.self).create()
let compactBlockProcessor = CompactBlockProcessor(container: mockContainer, config: processorConfig)
let date = Date()
let transactionRepositoryMock = TransactionRepositoryMock()
transactionRepositoryMock.lastScannedHeightReturnValue = startHeight
mockContainer.mock(type: TransactionRepository.self, isSingleton: true) { _ in transactionRepositoryMock }
let blockDownloader = BlockDownloaderImpl(
service: service,
downloaderService: BlockDownloaderServiceImpl(service: service, storage: storage),
storage: storage,
internalSyncProgress: internalSyncProgress,
metrics: SDKMetrics(),
logger: logger
)
mockContainer.mock(type: BlockDownloader.self, isSingleton: true) { _ in blockDownloader }
}
func testStream() async throws {
try await makeDependencies(timeout: 10000)
var blocks: [ZcashCompactBlock] = []
let stream = service.blockStream(startHeight: startHeight, endHeight: latestBlockHeight)
do {
let blockDownloader = await compactBlockProcessor.blockDownloader
await blockDownloader.setDownloadLimit(latestBlockHeight)
try await blockDownloader.setSyncRange(startHeight...latestBlockHeight, batchSize: 100)
await blockDownloader.startDownload(maxBlockBufferSize: 10)
try await blockDownloader.waitUntilRequestedBlocksAreDownloaded(in: startHeight...latestBlockHeight)
for try await compactBlock in stream {
blocks.append(compactBlock)
}
} catch {
XCTFail("failed with error: \(error)")
}
XCTAssertEqual(blocks.count, latestBlockHeight - startHeight + 1)
}
func testStreamCancellation() async throws {
try await makeDependencies(timeout: 10000)
let action = DownloadAction(container: mockContainer, configProvider: CompactBlockProcessor.ConfigProvider(config: processorConfig))
let syncRanges = SyncRanges(
latestBlockHeight: latestBlockHeight,
downloadRange: startHeight...latestBlockHeight,
scanRange: nil,
enhanceRange: nil,
fetchUTXORange: nil,
latestScannedHeight: startHeight,
latestDownloadedBlockHeight: startHeight
)
let context = ActionContext(state: .download)
await context.update(syncRanges: syncRanges)
let expectation = XCTestExpectation()
let cancelableTask = Task {
do {
_ = try await action.run(with: context, didUpdate: { _ in })
let lastDownloadedHeight = try await internalSyncProgress.latestDownloadedBlockHeight
// Just to be sure that download was interrupted before download was finished.
XCTAssertLessThan(lastDownloadedHeight, latestBlockHeight)
expectation.fulfill()
} catch {
XCTFail("Downloading failed with error: \(error)")
expectation.fulfill()
}
}
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
cancelableTask.cancel()
}
await fulfillment(of: [expectation], timeout: 5)
await action.stop()
}
func testStreamTimeout() async throws {
try await makeDependencies(timeout: 100)
let action = DownloadAction(container: mockContainer, configProvider: CompactBlockProcessor.ConfigProvider(config: processorConfig))
let syncRanges = SyncRanges(
latestBlockHeight: latestBlockHeight,
downloadRange: startHeight...latestBlockHeight,
scanRange: nil,
enhanceRange: nil,
fetchUTXORange: nil,
latestScannedHeight: startHeight,
latestDownloadedBlockHeight: startHeight
)
let context = ActionContext(state: .download)
await context.update(syncRanges: syncRanges)
let date = Date()
do {
_ = try await action.run(with: context, didUpdate: { _ in })
XCTFail("It is expected that this downloading fails.")
} catch {
if let lwdError = error as? ZcashError {
switch lwdError {
@ -161,12 +209,12 @@ class BlockStreamingTest: ZcashTestCase {
XCTFail("Error should have been a timeLimit reached Error")
}
}
let now = Date()
let elapsed = now.timeIntervalSince(date)
print("took \(elapsed) seconds")
await compactBlockProcessor.stop()
await action.stop()
}
}

View File

@ -30,6 +30,10 @@ class CompactBlockProcessorTests: ZcashTestCase {
try await super.setUp()
logger = OSLogger(logLevel: .debug)
for key in InternalSyncProgress.Key.allCases {
UserDefaults.standard.set(0, forKey: key.with(.default))
}
let pathProvider = DefaultResourceProvider(network: network)
processorConfig = CompactBlockProcessor.Configuration(
alias: .default,
@ -170,7 +174,7 @@ class CompactBlockProcessorTests: ZcashTestCase {
let expectedUpdates = expectedBatches(
currentHeight: processorConfig.walletBirthday,
targetHeight: mockLatestHeight,
batchSize: processorConfig.downloadBatchSize
batchSize: processorConfig.batchSize
)
updatedNotificationExpectation.expectedFulfillmentCount = expectedUpdates
@ -189,8 +193,8 @@ class CompactBlockProcessorTests: ZcashTestCase {
var expectedSyncRanges = SyncRanges(
latestBlockHeight: latestBlockchainHeight,
downloadedButUnscannedRange: 1...latestDownloadedHeight,
downloadAndScanRange: latestDownloadedHeight...latestBlockchainHeight,
downloadRange: latestDownloadedHeight...latestBlockchainHeight,
scanRange: latestDownloadedHeight...latestBlockchainHeight,
enhanceRange: processorConfig.walletBirthday...latestBlockchainHeight,
fetchUTXORange: processorConfig.walletBirthday...latestBlockchainHeight,
latestScannedHeight: 0,
@ -217,13 +221,13 @@ class CompactBlockProcessorTests: ZcashTestCase {
)
// Test mid-range
latestDownloadedHeight = BlockHeight(network.constants.saplingActivationHeight + ZcashSDK.DefaultDownloadBatch)
latestDownloadedHeight = BlockHeight(network.constants.saplingActivationHeight + ZcashSDK.DefaultBatchSize)
latestBlockchainHeight = BlockHeight(network.constants.saplingActivationHeight + 1000)
expectedSyncRanges = SyncRanges(
latestBlockHeight: latestBlockchainHeight,
downloadedButUnscannedRange: 1...latestDownloadedHeight,
downloadAndScanRange: latestDownloadedHeight + 1...latestBlockchainHeight,
downloadRange: latestDownloadedHeight + 1...latestBlockchainHeight,
scanRange: processorConfig.walletBirthday...latestBlockchainHeight,
enhanceRange: processorConfig.walletBirthday...latestBlockchainHeight,
fetchUTXORange: processorConfig.walletBirthday...latestBlockchainHeight,
latestScannedHeight: 0,
@ -256,8 +260,8 @@ class CompactBlockProcessorTests: ZcashTestCase {
expectedSyncRanges = SyncRanges(
latestBlockHeight: latestBlockchainHeight,
downloadedButUnscannedRange: 1...latestDownloadedHeight,
downloadAndScanRange: latestDownloadedHeight + 1...latestBlockchainHeight,
downloadRange: latestDownloadedHeight + 1...latestBlockchainHeight,
scanRange: processorConfig.walletBirthday...latestBlockchainHeight,
enhanceRange: processorConfig.walletBirthday...latestBlockchainHeight,
fetchUTXORange: processorConfig.walletBirthday...latestBlockchainHeight,
latestScannedHeight: 0,
@ -283,72 +287,6 @@ class CompactBlockProcessorTests: ZcashTestCase {
"Failure when testing last range"
)
}
func testShouldClearBlockCacheReturnsNilWhenScannedHeightEqualsDownloadedHeight() {
/*
downloaded but not scanned: -1...-1
download and scan: 1493120...2255953
enhance range: 1410000...2255953
fetchUTXO range: 1410000...2255953
total progress range: 1493120...2255953
*/
let range = SyncRanges(
latestBlockHeight: 2255953,
downloadedButUnscannedRange: -1 ... -1,
downloadAndScanRange: 1493120...2255953,
enhanceRange: 1410000...2255953,
fetchUTXORange: 1410000...2255953,
latestScannedHeight: 1493119,
latestDownloadedBlockHeight: 1493119
)
XCTAssertNil(range.shouldClearBlockCacheAndUpdateInternalState())
}
func testShouldClearBlockCacheReturnsAHeightWhenScannedIsGreaterThanDownloaded() {
/*
downloaded but not scanned: -1...-1
download and scan: 1493120...2255953
enhance range: 1410000...2255953
fetchUTXO range: 1410000...2255953
total progress range: 1493120...2255953
*/
let range = SyncRanges(
latestBlockHeight: 2255953,
downloadedButUnscannedRange: -1 ... -1,
downloadAndScanRange: 1493120...2255953,
enhanceRange: 1410000...2255953,
fetchUTXORange: 1410000...2255953,
latestScannedHeight: 1493129,
latestDownloadedBlockHeight: 1493119
)
XCTAssertEqual(range.shouldClearBlockCacheAndUpdateInternalState(), BlockHeight(1493129))
}
func testShouldClearBlockCacheReturnsNilWhenScannedIsGreaterThanDownloaded() {
/*
downloaded but not scanned: 1493120...1494120
download and scan: 1494121...2255953
enhance range: 1410000...2255953
fetchUTXO range: 1410000...2255953
total progress range: 1493120...2255953
*/
let range = SyncRanges(
latestBlockHeight: 2255953,
downloadedButUnscannedRange: 1493120...1494120,
downloadAndScanRange: 1494121...2255953,
enhanceRange: 1410000...2255953,
fetchUTXORange: 1410000...2255953,
latestScannedHeight: 1493119,
latestDownloadedBlockHeight: 1494120
)
XCTAssertNil(range.shouldClearBlockCacheAndUpdateInternalState())
}
func testDetermineLowerBoundPastBirthday() async {
let errorHeight = 781_906

View File

@ -1,607 +0,0 @@
//
// BlockBatchValidationTests.swift
// ZcashLightClientKit-Unit-Tests
//
// Created by Francisco Gindre on 6/17/21.
//
import XCTest
@testable import TestUtils
@testable import ZcashLightClientKit
class BlockBatchValidationTests: ZcashTestCase {
let testFileManager = FileManager()
var rustBackend: ZcashRustBackendWelding!
override func setUpWithError() throws {
try super.setUpWithError()
Dependencies.setup(
in: mockContainer,
urls: Initializer.URLs(
fsBlockDbRoot: testTempDirectory,
dataDbURL: try! __dataDbURL(),
generalStorageURL: testGeneralStorageDirectory,
spendParamsURL: try! __spendParamsURL(),
outputParamsURL: try! __outputParamsURL()
),
alias: .default,
networkType: .testnet,
endpoint: LightWalletEndpointBuilder.default,
loggingPolicy: .default(.debug)
)
mockContainer.mock(type: LatestBlocksDataProvider.self, isSingleton: true) { _ in LatestBlocksDataProviderMock() }
rustBackend = ZcashRustBackend.makeForTests(fsBlockDbRoot: testTempDirectory, networkType: .testnet)
}
override func tearDownWithError() throws {
try super.tearDownWithError()
rustBackend = nil
testTempDirectory = nil
}
func testBranchIdFailure() async throws {
let network = ZcashNetworkBuilder.network(for: .mainnet)
let service = MockLightWalletService(
latestBlockHeight: 1210000,
service: LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.default).make()
)
mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in service }
let storage = FSCompactBlockRepository(
fsBlockDbRoot: testTempDirectory,
metadataStore: FSMetadataStore.live(
fsBlockDbRoot: testTempDirectory,
rustBackend: rustBackend,
logger: logger
),
blockDescriptor: .live,
contentProvider: DirectoryListingProviders.defaultSorted,
logger: logger
)
mockContainer.mock(type: CompactBlockRepository.self, isSingleton: true) { _ in storage }
try await storage.create()
mockContainer.mock(type: BlockDownloaderService.self, isSingleton: true) { _ in
let repository = ZcashConsoleFakeStorage(latestBlockHeight: 1220000)
return BlockDownloaderServiceImpl(service: service, storage: repository)
}
let config = CompactBlockProcessor.Configuration(
alias: .default,
fsBlockCacheRoot: testTempDirectory,
dataDb: try! __dataDbURL(),
spendParamsURL: try! __spendParamsURL(),
outputParamsURL: try! __outputParamsURL(),
saplingParamsSourceURL: SaplingParamsSourceURL.tests,
downloadBatchSize: 100,
retries: 5,
maxBackoffInterval: 10,
rewindDistance: 100,
walletBirthdayProvider: { 1210000 },
saplingActivation: network.constants.saplingActivationHeight,
network: network
)
var info = LightdInfo()
info.blockHeight = 130000
info.branch = "d34db33f"
info.chainName = "main"
info.buildUser = "test user"
info.consensusBranchID = "d34db33f"
info.saplingActivationHeight = UInt64(network.constants.saplingActivationHeight)
service.mockLightDInfo = info
let mockBackend = await RustBackendMockHelper(rustBackend: rustBackend, consensusBranchID: Int32(0xd34d))
mockContainer.mock(type: ZcashRustBackendWelding.self, isSingleton: true) { _ in mockBackend.rustBackendMock }
let compactBlockProcessor = CompactBlockProcessor(
container: mockContainer,
config: config
)
do {
try await compactBlockProcessor.figureNextBatch(downloaderService: mockContainer.resolve(BlockDownloaderService.self))
XCTAssertFalse(Task.isCancelled)
} catch {
switch error {
case ZcashError.compactBlockProcessorWrongConsensusBranchId:
break
default:
XCTFail("Expected ZcashError.compactBlockProcessorWrongConsensusBranchId but found \(error)")
}
}
}
func testBranchNetworkMismatchFailure() async throws {
let network = ZcashNetworkBuilder.network(for: .mainnet)
let service = MockLightWalletService(
latestBlockHeight: 1210000,
service: LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.default).make()
)
mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in service }
let storage = FSCompactBlockRepository(
fsBlockDbRoot: testTempDirectory,
metadataStore: FSMetadataStore.live(
fsBlockDbRoot: testTempDirectory,
rustBackend: rustBackend,
logger: logger
),
blockDescriptor: .live,
contentProvider: DirectoryListingProviders.defaultSorted,
logger: logger
)
mockContainer.mock(type: CompactBlockRepository.self, isSingleton: true) { _ in storage }
try await storage.create()
mockContainer.mock(type: BlockDownloaderService.self, isSingleton: true) { _ in
let repository = ZcashConsoleFakeStorage(latestBlockHeight: 1220000)
return BlockDownloaderServiceImpl(service: service, storage: repository)
}
let config = CompactBlockProcessor.Configuration(
alias: .default,
fsBlockCacheRoot: testTempDirectory,
dataDb: try! __dataDbURL(),
spendParamsURL: try! __spendParamsURL(),
outputParamsURL: try! __outputParamsURL(),
saplingParamsSourceURL: SaplingParamsSourceURL.tests,
downloadBatchSize: 100,
retries: 5,
maxBackoffInterval: 10,
rewindDistance: 100,
walletBirthdayProvider: { 1210000 },
saplingActivation: network.constants.saplingActivationHeight,
network: network
)
var info = LightdInfo()
info.blockHeight = 130000
info.branch = "d34db33f"
info.chainName = "test"
info.buildUser = "test user"
info.consensusBranchID = "d34db4d"
info.saplingActivationHeight = UInt64(network.constants.saplingActivationHeight)
service.mockLightDInfo = info
let mockBackend = await RustBackendMockHelper(rustBackend: rustBackend, consensusBranchID: 0xd34db4d)
mockContainer.mock(type: ZcashRustBackendWelding.self, isSingleton: true) { _ in mockBackend.rustBackendMock }
let compactBlockProcessor = CompactBlockProcessor(
container: mockContainer,
config: config
)
do {
try await compactBlockProcessor.figureNextBatch(downloaderService: mockContainer.resolve(BlockDownloaderService.self))
XCTAssertFalse(Task.isCancelled)
} catch {
switch error {
case ZcashError.compactBlockProcessorNetworkMismatch(.mainnet, .testnet):
break
default:
XCTFail("Expected ZcashError.compactBlockProcessorNetworkMismatch but found \(error)")
}
}
}
func testBranchNetworkTypeWrongFailure() async throws {
let network = ZcashNetworkBuilder.network(for: .testnet)
let service = MockLightWalletService(
latestBlockHeight: 1210000,
service: LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.default).make()
)
mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in service }
let storage = FSCompactBlockRepository(
fsBlockDbRoot: testTempDirectory,
metadataStore: FSMetadataStore.live(
fsBlockDbRoot: testTempDirectory,
rustBackend: rustBackend,
logger: logger
),
blockDescriptor: .live,
contentProvider: DirectoryListingProviders.defaultSorted,
logger: logger
)
mockContainer.mock(type: CompactBlockRepository.self, isSingleton: true) { _ in storage }
try await storage.create()
mockContainer.mock(type: BlockDownloaderService.self, isSingleton: true) { _ in
let repository = ZcashConsoleFakeStorage(latestBlockHeight: 1220000)
return BlockDownloaderServiceImpl(service: service, storage: repository)
}
let config = CompactBlockProcessor.Configuration(
alias: .default,
fsBlockCacheRoot: testTempDirectory,
dataDb: try! __dataDbURL(),
spendParamsURL: try! __spendParamsURL(),
outputParamsURL: try! __outputParamsURL(),
saplingParamsSourceURL: SaplingParamsSourceURL.tests,
downloadBatchSize: 100,
retries: 5,
maxBackoffInterval: 10,
rewindDistance: 100,
walletBirthdayProvider: { 1210000 },
saplingActivation: network.constants.saplingActivationHeight,
network: network
)
var info = LightdInfo()
info.blockHeight = 130000
info.branch = "d34db33f"
info.chainName = "another"
info.buildUser = "test user"
info.consensusBranchID = "d34db4d"
info.saplingActivationHeight = UInt64(network.constants.saplingActivationHeight)
service.mockLightDInfo = info
let mockBackend = await RustBackendMockHelper(rustBackend: rustBackend, consensusBranchID: 0xd34db4d)
mockContainer.mock(type: ZcashRustBackendWelding.self, isSingleton: true) { _ in mockBackend.rustBackendMock }
let compactBlockProcessor = CompactBlockProcessor(
container: mockContainer,
config: config
)
do {
try await compactBlockProcessor.figureNextBatch(downloaderService: mockContainer.resolve(BlockDownloaderService.self))
XCTAssertFalse(Task.isCancelled)
} catch {
switch error {
case ZcashError.compactBlockProcessorChainName:
break
default:
XCTFail("Expected ZcashError.compactBlockProcessorChainName but found \(error)")
}
}
}
func testSaplingActivationHeightMismatch() async throws {
let network = ZcashNetworkBuilder.network(for: .mainnet)
let service = MockLightWalletService(
latestBlockHeight: 1210000,
service: LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.default).make()
)
mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in service }
let storage = FSCompactBlockRepository(
fsBlockDbRoot: testTempDirectory,
metadataStore: FSMetadataStore.live(
fsBlockDbRoot: testTempDirectory,
rustBackend: rustBackend,
logger: logger
),
blockDescriptor: .live,
contentProvider: DirectoryListingProviders.defaultSorted,
logger: logger
)
mockContainer.mock(type: CompactBlockRepository.self, isSingleton: true) { _ in storage }
try await storage.create()
mockContainer.mock(type: BlockDownloaderService.self, isSingleton: true) { _ in
let repository = ZcashConsoleFakeStorage(latestBlockHeight: 1220000)
return BlockDownloaderServiceImpl(service: service, storage: repository)
}
let config = CompactBlockProcessor.Configuration(
alias: .default,
fsBlockCacheRoot: testTempDirectory,
dataDb: try! __dataDbURL(),
spendParamsURL: try! __spendParamsURL(),
outputParamsURL: try! __outputParamsURL(),
saplingParamsSourceURL: SaplingParamsSourceURL.tests,
downloadBatchSize: 100,
retries: 5,
maxBackoffInterval: 10,
rewindDistance: 100,
walletBirthdayProvider: { 1210000 },
saplingActivation: network.constants.saplingActivationHeight,
network: network
)
var info = LightdInfo()
info.blockHeight = 130000
info.branch = "d34db33f"
info.chainName = "main"
info.buildUser = "test user"
info.consensusBranchID = "d34db4d"
info.saplingActivationHeight = UInt64(3434343)
service.mockLightDInfo = info
let mockBackend = await RustBackendMockHelper(rustBackend: rustBackend, consensusBranchID: 0xd34db4d)
mockContainer.mock(type: ZcashRustBackendWelding.self, isSingleton: true) { _ in mockBackend.rustBackendMock }
let compactBlockProcessor = CompactBlockProcessor(
container: mockContainer,
config: config
)
do {
try await compactBlockProcessor.figureNextBatch(downloaderService: mockContainer.resolve(BlockDownloaderService.self))
XCTAssertFalse(Task.isCancelled)
} catch {
switch error {
case ZcashError.compactBlockProcessorSaplingActivationMismatch(
network.constants.saplingActivationHeight,
BlockHeight(info.saplingActivationHeight)
):
break
default:
XCTFail("Expected ZcashError.compactBlockProcessorSaplingActivationMismatch but found \(error)")
}
}
}
func testResultIsWait() async throws {
let network = ZcashNetworkBuilder.network(for: .mainnet)
let expectedLatestHeight = BlockHeight(1210000)
let service = MockLightWalletService(
latestBlockHeight: expectedLatestHeight,
service: LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.default).make()
)
let expectedStoredLatestHeight = BlockHeight(1220000)
let expectedResult = CompactBlockProcessor.NextState.wait(
latestHeight: expectedLatestHeight,
latestDownloadHeight: expectedStoredLatestHeight
)
let repository = ZcashConsoleFakeStorage(latestBlockHeight: expectedStoredLatestHeight)
let downloaderService = BlockDownloaderServiceImpl(service: service, storage: repository)
let config = CompactBlockProcessor.Configuration(
alias: .default,
fsBlockCacheRoot: testTempDirectory,
dataDb: try! __dataDbURL(),
spendParamsURL: try! __spendParamsURL(),
outputParamsURL: try! __outputParamsURL(),
saplingParamsSourceURL: SaplingParamsSourceURL.tests,
downloadBatchSize: 100,
retries: 5,
maxBackoffInterval: 10,
rewindDistance: 100,
walletBirthdayProvider: { 1210000 },
saplingActivation: network.constants.saplingActivationHeight,
network: network
)
var info = LightdInfo()
info.blockHeight = UInt64(expectedLatestHeight)
info.branch = "d34db33f"
info.chainName = "main"
info.buildUser = "test user"
info.consensusBranchID = "d34db4d"
info.saplingActivationHeight = UInt64(network.constants.saplingActivationHeight)
service.mockLightDInfo = info
let mockBackend = await RustBackendMockHelper(rustBackend: rustBackend, consensusBranchID: 0xd34db4d)
var nextBatch: CompactBlockProcessor.NextState?
do {
nextBatch = try await CompactBlockProcessor.NextStateHelper.nextState(
service: service,
downloaderService: downloaderService,
latestBlocksDataProvider: LatestBlocksDataProviderMock(
latestScannedHeight: expectedStoredLatestHeight,
latestBlockHeight: expectedLatestHeight
),
config: config,
rustBackend: mockBackend.rustBackendMock,
internalSyncProgress: InternalSyncProgress(
alias: .default,
storage: InternalSyncProgressMemoryStorage(),
logger: logger
),
alias: .default
)
XCTAssertFalse(Task.isCancelled)
} catch {
XCTFail("this shouldn't happen: \(error)")
}
guard nextBatch != nil else {
XCTFail("result should not be nil")
return
}
XCTAssertTrue(
{
switch (nextBatch, expectedResult) {
case let (.wait(latestHeight, latestDownloadHeight), .wait(expectedLatestHeight, exectedLatestDownloadHeight)):
return latestHeight == expectedLatestHeight && latestDownloadHeight == exectedLatestDownloadHeight
default:
return false
}
}(),
"Expected \(expectedResult) got: \(String(describing: nextBatch))"
)
}
func testResultProcessNew() async throws {
let network = ZcashNetworkBuilder.network(for: .mainnet)
let expectedLatestHeight = BlockHeight(1230000)
let service = MockLightWalletService(
latestBlockHeight: expectedLatestHeight,
service: LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.default).make()
)
let expectedStoredLatestHeight = BlockHeight(1220000)
let walletBirthday = BlockHeight(1210000)
let ranges = SyncRanges(
latestBlockHeight: expectedLatestHeight,
downloadedButUnscannedRange: nil,
downloadAndScanRange: expectedStoredLatestHeight + 1...expectedLatestHeight,
enhanceRange: walletBirthday...expectedLatestHeight,
fetchUTXORange: walletBirthday...expectedLatestHeight,
latestScannedHeight: expectedStoredLatestHeight,
latestDownloadedBlockHeight: expectedStoredLatestHeight
)
let expectedResult = CompactBlockProcessor.NextState.processNewBlocks(ranges: ranges)
let repository = ZcashConsoleFakeStorage(latestBlockHeight: expectedStoredLatestHeight)
let downloaderService = BlockDownloaderServiceImpl(service: service, storage: repository)
let config = CompactBlockProcessor.Configuration(
alias: .default,
fsBlockCacheRoot: testTempDirectory,
dataDb: try! __dataDbURL(),
spendParamsURL: try! __spendParamsURL(),
outputParamsURL: try! __outputParamsURL(),
saplingParamsSourceURL: SaplingParamsSourceURL.tests,
downloadBatchSize: 100,
retries: 5,
maxBackoffInterval: 10,
rewindDistance: 100,
walletBirthdayProvider: { walletBirthday },
saplingActivation: network.constants.saplingActivationHeight,
network: network
)
var info = LightdInfo()
info.blockHeight = UInt64(expectedLatestHeight)
info.branch = "d34db33f"
info.chainName = "main"
info.buildUser = "test user"
info.consensusBranchID = "d34db4d"
info.saplingActivationHeight = UInt64(network.constants.saplingActivationHeight)
service.mockLightDInfo = info
let mockBackend = await RustBackendMockHelper(rustBackend: rustBackend, consensusBranchID: 0xd34db4d)
var nextBatch: CompactBlockProcessor.NextState?
do {
nextBatch = try await CompactBlockProcessor.NextStateHelper.nextState(
service: service,
downloaderService: downloaderService,
latestBlocksDataProvider: LatestBlocksDataProviderMock(
latestScannedHeight: expectedStoredLatestHeight,
latestBlockHeight: expectedLatestHeight
),
config: config,
rustBackend: mockBackend.rustBackendMock,
internalSyncProgress: InternalSyncProgress(
alias: .default,
storage: InternalSyncProgressMemoryStorage(),
logger: logger
),
alias: .default
)
XCTAssertFalse(Task.isCancelled)
} catch {
XCTFail("this shouldn't happen: \(error)")
}
guard nextBatch != nil else {
XCTFail("result should not be nil")
return
}
XCTAssertTrue(
{
switch (nextBatch, expectedResult) {
case let (.processNewBlocks(ranges), .processNewBlocks(expectedRanges)):
return ranges == expectedRanges
default:
return false
}
}(),
"Expected \(expectedResult) got: \(String(describing: nextBatch))"
)
}
func testResultProcessorFinished() async throws {
let network = ZcashNetworkBuilder.network(for: .mainnet)
let expectedLatestHeight = BlockHeight(1230000)
let service = MockLightWalletService(
latestBlockHeight: expectedLatestHeight,
service: LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.default).make()
)
let expectedStoredLatestHeight = BlockHeight(1230000)
let walletBirthday = BlockHeight(1210000)
let expectedResult = CompactBlockProcessor.NextState.finishProcessing(height: expectedStoredLatestHeight)
let repository = ZcashConsoleFakeStorage(latestBlockHeight: expectedStoredLatestHeight)
let downloaderService = BlockDownloaderServiceImpl(service: service, storage: repository)
let config = CompactBlockProcessor.Configuration(
alias: .default,
fsBlockCacheRoot: testTempDirectory,
dataDb: try! __dataDbURL(),
spendParamsURL: try! __spendParamsURL(),
outputParamsURL: try! __outputParamsURL(),
saplingParamsSourceURL: SaplingParamsSourceURL.tests,
downloadBatchSize: 100,
retries: 5,
maxBackoffInterval: 10,
rewindDistance: 100,
walletBirthdayProvider: { walletBirthday },
saplingActivation: network.constants.saplingActivationHeight,
network: network
)
let internalSyncProgress = InternalSyncProgress(
alias: .default,
storage: InternalSyncProgressMemoryStorage(),
logger: logger
)
try await internalSyncProgress.set(expectedStoredLatestHeight, .latestEnhancedHeight)
try await internalSyncProgress.set(expectedStoredLatestHeight, .latestUTXOFetchedHeight)
var info = LightdInfo()
info.blockHeight = UInt64(expectedLatestHeight)
info.branch = "d34db33f"
info.chainName = "main"
info.buildUser = "test user"
info.consensusBranchID = "d34db4d"
info.saplingActivationHeight = UInt64(network.constants.saplingActivationHeight)
service.mockLightDInfo = info
let mockBackend = await RustBackendMockHelper(rustBackend: rustBackend, consensusBranchID: 0xd34db4d)
var nextBatch: CompactBlockProcessor.NextState?
do {
nextBatch = try await CompactBlockProcessor.NextStateHelper.nextState(
service: service,
downloaderService: downloaderService,
latestBlocksDataProvider: LatestBlocksDataProviderMock(
latestScannedHeight: expectedStoredLatestHeight,
latestBlockHeight: expectedLatestHeight
),
config: config,
rustBackend: mockBackend.rustBackendMock,
internalSyncProgress: internalSyncProgress,
alias: .default
)
XCTAssertFalse(Task.isCancelled)
} catch {
XCTFail("this shouldn't happen: \(error)")
}
guard nextBatch != nil else {
XCTFail("result should not be nil")
return
}
XCTAssertTrue(
{
switch (nextBatch, expectedResult) {
case let (.finishProcessing(height), .finishProcessing(expectedHeight)):
return height == expectedHeight
default:
return false
}
}(),
"Expected \(expectedResult) got: \(String(describing: nextBatch))"
)
}
}

View File

@ -0,0 +1,157 @@
//
// ChecksBeforeSyncActionTests.swift
//
//
// Created by Lukáš Korba on 22.05.2023.
//
import XCTest
@testable import TestUtils
@testable import ZcashLightClientKit
final class ChecksBeforeSyncActionTests: ZcashTestCase {
var underlyingDownloadRange: CompactBlockRange?
var underlyingScanRange: CompactBlockRange?
var underlyingLatestScannedHeight: BlockHeight?
var underlyingLatestDownloadedBlockHeight: BlockHeight?
override func setUp() {
super.setUp()
underlyingDownloadRange = nil
underlyingScanRange = nil
underlyingLatestScannedHeight = nil
underlyingLatestDownloadedBlockHeight = nil
}
func testChecksBeforeSyncAction_shouldClearBlockCacheAndUpdateInternalState_noDownloadNoScanRange() async throws {
let checksBeforeSyncAction = setupAction()
let syncRanges = setupSyncRanges()
let latestScannedHeight = checksBeforeSyncAction.shouldClearBlockCacheAndUpdateInternalState(syncRange: syncRanges)
XCTAssertNil(latestScannedHeight, "latestScannedHeight is expected to be nil.")
}
func testChecksBeforeSyncAction_shouldClearBlockCacheAndUpdateInternalState_nothingToClear() async throws {
let checksBeforeSyncAction = setupAction()
underlyingDownloadRange = CompactBlockRange(uncheckedBounds: (1000, 2000))
underlyingScanRange = CompactBlockRange(uncheckedBounds: (1000, 2000))
underlyingLatestScannedHeight = BlockHeight(2000)
underlyingLatestDownloadedBlockHeight = BlockHeight(2000)
let syncRanges = setupSyncRanges()
let latestScannedHeight = checksBeforeSyncAction.shouldClearBlockCacheAndUpdateInternalState(syncRange: syncRanges)
XCTAssertNil(latestScannedHeight, "latestScannedHeight is expected to be nil.")
}
func testChecksBeforeSyncAction_shouldClearBlockCacheAndUpdateInternalState_somethingToClear() async throws {
let checksBeforeSyncAction = setupAction()
underlyingDownloadRange = CompactBlockRange(uncheckedBounds: (1000, 2000))
underlyingScanRange = CompactBlockRange(uncheckedBounds: (1000, 2000))
underlyingLatestScannedHeight = BlockHeight(2000)
underlyingLatestDownloadedBlockHeight = BlockHeight(1000)
let syncRanges = setupSyncRanges()
let latestScannedHeight = checksBeforeSyncAction.shouldClearBlockCacheAndUpdateInternalState(syncRange: syncRanges)
XCTAssertNotNil(latestScannedHeight, "latestScannedHeight is not expected to be nil.")
}
func testChecksBeforeSyncAction_NextAction_ClearStorage() async throws {
let compactBlockRepository = CompactBlockRepositoryMock()
let internalSyncProgressStorageMock = InternalSyncProgressStorageMock()
compactBlockRepository.clearClosure = { }
internalSyncProgressStorageMock.setForClosure = { _, _ in }
let checksBeforeSyncAction = setupAction(
compactBlockRepository,
internalSyncProgressStorageMock
)
underlyingDownloadRange = CompactBlockRange(uncheckedBounds: (1000, 2000))
underlyingScanRange = CompactBlockRange(uncheckedBounds: (1000, 2000))
underlyingLatestScannedHeight = BlockHeight(2000)
underlyingLatestDownloadedBlockHeight = BlockHeight(1000)
let syncContext = await setupActionContext()
do {
let nextContext = try await checksBeforeSyncAction.run(with: syncContext) { _ in }
XCTAssertTrue(compactBlockRepository.clearCalled, "storage.clear() is expected to be called.")
XCTAssertTrue(internalSyncProgressStorageMock.setForCalled, "internalSyncProgress.set() is expected to be called.")
let nextState = await nextContext.state
XCTAssertTrue(
nextState == .fetchUTXO,
"nextContext after .checksBeforeSync is expected to be .fetchUTXO but received \(nextState)"
)
} catch {
XCTFail("testChecksBeforeSyncAction_NextAction_ClearStorage is not expected to fail. \(error)")
}
}
func testChecksBeforeSyncAction_NextAction_CreateStorage() async throws {
let compactBlockRepository = CompactBlockRepositoryMock()
let internalSyncProgressStorageMock = InternalSyncProgressStorageMock()
compactBlockRepository.createClosure = { }
let checksBeforeSyncAction = setupAction(compactBlockRepository)
let syncContext = await setupActionContext()
do {
let nextContext = try await checksBeforeSyncAction.run(with: syncContext) { _ in }
XCTAssertTrue(compactBlockRepository.createCalled, "storage.create() is expected to be called.")
XCTAssertFalse(internalSyncProgressStorageMock.setForCalled, "internalSyncProgress.set() is not expected to be called.")
let nextState = await nextContext.state
XCTAssertTrue(
nextState == .fetchUTXO,
"nextContext after .checksBeforeSync is expected to be .fetchUTXO but received \(nextState)"
)
} catch {
XCTFail("testChecksBeforeSyncAction_NextAction_CreateStorage is not expected to fail. \(error)")
}
}
private func setupAction(
_ compactBlockRepositoryMock: CompactBlockRepositoryMock = CompactBlockRepositoryMock(),
_ internalSyncProgressStorageMock: InternalSyncProgressStorageMock = InternalSyncProgressStorageMock(),
_ loggerMock: LoggerMock = LoggerMock()
) -> ChecksBeforeSyncAction {
mockContainer.register(type: InternalSyncProgress.self, isSingleton: true) { _ in
InternalSyncProgress(alias: .default, storage: internalSyncProgressStorageMock, logger: loggerMock)
}
mockContainer.mock(type: CompactBlockRepository.self, isSingleton: true) { _ in compactBlockRepositoryMock }
return ChecksBeforeSyncAction(
container: mockContainer
)
}
private func setupSyncRanges() -> SyncRanges {
SyncRanges(
latestBlockHeight: 0,
downloadRange: underlyingDownloadRange,
scanRange: underlyingScanRange,
enhanceRange: nil,
fetchUTXORange: nil,
latestScannedHeight: underlyingLatestScannedHeight,
latestDownloadedBlockHeight: underlyingLatestDownloadedBlockHeight
)
}
private func setupActionContext() async -> ActionContext {
let syncContext: ActionContext = .init(state: .checksBeforeSync)
await syncContext.update(syncRanges: setupSyncRanges())
await syncContext.update(totalProgressRange: CompactBlockRange(uncheckedBounds: (1000, 2000)))
return syncContext
}
}

View File

@ -0,0 +1,40 @@
//
// ClearAlreadyScannedBlocksActionTests.swift
//
//
// Created by Lukáš Korba on 22.05.2023.
//
import XCTest
@testable import TestUtils
@testable import ZcashLightClientKit
final class ClearAlreadyScannedBlocksActionTests: ZcashTestCase {
func testClearAlreadyScannedBlocksAction_NextAction() async throws {
let compactBlockRepositoryMock = CompactBlockRepositoryMock()
let transactionRepositoryMock = TransactionRepositoryMock()
compactBlockRepositoryMock.clearUpToClosure = { _ in }
transactionRepositoryMock.lastScannedHeightReturnValue = 1
mockContainer.mock(type: CompactBlockRepository.self, isSingleton: true) { _ in compactBlockRepositoryMock }
mockContainer.mock(type: TransactionRepository.self, isSingleton: true) { _ in transactionRepositoryMock }
let clearAlreadyScannedBlocksAction = ClearAlreadyScannedBlocksAction(
container: mockContainer
)
do {
let nextContext = try await clearAlreadyScannedBlocksAction.run(with: .init(state: .clearAlreadyScannedBlocks)) { _ 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,
"nextContext after .clearAlreadyScannedBlocks is expected to be .enhance but received \(nextState)"
)
} catch {
XCTFail("testClearAlreadyScannedBlocksAction_NextAction is not expected to fail. \(error)")
}
}
}

View File

@ -0,0 +1,36 @@
//
// ClearCacheActionTests.swift
//
//
// Created by Lukáš Korba on 22.05.2023.
//
import XCTest
@testable import TestUtils
@testable import ZcashLightClientKit
final class ClearCacheActionTests: ZcashTestCase {
func testClearCacheAction_NextAction() async throws {
let compactBlockRepositoryMock = CompactBlockRepositoryMock()
compactBlockRepositoryMock.clearClosure = { }
mockContainer.mock(type: CompactBlockRepository.self, isSingleton: true) { _ in compactBlockRepositoryMock }
let clearCacheAction = ClearCacheAction(
container: mockContainer
)
do {
let nextContext = try await clearCacheAction.run(with: .init(state: .clearCache)) { _ in }
XCTAssertTrue(compactBlockRepositoryMock.clearCalled, "storage.clear() is expected to be called.")
let nextState = await nextContext.state
XCTAssertTrue(
nextState == .finished,
"nextContext after .clearCache is expected to be .finished but received \(nextState)"
)
} catch {
XCTFail("testClearCacheAction_NextAction is not expected to fail. \(error)")
}
}
}

View File

@ -0,0 +1,242 @@
//
// ComputeSyncRangesActionTests.swift
//
//
// Created by Lukáš Korba on 22.05.2023.
//
import XCTest
@testable import TestUtils
@testable import ZcashLightClientKit
final class ComputeSyncRangesActionTests: ZcashTestCase {
var underlyingDownloadRange: CompactBlockRange?
var underlyingScanRange: CompactBlockRange?
override func setUp() {
super.setUp()
underlyingDownloadRange = nil
underlyingScanRange = nil
}
func testComputeSyncRangesAction_computeTotalProgressRange_noDownloadNoScanRange() async throws {
let computeSyncRangesAction = setupAction()
let syncRanges = setupSyncRanges()
let totalProgressRange = computeSyncRangesAction.computeTotalProgressRange(from: syncRanges)
XCTAssertTrue(
totalProgressRange == 0...0,
"testComputeSyncRangesAction_computeTotalProgressRange_noDownloadNoScanRange is expected to be 0...0 but received \(totalProgressRange)"
)
}
func testComputeSyncRangesAction_computeTotalProgressRange_ValidRange() async throws {
let computeSyncRangesAction = setupAction()
underlyingDownloadRange = CompactBlockRange(uncheckedBounds: (1000, 2000))
underlyingScanRange = CompactBlockRange(uncheckedBounds: (1000, 2000))
let syncRanges = setupSyncRanges()
let totalProgressRange = computeSyncRangesAction.computeTotalProgressRange(from: syncRanges)
let expectedRange = 1000...2000
XCTAssertTrue(
totalProgressRange == expectedRange,
"testComputeSyncRangesAction_computeTotalProgressRange_ValidRange is expected to be \(expectedRange) but received \(totalProgressRange)"
)
}
func testComputeSyncRangesAction_finishProcessingCase() async throws {
let blockDownloaderServiceMock = BlockDownloaderServiceMock()
let latestBlocksDataProviderMock = LatestBlocksDataProviderMock()
let internalSyncProgressStorageMock = InternalSyncProgressStorageMock()
let loggerMock = LoggerMock()
let computeSyncRangesAction = setupDefaultMocksAndReturnAction(
blockDownloaderServiceMock,
latestBlocksDataProviderMock,
internalSyncProgressStorageMock,
loggerMock
)
let syncContext = await setupActionContext()
do {
let nextContext = try await computeSyncRangesAction.run(with: syncContext) { _ in }
XCTAssertTrue(
blockDownloaderServiceMock.lastDownloadedBlockHeightCalled,
"downloaderService.lastDownloadedBlockHeight() is expected to be called."
)
XCTAssertTrue(
latestBlocksDataProviderMock.updateScannedDataCalled,
"latestBlocksDataProvider.updateScannedData() is expected to be called."
)
XCTAssertTrue(latestBlocksDataProviderMock.updateBlockDataCalled, "latestBlocksDataProvider.updateBlockData() is expected to be called.")
XCTAssertFalse(loggerMock.infoFileFunctionLineCalled, "logger.info() is not expected to be called.")
let nextState = await nextContext.state
XCTAssertTrue(
nextState == .finished,
"nextContext after .computeSyncRanges is expected to be .finished but received \(nextState)"
)
} catch {
XCTFail("testComputeSyncRangesAction_finishProcessingCase is not expected to fail. \(error)")
}
}
func testComputeSyncRangesAction_checksBeforeSyncCase() async throws {
let blockDownloaderServiceMock = BlockDownloaderServiceMock()
let latestBlocksDataProviderMock = LatestBlocksDataProviderMock()
let internalSyncProgressStorageMock = InternalSyncProgressStorageMock()
let loggerMock = LoggerMock()
let computeSyncRangesAction = setupDefaultMocksAndReturnAction(
blockDownloaderServiceMock,
latestBlocksDataProviderMock,
internalSyncProgressStorageMock,
loggerMock
)
latestBlocksDataProviderMock.underlyingLatestBlockHeight = 10
let syncContext = await setupActionContext()
do {
let nextContext = try await computeSyncRangesAction.run(with: syncContext) { _ in }
XCTAssertTrue(
blockDownloaderServiceMock.lastDownloadedBlockHeightCalled,
"downloaderService.lastDownloadedBlockHeight() is expected to be called."
)
XCTAssertTrue(
latestBlocksDataProviderMock.updateScannedDataCalled,
"latestBlocksDataProvider.updateScannedData() is expected to be called."
)
XCTAssertTrue(latestBlocksDataProviderMock.updateBlockDataCalled, "latestBlocksDataProvider.updateBlockData() is expected to be called.")
XCTAssertFalse(loggerMock.infoFileFunctionLineCalled, "logger.info() is not expected to be called.")
let nextState = await nextContext.state
XCTAssertTrue(
nextState == .checksBeforeSync,
"nextContext after .computeSyncRanges is expected to be .checksBeforeSync but received \(nextState)"
)
} catch {
XCTFail("testComputeSyncRangesAction_checksBeforeSyncCase is not expected to fail. \(error)")
}
}
func testComputeSyncRangesAction_waitCase() async throws {
let blockDownloaderServiceMock = BlockDownloaderServiceMock()
let latestBlocksDataProviderMock = LatestBlocksDataProviderMock()
let internalSyncProgressStorageMock = InternalSyncProgressStorageMock()
let loggerMock = LoggerMock()
let computeSyncRangesAction = setupDefaultMocksAndReturnAction(
blockDownloaderServiceMock,
latestBlocksDataProviderMock,
internalSyncProgressStorageMock,
loggerMock
)
blockDownloaderServiceMock.lastDownloadedBlockHeightReturnValue = 10
latestBlocksDataProviderMock.underlyingLatestScannedHeight = 10
internalSyncProgressStorageMock.integerForReturnValue = 10
loggerMock.infoFileFunctionLineClosure = { _, _, _, _ in }
let syncContext = await setupActionContext()
do {
let nextContext = try await computeSyncRangesAction.run(with: syncContext) { _ in }
XCTAssertTrue(
blockDownloaderServiceMock.lastDownloadedBlockHeightCalled,
"downloaderService.lastDownloadedBlockHeight() is expected to be called."
)
XCTAssertTrue(
latestBlocksDataProviderMock.updateScannedDataCalled,
"latestBlocksDataProvider.updateScannedData() is expected to be called."
)
XCTAssertTrue(latestBlocksDataProviderMock.updateBlockDataCalled, "latestBlocksDataProvider.updateBlockData() is expected to be called.")
XCTAssertTrue(loggerMock.infoFileFunctionLineCalled, "logger.info() is expected to be called.")
let nextState = await nextContext.state
XCTAssertTrue(
nextState == .finished,
"nextContext after .computeSyncRanges is expected to be .finished but received \(nextState)"
)
} catch {
XCTFail("testComputeSyncRangesAction_waitCase is not expected to fail. \(error)")
}
}
private func setupSyncRanges() -> SyncRanges {
SyncRanges(
latestBlockHeight: 0,
downloadRange: underlyingDownloadRange,
scanRange: underlyingScanRange,
enhanceRange: nil,
fetchUTXORange: nil,
latestScannedHeight: nil,
latestDownloadedBlockHeight: nil
)
}
private func setupActionContext() async -> ActionContext {
let syncContext: ActionContext = .init(state: .computeSyncRanges)
await syncContext.update(syncRanges: setupSyncRanges())
await syncContext.update(totalProgressRange: CompactBlockRange(uncheckedBounds: (1000, 2000)))
return syncContext
}
private func setupAction(
_ blockDownloaderServiceMock: BlockDownloaderServiceMock = BlockDownloaderServiceMock(),
_ latestBlocksDataProviderMock: LatestBlocksDataProviderMock = LatestBlocksDataProviderMock(),
_ internalSyncProgressStorageMock: InternalSyncProgressStorageMock = InternalSyncProgressStorageMock(),
_ loggerMock: LoggerMock = LoggerMock()
) -> ComputeSyncRangesAction {
mockContainer.register(type: InternalSyncProgress.self, isSingleton: true) { _ in
InternalSyncProgress(alias: .default, storage: internalSyncProgressStorageMock, logger: loggerMock)
}
mockContainer.mock(type: BlockDownloaderService.self, isSingleton: true) { _ in blockDownloaderServiceMock }
mockContainer.mock(type: LatestBlocksDataProvider.self, isSingleton: true) { _ in latestBlocksDataProviderMock }
mockContainer.mock(type: Logger.self, isSingleton: true) { _ in loggerMock }
let config: CompactBlockProcessor.Configuration = .standard(
for: ZcashNetworkBuilder.network(for: .testnet), walletBirthday: 0
)
return ComputeSyncRangesAction(
container: mockContainer,
configProvider: CompactBlockProcessor.ConfigProvider(config: config)
)
}
private func setupDefaultMocksAndReturnAction(
_ blockDownloaderServiceMock: BlockDownloaderServiceMock = BlockDownloaderServiceMock(),
_ latestBlocksDataProviderMock: LatestBlocksDataProviderMock = LatestBlocksDataProviderMock(),
_ internalSyncProgressStorageMock: InternalSyncProgressStorageMock = InternalSyncProgressStorageMock(),
_ loggerMock: LoggerMock = LoggerMock()
) -> ComputeSyncRangesAction {
blockDownloaderServiceMock.lastDownloadedBlockHeightReturnValue = 1
latestBlocksDataProviderMock.underlyingLatestBlockHeight = 1
latestBlocksDataProviderMock.underlyingLatestScannedHeight = 1
latestBlocksDataProviderMock.updateScannedDataClosure = { }
latestBlocksDataProviderMock.updateBlockDataClosure = { }
internalSyncProgressStorageMock.integerForReturnValue = 1
internalSyncProgressStorageMock.boolForReturnValue = true
internalSyncProgressStorageMock.setBoolClosure = { _, _ in }
loggerMock.debugFileFunctionLineClosure = { _, _, _, _ in }
return setupAction(
blockDownloaderServiceMock,
latestBlocksDataProviderMock,
internalSyncProgressStorageMock,
loggerMock
)
}
}

View File

@ -0,0 +1,184 @@
//
// DownloadActionTests.swift
//
//
// Created by Lukáš Korba on 21.05.2023.
//
import XCTest
@testable import TestUtils
@testable import ZcashLightClientKit
final class DownloadActionTests: ZcashTestCase {
var underlyingDownloadRange: CompactBlockRange?
var underlyingScanRange: CompactBlockRange?
func testDownloadAction_NextAction() async throws {
let blockDownloaderMock = BlockDownloaderMock()
let transactionRepositoryMock = TransactionRepositoryMock()
transactionRepositoryMock.lastScannedHeightReturnValue = 1
blockDownloaderMock.setSyncRangeBatchSizeClosure = { _, _ in }
blockDownloaderMock.setDownloadLimitClosure = { _ in }
blockDownloaderMock.startDownloadMaxBlockBufferSizeClosure = { _ in }
blockDownloaderMock.waitUntilRequestedBlocksAreDownloadedInClosure = { _ in }
let downloadAction = setupAction(
blockDownloaderMock,
transactionRepositoryMock
)
underlyingDownloadRange = CompactBlockRange(uncheckedBounds: (1000, 2000))
underlyingScanRange = CompactBlockRange(uncheckedBounds: (1000, 2000))
let syncContext = await setupActionContext()
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.")
XCTAssertTrue(
blockDownloaderMock.waitUntilRequestedBlocksAreDownloadedInCalled,
"downloader.waitUntilRequestedBlocksAreDownloaded() is expected to be called."
)
let nextState = await nextContext.state
XCTAssertTrue(
nextState == .validate,
"nextContext after .download is expected to be .validate but received \(nextState)"
)
} catch {
XCTFail("testDownloadAction_NextAction is not expected to fail. \(error)")
}
}
func testDownloadAction_NoDownloadAndScanRange() async throws {
let blockDownloaderMock = BlockDownloaderMock()
let transactionRepositoryMock = TransactionRepositoryMock()
let downloadAction = setupAction(
blockDownloaderMock,
transactionRepositoryMock
)
let syncContext = await setupActionContext()
do {
let nextContext = try await downloadAction.run(with: syncContext) { _ in }
XCTAssertFalse(
transactionRepositoryMock.lastScannedHeightCalled,
"transactionRepository.lastScannedHeight() is not 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.")
XCTAssertFalse(
blockDownloaderMock.waitUntilRequestedBlocksAreDownloadedInCalled,
"downloader.waitUntilRequestedBlocksAreDownloaded() is not expected to be called."
)
let nextState = await nextContext.state
XCTAssertTrue(
nextState == .validate,
"nextContext after .download is expected to be .validate but received \(nextState)"
)
} catch {
XCTFail("testDownloadAction_NoDownloadAndScanRange is not expected to fail. \(error)")
}
}
func testDownloadAction_NothingMoreToDownload() async throws {
let blockDownloaderMock = BlockDownloaderMock()
let transactionRepositoryMock = TransactionRepositoryMock()
transactionRepositoryMock.lastScannedHeightReturnValue = 2001
let downloadAction = setupAction(
blockDownloaderMock,
transactionRepositoryMock
)
underlyingDownloadRange = CompactBlockRange(uncheckedBounds: (1000, 2000))
underlyingScanRange = CompactBlockRange(uncheckedBounds: (1000, 2000))
let syncContext = await setupActionContext()
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.")
XCTAssertFalse(
blockDownloaderMock.waitUntilRequestedBlocksAreDownloadedInCalled,
"downloader.waitUntilRequestedBlocksAreDownloaded() is not expected to be called."
)
let nextState = await nextContext.state
XCTAssertTrue(
nextState == .validate,
"nextContext after .download is expected to be .validate but received \(nextState)"
)
} catch {
XCTFail("testDownloadAction_NothingMoreToDownload is not expected to fail. \(error)")
}
}
func testDownloadAction_DownloadStops() async throws {
let blockDownloaderMock = BlockDownloaderMock()
blockDownloaderMock.stopDownloadClosure = { }
let downloadAction = setupAction(
blockDownloaderMock
)
await downloadAction.stop()
XCTAssertTrue(blockDownloaderMock.stopDownloadCalled, "downloader.stopDownload() is expected to be called.")
}
private func setupActionContext() async -> ActionContext {
let syncContext: ActionContext = .init(state: .download)
let syncRanges = SyncRanges(
latestBlockHeight: 0,
downloadRange: underlyingDownloadRange,
scanRange: underlyingScanRange,
enhanceRange: nil,
fetchUTXORange: nil,
latestScannedHeight: nil,
latestDownloadedBlockHeight: nil
)
await syncContext.update(syncRanges: syncRanges)
return syncContext
}
private func setupAction(
_ blockDownloaderMock: BlockDownloaderMock = BlockDownloaderMock(),
_ transactionRepositoryMock: TransactionRepositoryMock = TransactionRepositoryMock(),
_ loggerMock: LoggerMock = LoggerMock()
) -> DownloadAction {
mockContainer.mock(type: BlockDownloader.self, isSingleton: true) { _ in blockDownloaderMock }
mockContainer.mock(type: TransactionRepository.self, isSingleton: true) { _ in transactionRepositoryMock }
mockContainer.mock(type: Logger.self, isSingleton: true) { _ in loggerMock }
loggerMock.debugFileFunctionLineClosure = { _, _, _, _ in }
let config: CompactBlockProcessor.Configuration = .standard(
for: ZcashNetworkBuilder.network(for: .testnet), walletBirthday: 0
)
return DownloadAction(
container: mockContainer,
configProvider: CompactBlockProcessor.ConfigProvider(config: config)
)
}
}

View File

@ -0,0 +1,356 @@
//
// EnhanceActionTests.swift
//
//
// Created by Lukáš Korba on 19.05.2023.
//
import XCTest
@testable import TestUtils
@testable import ZcashLightClientKit
final class EnhanceActionTests: ZcashTestCase {
var underlyingDownloadRange: CompactBlockRange?
var underlyingScanRange: CompactBlockRange?
var underlyingEnhanceRange: CompactBlockRange?
override func setUp() {
super.setUp()
underlyingDownloadRange = nil
underlyingScanRange = nil
underlyingEnhanceRange = nil
}
func testEnhanceAction_decideWhatToDoNext_NoDownloadAndScanRange() async throws {
let enhanceAction = setupAction()
let syncContext = await setupActionContext()
let nextContext = await enhanceAction.decideWhatToDoNext(context: syncContext, lastScannedHeight: 1)
let nextState = await nextContext.state
XCTAssertTrue(
nextState == .clearCache,
"testEnhanceAction_decideWhatToDoNext_NoDownloadAndScanRange is expected to be .clearCache but received \(nextState)"
)
}
func testEnhanceAction_decideWhatToDoNext_NothingToDownloadAndScanLeft() async throws {
let enhanceAction = setupAction()
underlyingDownloadRange = CompactBlockRange(uncheckedBounds: (1000, 2000))
underlyingScanRange = CompactBlockRange(uncheckedBounds: (1000, 2000))
let syncContext = await setupActionContext()
let nextContext = await enhanceAction.decideWhatToDoNext(context: syncContext, lastScannedHeight: 2000)
let nextState = await nextContext.state
XCTAssertTrue(
nextState == .clearCache,
"testEnhanceAction_decideWhatToDoNext_NothingToDownloadAndScanLeft is expected to be .clearCache but received \(nextState)"
)
}
func testEnhanceAction_decideWhatToDoNext_DownloadExpected() async throws {
let enhanceAction = setupAction()
underlyingDownloadRange = CompactBlockRange(uncheckedBounds: (1000, 2000))
underlyingScanRange = CompactBlockRange(uncheckedBounds: (1000, 2000))
let syncContext = await setupActionContext()
let nextContext = await enhanceAction.decideWhatToDoNext(context: syncContext, lastScannedHeight: 1500)
let nextState = await nextContext.state
XCTAssertTrue(
nextState == .download,
"testEnhanceAction_decideWhatToDoNext_DownloadExpected is expected to be .download but received \(nextState)"
)
}
func testEnhanceAction_NoEnhanceRange() async throws {
let blockEnhancerMock = BlockEnhancerMock()
let transactionRepositoryMock = TransactionRepositoryMock()
let internalSyncProgressStorageMock = InternalSyncProgressStorageMock()
transactionRepositoryMock.lastScannedHeightReturnValue = 1
let enhanceAction = setupAction(
blockEnhancerMock,
transactionRepositoryMock,
internalSyncProgressStorageMock
)
let syncContext = await setupActionContext()
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.")
XCTAssertFalse(internalSyncProgressStorageMock.integerForCalled, "internalSyncProgress.load() is not expected to be called.")
} catch {
XCTFail("testEnhanceAction_NoEnhanceRange is not expected to fail. \(error)")
}
}
func testEnhanceAction_1000BlocksConditionNotFulfilled() async throws {
let blockEnhancerMock = BlockEnhancerMock()
let transactionRepositoryMock = TransactionRepositoryMock()
let internalSyncProgressStorageMock = InternalSyncProgressStorageMock()
transactionRepositoryMock.lastScannedHeightReturnValue = 1
internalSyncProgressStorageMock.integerForReturnValue = 1
let enhanceAction = setupAction(
blockEnhancerMock,
transactionRepositoryMock,
internalSyncProgressStorageMock
)
underlyingEnhanceRange = CompactBlockRange(uncheckedBounds: (1000, 2000))
let syncContext = await setupActionContext()
do {
_ = try await enhanceAction.run(with: syncContext) { _ in }
XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.")
XCTAssertTrue(internalSyncProgressStorageMock.integerForCalled, "internalSyncProgress.load() 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)")
}
}
func testEnhanceAction_EnhancementOfBlocksCalled_FoundTransactions() async throws {
let blockEnhancerMock = BlockEnhancerMock()
let transactionRepositoryMock = TransactionRepositoryMock()
let internalSyncProgressStorageMock = InternalSyncProgressStorageMock()
transactionRepositoryMock.lastScannedHeightReturnValue = 1500
internalSyncProgressStorageMock.integerForReturnValue = 1
let transaction = ZcashTransaction.Overview(
accountId: 0,
blockTime: 1.0,
expiryHeight: 663206,
fee: Zatoshi(0),
id: 2,
index: 1,
hasChange: false,
memoCount: 1,
minedHeight: 663188,
raw: Data(),
rawID: Data(),
receivedNoteCount: 1,
sentNoteCount: 0,
value: Zatoshi(100000),
isExpiredUmined: false
)
blockEnhancerMock.enhanceAtDidEnhanceClosure = { _, didEnhance in
await didEnhance(EnhancementProgress.zero)
return [transaction]
}
let enhanceAction = setupAction(
blockEnhancerMock,
transactionRepositoryMock,
internalSyncProgressStorageMock
)
underlyingEnhanceRange = CompactBlockRange(uncheckedBounds: (1000, 2000))
let syncContext = await setupActionContext()
do {
_ = try await enhanceAction.run(with: syncContext) { event in
guard case let .foundTransactions(transactions, _) = event else {
XCTFail("Event is expected to be .foundTransactions but received \(event)")
return
}
XCTAssertTrue(transactions.count == 1)
guard let receivedTransaction = transactions.first else {
XCTFail("Transaction.first is expected to pass.")
return
}
XCTAssertEqual(receivedTransaction.expiryHeight, transaction.expiryHeight, "ReceivedTransaction differs from mocked one.")
}
XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.")
XCTAssertTrue(internalSyncProgressStorageMock.integerForCalled, "internalSyncProgress.load() 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)")
}
}
func testEnhanceAction_EnhancementOfBlocksCalled_minedTransaction() async throws {
let blockEnhancerMock = BlockEnhancerMock()
let transactionRepositoryMock = TransactionRepositoryMock()
let internalSyncProgressStorageMock = InternalSyncProgressStorageMock()
transactionRepositoryMock.lastScannedHeightReturnValue = 1500
internalSyncProgressStorageMock.integerForReturnValue = 1
let transaction = ZcashTransaction.Overview(
accountId: 0,
blockTime: 1.0,
expiryHeight: 663206,
fee: Zatoshi(0),
id: 2,
index: 1,
hasChange: false,
memoCount: 1,
minedHeight: 663188,
raw: Data(),
rawID: Data(),
receivedNoteCount: 1,
sentNoteCount: 0,
value: Zatoshi(100000),
isExpiredUmined: false
)
blockEnhancerMock.enhanceAtDidEnhanceClosure = { _, didEnhance in
await didEnhance(
EnhancementProgress(
totalTransactions: 0,
enhancedTransactions: 0,
lastFoundTransaction: transaction,
range: 0...0,
newlyMined: true
)
)
return nil
}
let enhanceAction = setupAction(
blockEnhancerMock,
transactionRepositoryMock,
internalSyncProgressStorageMock
)
underlyingEnhanceRange = CompactBlockRange(uncheckedBounds: (1000, 2000))
let syncContext = await setupActionContext()
do {
_ = try await enhanceAction.run(with: syncContext) { event in
guard case .minedTransaction(let minedTransaction) = event else {
XCTFail("Event is expected to be .minedTransaction but received \(event)")
return
}
XCTAssertEqual(minedTransaction.expiryHeight, transaction.expiryHeight, "MinedTransaction differs from mocked one.")
}
XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.")
XCTAssertTrue(internalSyncProgressStorageMock.integerForCalled, "internalSyncProgress.load() 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)")
}
}
func testEnhanceAction_EnhancementOfBlocksCalled_usingSmallRange_minedTransaction() async throws {
let blockEnhancerMock = BlockEnhancerMock()
let transactionRepositoryMock = TransactionRepositoryMock()
let internalSyncProgressStorageMock = InternalSyncProgressStorageMock()
transactionRepositoryMock.lastScannedHeightReturnValue = 200
internalSyncProgressStorageMock.integerForReturnValue = 1
let transaction = ZcashTransaction.Overview(
accountId: 0,
blockTime: 1.0,
expiryHeight: 663206,
fee: Zatoshi(0),
id: 2,
index: 1,
hasChange: false,
memoCount: 1,
minedHeight: 663188,
raw: Data(),
rawID: Data(),
receivedNoteCount: 1,
sentNoteCount: 0,
value: Zatoshi(100000),
isExpiredUmined: false
)
blockEnhancerMock.enhanceAtDidEnhanceClosure = { _, didEnhance in
await didEnhance(
EnhancementProgress(
totalTransactions: 0,
enhancedTransactions: 0,
lastFoundTransaction: transaction,
range: 0...0,
newlyMined: true
)
)
return nil
}
let enhanceAction = setupAction(
blockEnhancerMock,
transactionRepositoryMock,
internalSyncProgressStorageMock
)
underlyingEnhanceRange = CompactBlockRange(uncheckedBounds: (100, 200))
let syncContext = await setupActionContext()
do {
_ = try await enhanceAction.run(with: syncContext) { event in
guard case .minedTransaction(let minedTransaction) = event else {
XCTFail("Event is expected to be .minedTransaction but received \(event)")
return
}
XCTAssertEqual(minedTransaction.expiryHeight, transaction.expiryHeight, "MinedTransaction differs from mocked one.")
}
XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.")
XCTAssertTrue(internalSyncProgressStorageMock.integerForCalled, "internalSyncProgress.load() 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)")
}
}
private func setupActionContext() async -> ActionContext {
let syncContext: ActionContext = .init(state: .enhance)
let syncRanges = SyncRanges(
latestBlockHeight: 0,
downloadRange: underlyingDownloadRange,
scanRange: underlyingScanRange,
enhanceRange: underlyingEnhanceRange,
fetchUTXORange: nil,
latestScannedHeight: nil,
latestDownloadedBlockHeight: nil
)
await syncContext.update(syncRanges: syncRanges)
await syncContext.update(totalProgressRange: CompactBlockRange(uncheckedBounds: (1000, 2000)))
return syncContext
}
private func setupAction(
_ blockEnhancerMock: BlockEnhancerMock = BlockEnhancerMock(),
_ transactionRepositoryMock: TransactionRepositoryMock = TransactionRepositoryMock(),
_ internalSyncProgressStorageMock: InternalSyncProgressStorageMock = InternalSyncProgressStorageMock(),
_ loggerMock: LoggerMock = LoggerMock()
) -> EnhanceAction {
mockContainer.register(type: InternalSyncProgress.self, isSingleton: true) { _ in
InternalSyncProgress(alias: .default, storage: internalSyncProgressStorageMock, logger: loggerMock)
}
mockContainer.mock(type: BlockEnhancer.self, isSingleton: true) { _ in blockEnhancerMock }
mockContainer.mock(type: TransactionRepository.self, isSingleton: true) { _ in transactionRepositoryMock }
mockContainer.mock(type: Logger.self, isSingleton: true) { _ in loggerMock }
let config: CompactBlockProcessor.Configuration = .standard(
for: ZcashNetworkBuilder.network(for: .testnet), walletBirthday: 0
)
return EnhanceAction(
container: mockContainer,
configProvider: CompactBlockProcessor.ConfigProvider(config: config)
)
}
}

View File

@ -0,0 +1,61 @@
//
// FetchUTXOsActionTests.swift
//
//
// Created by Lukáš Korba on 18.05.2023.
//
import XCTest
@testable import TestUtils
@testable import ZcashLightClientKit
final class FetchUTXOsActionTests: ZcashTestCase {
func testFetchUTXOsAction_NextAction() async throws {
let loggerMock = LoggerMock()
let uTXOFetcherMock = UTXOFetcherMock()
loggerMock.debugFileFunctionLineClosure = { _, _, _, _ in }
let insertedEntity = UnspentTransactionOutputEntityMock(address: "addr", txid: Data(), index: 0, script: Data(), valueZat: 1, height: 2)
let skippedEntity = UnspentTransactionOutputEntityMock(address: "addr2", txid: Data(), index: 1, script: Data(), valueZat: 2, height: 3)
uTXOFetcherMock.fetchAtDidFetchReturnValue = (inserted: [insertedEntity], skipped: [skippedEntity])
mockContainer.mock(type: Logger.self, isSingleton: true) { _ in loggerMock }
mockContainer.mock(type: UTXOFetcher.self, isSingleton: true) { _ in uTXOFetcherMock }
let fetchUTXOsAction = FetchUTXOsAction(container: mockContainer)
let syncContext: ActionContext = .init(state: .fetchUTXO)
let syncRanges = SyncRanges(
latestBlockHeight: 0,
downloadRange: nil,
scanRange: nil,
enhanceRange: nil,
fetchUTXORange: CompactBlockRange(uncheckedBounds: (1000, 2000)),
latestScannedHeight: nil,
latestDownloadedBlockHeight: nil
)
await syncContext.update(syncRanges: syncRanges)
do {
let nextContext = try await fetchUTXOsAction.run(with: syncContext) { event in
guard case .storedUTXOs(let result) = event else {
XCTFail("testFetchUTXOsAction_NextAction event expected to be .storedUTXOs but received \(event)")
return
}
XCTAssertEqual(result.inserted as! [UnspentTransactionOutputEntityMock], [insertedEntity])
XCTAssertEqual(result.skipped as! [UnspentTransactionOutputEntityMock], [skippedEntity])
}
XCTAssertTrue(loggerMock.debugFileFunctionLineCalled, "logger.debug(...) is expected to be called.")
XCTAssertTrue(uTXOFetcherMock.fetchAtDidFetchCalled, "utxoFetcher.fetch() is expected to be called.")
let nextState = await nextContext.state
XCTAssertTrue(
nextState == .handleSaplingParams,
"nextContext after .fetchUTXO is expected to be .handleSaplingParams but received \(nextState)"
)
} catch {
XCTFail("testFetchUTXOsAction_NextAction is not expected to fail. \(error)")
}
}
}

View File

@ -0,0 +1,299 @@
//
// MigrateLegacyCacheDBActionTests.swift
//
//
// Created by Lukáš Korba on 23.05.2023.
//
import XCTest
@testable import TestUtils
@testable import ZcashLightClientKit
final class MigrateLegacyCacheDBActionTests: ZcashTestCase {
var underlyingAlias: ZcashSynchronizerAlias?
var underlyingCacheDbURL: URL?
var underlyingFsBlockCacheRoot: URL?
override func setUp() {
super.setUp()
underlyingAlias = nil
underlyingCacheDbURL = nil
underlyingFsBlockCacheRoot = nil
}
func testMigrateLegacyCacheDBAction_noCacheDbURL() async throws {
let compactBlockRepositoryMock = CompactBlockRepositoryMock()
let transactionRepositoryMock = TransactionRepositoryMock()
let zcashFileManagerMock = ZcashFileManagerMock()
let internalSyncProgressStorageMock = InternalSyncProgressStorageMock()
let migrateLegacyCacheDBAction = setupAction(
compactBlockRepositoryMock,
transactionRepositoryMock,
zcashFileManagerMock,
internalSyncProgressStorageMock
)
do {
let nextContext = try await migrateLegacyCacheDBAction.run(with: .init(state: .migrateLegacyCacheDB)) { _ in }
XCTAssertFalse(compactBlockRepositoryMock.createCalled, "storage.create() is not expected to be called.")
XCTAssertFalse(
transactionRepositoryMock.lastScannedHeightCalled,
"transactionRepository.lastScannedHeight() is not expected to be called."
)
XCTAssertFalse(zcashFileManagerMock.isReadableFileAtPathCalled, "fileManager.isReadableFile() is not expected to be called.")
XCTAssertFalse(zcashFileManagerMock.removeItemAtCalled, "fileManager.removeItem() is not expected to be called.")
XCTAssertFalse(internalSyncProgressStorageMock.setForCalled, "internalSyncProgress.set() is not expected to be called.")
let nextState = await nextContext.state
XCTAssertTrue(
nextState == .validateServer,
"nextContext after .migrateLegacyCacheDB is expected to be .validateServer but received \(nextState)"
)
} catch {
XCTFail("testMigrateLegacyCacheDBAction_noCacheDbURL is not expected to fail. \(error)")
}
}
func testMigrateLegacyCacheDBAction_noFsBlockCacheRoot() async throws {
let compactBlockRepositoryMock = CompactBlockRepositoryMock()
let transactionRepositoryMock = TransactionRepositoryMock()
let zcashFileManagerMock = ZcashFileManagerMock()
let internalSyncProgressStorageMock = InternalSyncProgressStorageMock()
underlyingCacheDbURL = DefaultResourceProvider(network: ZcashNetworkBuilder.network(for: .testnet)).fsCacheURL
let migrateLegacyCacheDBAction = setupAction(
compactBlockRepositoryMock,
transactionRepositoryMock,
zcashFileManagerMock,
internalSyncProgressStorageMock
)
do {
_ = try await migrateLegacyCacheDBAction.run(with: .init(state: .migrateLegacyCacheDB)) { _ in }
XCTFail("testMigrateLegacyCacheDBAction_noFsBlockCacheRoot is expected to fail.")
} catch ZcashError.compactBlockProcessorCacheDbMigrationFsCacheMigrationFailedSameURL {
XCTAssertFalse(compactBlockRepositoryMock.createCalled, "storage.create() is not expected to be called.")
XCTAssertFalse(
transactionRepositoryMock.lastScannedHeightCalled,
"transactionRepository.lastScannedHeight() is not expected to be called."
)
XCTAssertFalse(zcashFileManagerMock.isReadableFileAtPathCalled, "fileManager.isReadableFile() is not expected to be called.")
XCTAssertFalse(zcashFileManagerMock.removeItemAtCalled, "fileManager.removeItem() is not expected to be called.")
XCTAssertFalse(internalSyncProgressStorageMock.setForCalled, "internalSyncProgress.set() is not expected to be called.")
} catch {
XCTFail("""
testMigrateLegacyCacheDBAction_noFsBlockCacheRoot is expected to fail with \
ZcashError.compactBlockProcessorCacheDbMigrationFsCacheMigrationFailedSameURL but received \(error)
""")
}
}
func testMigrateLegacyCacheDBAction_aliasDoesntMatchDefault() async throws {
let compactBlockRepositoryMock = CompactBlockRepositoryMock()
let transactionRepositoryMock = TransactionRepositoryMock()
let zcashFileManagerMock = ZcashFileManagerMock()
let internalSyncProgressStorageMock = InternalSyncProgressStorageMock()
// any valid URL needed...
underlyingCacheDbURL = DefaultResourceProvider(network: ZcashNetworkBuilder.network(for: .testnet)).fsCacheURL
underlyingFsBlockCacheRoot = DefaultResourceProvider(network: ZcashNetworkBuilder.network(for: .testnet)).dataDbURL
underlyingAlias = .custom("any")
let migrateLegacyCacheDBAction = setupAction(
compactBlockRepositoryMock,
transactionRepositoryMock,
zcashFileManagerMock,
internalSyncProgressStorageMock
)
do {
let nextContext = try await migrateLegacyCacheDBAction.run(with: .init(state: .migrateLegacyCacheDB)) { _ in }
XCTAssertFalse(compactBlockRepositoryMock.createCalled, "storage.create() is not expected to be called.")
XCTAssertFalse(
transactionRepositoryMock.lastScannedHeightCalled,
"transactionRepository.lastScannedHeight() is not expected to be called."
)
XCTAssertFalse(zcashFileManagerMock.isReadableFileAtPathCalled, "fileManager.isReadableFile() is not expected to be called.")
XCTAssertFalse(zcashFileManagerMock.removeItemAtCalled, "fileManager.removeItem() is not expected to be called.")
XCTAssertFalse(internalSyncProgressStorageMock.setForCalled, "internalSyncProgress.set() is not expected to be called.")
let nextState = await nextContext.state
XCTAssertTrue(
nextState == .validateServer,
"nextContext after .migrateLegacyCacheDB is expected to be .validateServer but received \(nextState)"
)
} catch {
XCTFail("testMigrateLegacyCacheDBAction_aliasDoesntMatchDefault is not expected to fail. \(error)")
}
}
func testMigrateLegacyCacheDBAction_isNotReadableFile() async throws {
let compactBlockRepositoryMock = CompactBlockRepositoryMock()
let transactionRepositoryMock = TransactionRepositoryMock()
let zcashFileManagerMock = ZcashFileManagerMock()
let internalSyncProgressStorageMock = InternalSyncProgressStorageMock()
// any valid URL needed...
underlyingCacheDbURL = DefaultResourceProvider(network: ZcashNetworkBuilder.network(for: .testnet)).fsCacheURL
underlyingFsBlockCacheRoot = DefaultResourceProvider(network: ZcashNetworkBuilder.network(for: .testnet)).dataDbURL
zcashFileManagerMock.isReadableFileAtPathReturnValue = false
let migrateLegacyCacheDBAction = setupAction(
compactBlockRepositoryMock,
transactionRepositoryMock,
zcashFileManagerMock,
internalSyncProgressStorageMock
)
do {
let nextContext = try await migrateLegacyCacheDBAction.run(with: .init(state: .migrateLegacyCacheDB)) { _ in }
XCTAssertFalse(compactBlockRepositoryMock.createCalled, "storage.create() is not expected to be called.")
XCTAssertFalse(
transactionRepositoryMock.lastScannedHeightCalled,
"transactionRepository.lastScannedHeight() is not expected to be called."
)
XCTAssertTrue(zcashFileManagerMock.isReadableFileAtPathCalled, "fileManager.isReadableFile() is expected to be called.")
XCTAssertFalse(zcashFileManagerMock.removeItemAtCalled, "fileManager.removeItem() is not expected to be called.")
XCTAssertFalse(internalSyncProgressStorageMock.setForCalled, "internalSyncProgress.set() is not expected to be called.")
let nextState = await nextContext.state
XCTAssertTrue(
nextState == .validateServer,
"nextContext after .migrateLegacyCacheDB is expected to be .validateServer but received \(nextState)"
)
} catch {
XCTFail("testMigrateLegacyCacheDBAction_isNotReadableFile is not expected to fail. \(error)")
}
}
func testMigrateLegacyCacheDBAction_removeItemFailed() async throws {
let compactBlockRepositoryMock = CompactBlockRepositoryMock()
let transactionRepositoryMock = TransactionRepositoryMock()
let zcashFileManagerMock = ZcashFileManagerMock()
let internalSyncProgressStorageMock = InternalSyncProgressStorageMock()
// any valid URL needed...
underlyingCacheDbURL = DefaultResourceProvider(network: ZcashNetworkBuilder.network(for: .testnet)).fsCacheURL
underlyingFsBlockCacheRoot = DefaultResourceProvider(network: ZcashNetworkBuilder.network(for: .testnet)).dataDbURL
zcashFileManagerMock.isReadableFileAtPathReturnValue = true
zcashFileManagerMock.removeItemAtClosure = { _ in throw "remove failed" }
let migrateLegacyCacheDBAction = setupAction(
compactBlockRepositoryMock,
transactionRepositoryMock,
zcashFileManagerMock,
internalSyncProgressStorageMock
)
do {
_ = try await migrateLegacyCacheDBAction.run(with: .init(state: .migrateLegacyCacheDB)) { _ in }
} catch ZcashError.compactBlockProcessorCacheDbMigrationFailedToDeleteLegacyDb {
XCTAssertFalse(compactBlockRepositoryMock.createCalled, "storage.create() is not expected to be called.")
XCTAssertFalse(
transactionRepositoryMock.lastScannedHeightCalled,
"transactionRepository.lastScannedHeight() is not expected to be called."
)
XCTAssertTrue(zcashFileManagerMock.isReadableFileAtPathCalled, "fileManager.isReadableFile() is expected to be called.")
XCTAssertTrue(zcashFileManagerMock.removeItemAtCalled, "fileManager.removeItem() is expected to be called.")
XCTAssertFalse(internalSyncProgressStorageMock.setForCalled, "internalSyncProgress.set() is not expected to be called.")
} catch {
XCTFail("""
testMigrateLegacyCacheDBAction_removeItemFailed is expected to fail with \
ZcashError.compactBlockProcessorCacheDbMigrationFailedToDeleteLegacyDb but received \(error)
""")
}
}
func testMigrateLegacyCacheDBAction_nextAction() async throws {
let compactBlockRepositoryMock = CompactBlockRepositoryMock()
let transactionRepositoryMock = TransactionRepositoryMock()
let zcashFileManagerMock = ZcashFileManagerMock()
let internalSyncProgressStorageMock = InternalSyncProgressStorageMock()
// any valid URL needed...
underlyingCacheDbURL = DefaultResourceProvider(network: ZcashNetworkBuilder.network(for: .testnet)).fsCacheURL
underlyingFsBlockCacheRoot = DefaultResourceProvider(network: ZcashNetworkBuilder.network(for: .testnet)).dataDbURL
zcashFileManagerMock.isReadableFileAtPathReturnValue = true
zcashFileManagerMock.removeItemAtClosure = { _ in }
compactBlockRepositoryMock.createClosure = { }
transactionRepositoryMock.lastScannedHeightReturnValue = 1
internalSyncProgressStorageMock.setForClosure = { _, _ in }
let migrateLegacyCacheDBAction = setupAction(
compactBlockRepositoryMock,
transactionRepositoryMock,
zcashFileManagerMock,
internalSyncProgressStorageMock
)
do {
let nextContext = try await migrateLegacyCacheDBAction.run(with: .init(state: .migrateLegacyCacheDB)) { _ in }
XCTAssertTrue(compactBlockRepositoryMock.createCalled, "storage.create() is expected to be called.")
XCTAssertTrue(transactionRepositoryMock.lastScannedHeightCalled, "transactionRepository.lastScannedHeight() is expected to be called.")
XCTAssertTrue(zcashFileManagerMock.isReadableFileAtPathCalled, "fileManager.isReadableFile() is expected to be called.")
XCTAssertTrue(zcashFileManagerMock.removeItemAtCalled, "fileManager.removeItem() is expected to be called.")
XCTAssertTrue(internalSyncProgressStorageMock.setForCalled, "internalSyncProgress.set() is expected to be called.")
let nextState = await nextContext.state
XCTAssertTrue(
nextState == .validateServer,
"nextContext after .migrateLegacyCacheDB is expected to be .validateServer but received \(nextState)"
)
} catch {
XCTFail("testMigrateLegacyCacheDBAction_nextAction is not expected to fail. \(error)")
}
}
private func setupAction(
_ compactBlockRepositoryMock: CompactBlockRepositoryMock = CompactBlockRepositoryMock(),
_ transactionRepositoryMock: TransactionRepositoryMock = TransactionRepositoryMock(),
_ zcashFileManagerMock: ZcashFileManagerMock = ZcashFileManagerMock(),
_ internalSyncProgressStorageMock: InternalSyncProgressStorageMock = InternalSyncProgressStorageMock(),
_ loggerMock: LoggerMock = LoggerMock()
) -> MigrateLegacyCacheDBAction {
mockContainer.register(type: InternalSyncProgress.self, isSingleton: true) { _ in
InternalSyncProgress(alias: .default, storage: internalSyncProgressStorageMock, logger: loggerMock)
}
mockContainer.mock(type: CompactBlockRepository.self, isSingleton: true) { _ in compactBlockRepositoryMock }
mockContainer.mock(type: TransactionRepository.self, isSingleton: true) { _ in transactionRepositoryMock }
mockContainer.mock(type: ZcashFileManager.self, isSingleton: true) { _ in zcashFileManagerMock }
mockContainer.mock(type: Logger.self, isSingleton: true) { _ in loggerMock }
return MigrateLegacyCacheDBAction(
container: mockContainer,
configProvider: setupConfig()
)
}
private func setupConfig() -> CompactBlockProcessor.ConfigProvider {
let defaultConfig = CompactBlockProcessor.Configuration.standard(
for: ZcashNetworkBuilder.network(for: .testnet), walletBirthday: 0
)
let config = CompactBlockProcessor.Configuration(
alias: underlyingAlias ?? defaultConfig.alias,
cacheDbURL: underlyingCacheDbURL ?? defaultConfig.cacheDbURL,
fsBlockCacheRoot: underlyingFsBlockCacheRoot ?? defaultConfig.fsBlockCacheRoot,
dataDb: defaultConfig.dataDb,
spendParamsURL: defaultConfig.spendParamsURL,
outputParamsURL: defaultConfig.outputParamsURL,
saplingParamsSourceURL: defaultConfig.saplingParamsSourceURL,
walletBirthdayProvider: defaultConfig.walletBirthdayProvider,
saplingActivation: defaultConfig.saplingActivation,
network: defaultConfig.network
)
return CompactBlockProcessor.ConfigProvider(config: config)
}
}

View File

@ -0,0 +1,38 @@
//
// SaplingParamsActionTests.swift
//
//
// Created by Lukáš Korba on 18.05.2023.
//
import XCTest
@testable import TestUtils
@testable import ZcashLightClientKit
final class SaplingParamsActionTests: ZcashTestCase {
func testSaplingParamsAction_NextAction() async throws {
let loggerMock = LoggerMock()
let saplingParametersHandlerMock = SaplingParametersHandlerMock()
loggerMock.debugFileFunctionLineClosure = { _, _, _, _ in }
saplingParametersHandlerMock.handleIfNeededClosure = { }
mockContainer.mock(type: Logger.self, isSingleton: true) { _ in loggerMock }
mockContainer.mock(type: SaplingParametersHandler.self, isSingleton: true) { _ in saplingParametersHandlerMock }
let saplingParamsActionAction = SaplingParamsAction(container: mockContainer)
do {
let nextContext = try await saplingParamsActionAction.run(with: .init(state: .handleSaplingParams)) { _ in }
XCTAssertTrue(loggerMock.debugFileFunctionLineCalled, "logger.debug(...) is expected to be called.")
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)"
)
} catch {
XCTFail("testSaplingParamsAction_NextAction is not expected to fail. \(error)")
}
}
}

View File

@ -0,0 +1,126 @@
//
// ScanActionTests.swift
//
//
// Created by Lukáš Korba on 18.05.2023.
//
import XCTest
@testable import TestUtils
@testable import ZcashLightClientKit
final class ScanActionTests: ZcashTestCase {
func testScanAction_NextAction() async throws {
let blockScannerMock = BlockScannerMock()
let transactionRepositoryMock = TransactionRepositoryMock()
let loggerMock = LoggerMock()
transactionRepositoryMock.lastScannedHeightReturnValue = 1500
loggerMock.debugFileFunctionLineClosure = { _, _, _, _ in }
blockScannerMock.scanBlocksAtTotalProgressRangeDidScanClosure = { _, _, _ in 2 }
let scanAction = setupAction(blockScannerMock, transactionRepositoryMock, loggerMock)
let syncContext = await setupActionContext()
do {
let nextContext = try await scanAction.run(with: syncContext) { event in
guard case .progressPartialUpdate(.syncing(let progress)) = event else {
XCTFail("event is expected to be .progressPartialUpdate(.syncing()) but received \(event)")
return
}
XCTAssertEqual(progress.startHeight, BlockHeight(1000))
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
XCTAssertTrue(
nextState == .clearAlreadyScannedBlocks,
"nextContext after .scan is expected to be .clearAlreadyScannedBlocks but received \(nextState)"
)
} catch {
XCTFail("testScanAction_NextAction is not expected to fail. \(error)")
}
}
func testScanAction_EarlyOutForNoDownloadAndScanRangeSet() async throws {
let blockScannerMock = BlockScannerMock()
let transactionRepositoryMock = TransactionRepositoryMock()
let loggerMock = LoggerMock()
let scanAction = setupAction(blockScannerMock, transactionRepositoryMock, loggerMock)
let syncContext: ActionContext = .init(state: .scan)
do {
_ = try await scanAction.run(with: syncContext) { _ in }
XCTAssertFalse(
transactionRepositoryMock.lastScannedHeightCalled,
"transactionRepository.lastScannedHeight() is not 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 {
XCTFail("testScanAction_EarlyOutForNoDownloadAndScanRangeSet is not expected to fail. \(error)")
}
}
func testScanAction_StartRangeHigherThanEndRange() async throws {
let blockScannerMock = BlockScannerMock()
let transactionRepositoryMock = TransactionRepositoryMock()
let loggerMock = LoggerMock()
transactionRepositoryMock.lastScannedHeightReturnValue = 2001
let scanAction = setupAction(blockScannerMock, transactionRepositoryMock, loggerMock)
let syncContext = await setupActionContext()
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 {
XCTFail("testScanAction_StartRangeHigherThanEndRange is not expected to fail. \(error)")
}
}
private func setupAction(
_ blockScannerMock: BlockScannerMock,
_ transactionRepositoryMock: TransactionRepositoryMock,
_ loggerMock: LoggerMock
) -> ScanAction {
mockContainer.mock(type: BlockScanner.self, isSingleton: true) { _ in blockScannerMock }
mockContainer.mock(type: TransactionRepository.self, isSingleton: true) { _ in transactionRepositoryMock }
mockContainer.mock(type: Logger.self, isSingleton: true) { _ in loggerMock }
let config: CompactBlockProcessor.Configuration = .standard(
for: ZcashNetworkBuilder.network(for: .testnet), walletBirthday: 0
)
return ScanAction(
container: mockContainer,
configProvider: CompactBlockProcessor.ConfigProvider(config: config)
)
}
private func setupActionContext() async -> ActionContext {
let syncContext: ActionContext = .init(state: .scan)
let syncRanges = SyncRanges(
latestBlockHeight: 0,
downloadRange: CompactBlockRange(uncheckedBounds: (1000, 2000)),
scanRange: CompactBlockRange(uncheckedBounds: (1000, 2000)),
enhanceRange: nil,
fetchUTXORange: nil,
latestScannedHeight: nil,
latestDownloadedBlockHeight: nil
)
await syncContext.update(syncRanges: syncRanges)
await syncContext.update(totalProgressRange: CompactBlockRange(uncheckedBounds: (1000, 2000)))
return syncContext
}
}

View File

@ -0,0 +1,36 @@
//
// ValidateActionTests.swift
//
//
// Created by Lukáš Korba on 17.05.2023.
//
import XCTest
@testable import TestUtils
@testable import ZcashLightClientKit
final class ValidateActionTests: ZcashTestCase {
func testValidateAction_NextAction() async throws {
let blockValidatorMock = BlockValidatorMock()
blockValidatorMock.validateClosure = { }
mockContainer.mock(type: BlockValidator.self, isSingleton: true) { _ in blockValidatorMock }
let validateAction = ValidateAction(
container: mockContainer
)
do {
let nextContext = try await validateAction.run(with: .init(state: .validate)) { _ in }
XCTAssertTrue(blockValidatorMock.validateCalled, "validator.validate() is expected to be called.")
let nextState = await nextContext.state
XCTAssertTrue(
nextState == .scan,
"nextContext after .validate is expected to be .scan but received \(nextState)"
)
} catch {
XCTFail("testValidateAction_NextAction is not expected to fail. \(error)")
}
}
}

View File

@ -0,0 +1,163 @@
//
// ValidateServerActionTests.swift
//
//
// Created by Lukáš Korba on 16.05.2023.
//
import XCTest
@testable import TestUtils
@testable import ZcashLightClientKit
final class ValidateServerActionTests: ZcashTestCase {
var underlyingChainName = ""
var underlyingNetworkType = NetworkType.testnet
var underlyingSaplingActivationHeight: BlockHeight?
var underlyingConsensusBranchID = ""
override func setUp() {
super.setUp()
underlyingChainName = "test"
underlyingNetworkType = .testnet
underlyingSaplingActivationHeight = nil
underlyingConsensusBranchID = "c2d6d0b4"
}
func testValidateServerAction_NextAction() async throws {
let validateServerAction = setupAction()
do {
let nextContext = try await validateServerAction.run(with: .init(state: .validateServer)) { _ in }
let nextState = await nextContext.state
XCTAssertTrue(
nextState == .computeSyncRanges,
"nextContext after .validateServer is expected to be .computeSyncRanges but received \(nextState)"
)
} catch {
XCTFail("testValidateServerAction_NextAction is not expected to fail. \(error)")
}
}
func testValidateServerAction_ChainNameError() async throws {
underlyingChainName = "invalid"
let validateServerAction = setupAction()
do {
_ = try await validateServerAction.run(with: .init(state: .validateServer)) { _ in }
XCTFail("testValidateServerAction_ChainNameError is expected to fail.")
} catch ZcashError.compactBlockProcessorChainName(let chainName) {
XCTAssertEqual(chainName, "invalid")
} catch {
XCTFail("""
testValidateServerAction_ChainNameError is expected to fail but error \(error) doesn't match \
ZcashError.compactBlockProcessorChainName
""")
}
}
func testValidateServerAction_NetworkMatchError() async throws {
underlyingNetworkType = .mainnet
let validateServerAction = setupAction()
do {
_ = try await validateServerAction.run(with: .init(state: .validateServer)) { _ in }
XCTFail("testValidateServerAction_NetworkMatchError is expected to fail.")
} catch let ZcashError.compactBlockProcessorNetworkMismatch(expected, found) {
XCTAssertEqual(expected, .mainnet)
XCTAssertEqual(found, .testnet)
} catch {
XCTFail("""
testValidateServerAction_NetworkMatchError is expected to fail but error \(error) doesn't match \
ZcashError.compactBlockProcessorNetworkMismatch
""")
}
}
func testValidateServerAction_SaplingActivationError() async throws {
underlyingSaplingActivationHeight = 1
let validateServerAction = setupAction()
do {
_ = try await validateServerAction.run(with: .init(state: .validateServer)) { _ in }
XCTFail("testValidateServerAction_SaplingActivationError is expected to fail.")
} catch let ZcashError.compactBlockProcessorSaplingActivationMismatch(expected, found) {
XCTAssertEqual(expected, 280_000)
XCTAssertEqual(found, 1)
} catch {
XCTFail("""
testValidateServerAction_SaplingActivationError is expected to fail but error \(error) doesn't match \
ZcashError.compactBlockProcessorSaplingActivationMismatch
""")
}
}
func testValidateServerAction_ConsensusBranchIDError_InvalidRemoteBranch() async throws {
underlyingConsensusBranchID = "1 1"
let validateServerAction = setupAction()
do {
_ = try await validateServerAction.run(with: .init(state: .validateServer)) { _ in }
XCTFail("testValidateServerAction_ConsensusBranchIDError_InvalidRemoteBranch is expected to fail.")
} catch ZcashError.compactBlockProcessorConsensusBranchID {
} catch {
XCTFail("""
testValidateServerAction_ConsensusBranchIDError_InvalidRemoteBranch is expected to fail but error \(error) doesn't match \
ZcashError.compactBlockProcessorConsensusBranchID
""")
}
}
func testValidateServerAction_ConsensusBranchIDError_ValidRemoteBranch() async throws {
underlyingConsensusBranchID = "1"
let validateServerAction = setupAction()
do {
_ = try await validateServerAction.run(with: .init(state: .validateServer)) { _ in }
XCTFail("testValidateServerAction_ConsensusBranchIDError_ValidRemoteBranch is expected to fail.")
} catch let ZcashError.compactBlockProcessorWrongConsensusBranchId(expected, found) {
XCTAssertEqual(expected, -1026109260)
XCTAssertEqual(found, 1)
} catch {
XCTFail("""
testValidateServerAction_ConsensusBranchIDError_ValidRemoteBranch is expected to fail but error \(error) doesn't match \
ZcashError.compactBlockProcessorWrongConsensusBranchId
""")
}
}
private func setupAction() -> ValidateServerAction {
let config: CompactBlockProcessor.Configuration = .standard(
for: ZcashNetworkBuilder.network(for: underlyingNetworkType), walletBirthday: 0
)
let rustBackendMock = ZcashRustBackendWeldingMock(
consensusBranchIdForHeightClosure: { height in
XCTAssertEqual(height, 2, "")
return -1026109260
}
)
let lightWalletdInfoMock = LightWalletdInfoMock()
lightWalletdInfoMock.underlyingConsensusBranchID = underlyingConsensusBranchID
lightWalletdInfoMock.underlyingSaplingActivationHeight = UInt64(underlyingSaplingActivationHeight ?? config.saplingActivation)
lightWalletdInfoMock.underlyingBlockHeight = 2
lightWalletdInfoMock.underlyingChainName = underlyingChainName
let serviceMock = LightWalletServiceMock()
serviceMock.getInfoReturnValue = lightWalletdInfoMock
mockContainer.mock(type: ZcashRustBackendWelding.self, isSingleton: true) { _ in rustBackendMock }
mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in serviceMock }
return ValidateServerAction(
container: mockContainer,
configProvider: CompactBlockProcessor.ConfigProvider(config: config)
)
}
}

View File

@ -1,83 +0,0 @@
//
// CompactBlockProcessorOfflineTests.swift
//
//
// Created by Michal Fousek on 15.12.2022.
//
import XCTest
@testable import TestUtils
@testable import ZcashLightClientKit
class CompactBlockProcessorOfflineTests: ZcashTestCase {
let testFileManager = FileManager()
override func setUpWithError() throws {
try super.setUpWithError()
Dependencies.setup(
in: mockContainer,
urls: Initializer.URLs(
fsBlockDbRoot: testTempDirectory,
dataDbURL: try! __dataDbURL(),
generalStorageURL: testGeneralStorageDirectory,
spendParamsURL: try! __spendParamsURL(),
outputParamsURL: try! __outputParamsURL()
),
alias: .default,
networkType: .testnet,
endpoint: LightWalletEndpointBuilder.default,
loggingPolicy: .default(.debug)
)
}
override func tearDownWithError() throws {
try super.tearDownWithError()
}
func testComputeProcessingRangeForSingleLoop() async throws {
let network = ZcashNetworkBuilder.network(for: .testnet)
let rustBackend = ZcashRustBackend.makeForTests(fsBlockDbRoot: testTempDirectory, networkType: .testnet)
let processorConfig = CompactBlockProcessor.Configuration.standard(
for: network,
walletBirthday: ZcashNetworkBuilder.network(for: .testnet).constants.saplingActivationHeight
)
let service = MockLightWalletService(
latestBlockHeight: 690000,
service: LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.eccTestnet).make()
)
mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in service }
let storage = FSCompactBlockRepository(
fsBlockDbRoot: testTempDirectory,
metadataStore: FSMetadataStore.live(
fsBlockDbRoot: testTempDirectory,
rustBackend: rustBackend,
logger: logger
),
blockDescriptor: .live,
contentProvider: DirectoryListingProviders.defaultSorted,
logger: logger
)
mockContainer.mock(type: CompactBlockRepository.self, isSingleton: true) { _ in storage }
mockContainer.mock(type: LatestBlocksDataProvider.self, isSingleton: true) { _ in LatestBlocksDataProviderMock() }
let processor = CompactBlockProcessor(
container: mockContainer,
config: processorConfig
)
let fullRange = 0...1000
var range = await processor.computeSingleLoopDownloadRange(fullRange: fullRange, loopCounter: 0, batchSize: 100)
XCTAssertEqual(range, 0...99)
range = await processor.computeSingleLoopDownloadRange(fullRange: fullRange, loopCounter: 5, batchSize: 100)
XCTAssertEqual(range, 500...599)
range = await processor.computeSingleLoopDownloadRange(fullRange: fullRange, loopCounter: 10, batchSize: 100)
XCTAssertEqual(range, 1000...1000)
}
}

View File

@ -66,8 +66,8 @@ class InternalSyncProgressTests: ZcashTestCase {
switch nextState {
case let .processNewBlocks(ranges):
XCTAssertEqual(ranges.downloadedButUnscannedRange, 620001...630000)
XCTAssertEqual(ranges.downloadAndScanRange, 630001...640000)
XCTAssertEqual(ranges.downloadRange, 630001...640000)
XCTAssertEqual(ranges.scanRange, 620001...640000)
XCTAssertEqual(ranges.enhanceRange, 630001...640000)
XCTAssertEqual(ranges.fetchUTXORange, 630001...640000)

View File

@ -368,7 +368,7 @@ class SynchronizerOfflineTests: ZcashTestCase {
}
func testIsNewSessionOnUnpreparedToValidTransition() {
XCTAssertTrue(SessionTicker.live.isNewSyncSession(.unprepared, .syncing(.nullProgress)))
XCTAssertTrue(SessionTicker.live.isNewSyncSession(.unprepared, .syncing(0)))
}
func testIsNotNewSessionOnUnpreparedToStateThatWontSync() {
@ -378,18 +378,16 @@ class SynchronizerOfflineTests: ZcashTestCase {
func testIsNotNewSessionOnUnpreparedToInvalidOrUnexpectedTransitions() {
XCTAssertFalse(SessionTicker.live.isNewSyncSession(.unprepared, .synced))
XCTAssertFalse(SessionTicker.live.isNewSyncSession(.unprepared, .fetching(0)))
XCTAssertFalse(SessionTicker.live.isNewSyncSession(.unprepared, .enhancing(.zero)))
}
func testIsNotNewSyncSessionOnSameSession() {
XCTAssertFalse(
SessionTicker.live.isNewSyncSession(
.syncing(
BlockProgress(startHeight: 1, targetHeight: 10, progressHeight: 3)
0.5
),
.syncing(
BlockProgress(startHeight: 1, targetHeight: 10, progressHeight: 4)
0.6
)
)
)
@ -400,7 +398,7 @@ class SynchronizerOfflineTests: ZcashTestCase {
SessionTicker.live.isNewSyncSession(
.synced,
.syncing(
BlockProgress(startHeight: 1, targetHeight: 10, progressHeight: 4)
0.6
)
)
)
@ -411,7 +409,7 @@ class SynchronizerOfflineTests: ZcashTestCase {
SessionTicker.live.isNewSyncSession(
.disconnected,
.syncing(
BlockProgress(startHeight: 1, targetHeight: 10, progressHeight: 4)
0.6
)
)
)
@ -422,7 +420,7 @@ class SynchronizerOfflineTests: ZcashTestCase {
SessionTicker.live.isNewSyncSession(
.stopped,
.syncing(
BlockProgress(startHeight: 1, targetHeight: 10, progressHeight: 4)
0.6
)
)
)
@ -430,17 +428,16 @@ class SynchronizerOfflineTests: ZcashTestCase {
func testInternalSyncStatusesDontDifferWhenOuterStatusIsTheSame() {
XCTAssertFalse(InternalSyncStatus.disconnected.isDifferent(from: .disconnected))
XCTAssertFalse(InternalSyncStatus.fetching(0).isDifferent(from: .fetching(0)))
XCTAssertFalse(InternalSyncStatus.syncing(0).isDifferent(from: .syncing(0)))
XCTAssertFalse(InternalSyncStatus.stopped.isDifferent(from: .stopped))
XCTAssertFalse(InternalSyncStatus.synced.isDifferent(from: .synced))
XCTAssertFalse(InternalSyncStatus.syncing(.nullProgress).isDifferent(from: .syncing(.nullProgress)))
XCTAssertFalse(InternalSyncStatus.unprepared.isDifferent(from: .unprepared))
}
func testInternalSyncStatusMap_SyncingLowerBound() {
let synchronizerState = synchronizerState(
for:
InternalSyncStatus.syncing(BlockProgress(startHeight: 0, targetHeight: 100, progressHeight: 0))
InternalSyncStatus.syncing(0)
)
if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(0.0, data) {
@ -451,7 +448,7 @@ class SynchronizerOfflineTests: ZcashTestCase {
func testInternalSyncStatusMap_SyncingInTheMiddle() {
let synchronizerState = synchronizerState(
for:
InternalSyncStatus.syncing(BlockProgress(startHeight: 0, targetHeight: 100, progressHeight: 50))
InternalSyncStatus.syncing(0.45)
)
if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(0.45, data) {
@ -462,89 +459,16 @@ class SynchronizerOfflineTests: ZcashTestCase {
func testInternalSyncStatusMap_SyncingUpperBound() {
let synchronizerState = synchronizerState(
for:
InternalSyncStatus.syncing(BlockProgress(startHeight: 0, targetHeight: 100, progressHeight: 100))
InternalSyncStatus.syncing(0.9)
)
if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(0.9, data) {
XCTFail("Syncing is expected to be 90% (0.9) but received \(data).")
}
}
func testInternalSyncStatusMap_EnhancingLowerBound() {
let synchronizerState = synchronizerState(
for:
InternalSyncStatus.enhancing(
EnhancementProgress(
totalTransactions: 100,
enhancedTransactions: 0,
lastFoundTransaction: nil,
range: CompactBlockRange(uncheckedBounds: (0, 100)),
newlyMined: false
)
)
)
if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(0.9, data) {
XCTFail("Syncing is expected to be 90% (0.9) but received \(data).")
}
}
func testInternalSyncStatusMap_EnhancingInTheMiddle() {
let synchronizerState = synchronizerState(
for:
InternalSyncStatus.enhancing(
EnhancementProgress(
totalTransactions: 100,
enhancedTransactions: 50,
lastFoundTransaction: nil,
range: CompactBlockRange(uncheckedBounds: (0, 100)),
newlyMined: false
)
)
)
if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(0.94, data) {
XCTFail("Syncing is expected to be 94% (0.94) but received \(data).")
}
}
func testInternalSyncStatusMap_EnhancingUpperBound() {
let synchronizerState = synchronizerState(
for:
InternalSyncStatus.enhancing(
EnhancementProgress(
totalTransactions: 100,
enhancedTransactions: 100,
lastFoundTransaction: nil,
range: CompactBlockRange(uncheckedBounds: (0, 100)),
newlyMined: false
)
)
)
if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(0.98, data) {
XCTFail("Syncing is expected to be 98% (0.98) but received \(data).")
}
}
func testInternalSyncStatusMap_FetchingLowerBound() {
let synchronizerState = synchronizerState(for: InternalSyncStatus.fetching(0))
if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(0.98, data) {
XCTFail("Syncing is expected to be 98% (0.98) but received \(data).")
}
}
func testInternalSyncStatusMap_FetchingInTheMiddle() {
let synchronizerState = synchronizerState(for: InternalSyncStatus.fetching(0.5))
if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(0.99, data) {
XCTFail("Syncing is expected to be 99% (0.99) but received \(data).")
}
}
func testInternalSyncStatusMap_FetchingUpperBound() {
let synchronizerState = synchronizerState(for: InternalSyncStatus.fetching(1))
let synchronizerState = synchronizerState(for: InternalSyncStatus.syncing(1))
if case let .syncing(data) = synchronizerState.syncStatus, data != nextafter(1.0, data) {
XCTFail("Syncing is expected to be 100% (1.0) but received \(data).")

View File

@ -18,6 +18,7 @@ class CompactBlockProcessorEventHandler {
case minedTransaction
case handleReorg
case progressUpdated
case progressPartialUpdate
case storedUTXOs
case startedEnhancing
case startedFetching
@ -63,6 +64,8 @@ extension CompactBlockProcessor.Event {
return .stopped
case .minedTransaction:
return .minedTransaction
case .progressPartialUpdate:
return .progressPartialUpdate
}
}
}

View File

@ -1,38 +0,0 @@
//
// LatestBlocksDataProviderMock.swift
//
//
// Created by Lukáš Korba on 12.04.2023.
//
import Foundation
@testable import ZcashLightClientKit
actor LatestBlocksDataProviderMock: LatestBlocksDataProvider {
private(set) var latestScannedHeight: BlockHeight = .zero
private(set) var latestScannedTime: TimeInterval = 0.0
private(set) var latestBlockHeight: BlockHeight = .zero
private(set) var walletBirthday: BlockHeight = .zero
init(
latestScannedHeight: BlockHeight = .zero,
latestScannedTime: TimeInterval = 0,
latestBlockHeight: BlockHeight = .zero,
walletBirthday: BlockHeight = .zero
) {
self.latestScannedHeight = latestScannedHeight
self.latestScannedTime = latestScannedTime
self.latestBlockHeight = latestBlockHeight
self.walletBirthday = walletBirthday
}
func updateScannedData() async { }
func updateBlockData() async { }
func updateWalletBirthday(_ walletBirthday: BlockHeight) async { }
func updateLatestScannedHeight(_ latestScannedHeight: BlockHeight) async { }
func updateLatestScannedTime(_ latestScannedTime: TimeInterval) async { }
}

View File

@ -39,8 +39,6 @@ extension InternalSyncStatus {
switch self {
case .unprepared: return .unprepared
case .syncing: return .syncing
case .enhancing: return .enhancing
case .fetching: return .fetching
case .synced: return .synced
case .stopped: return .stopped
case .disconnected: return .disconnected

View File

@ -1,5 +1,6 @@
import Combine
@testable import ZcashLightClientKit
import Foundation
{% macro methodName method%}{%if method|annotated:"mockedName" %}{{ method.annotations.mockedName }}{% else %}{% call swiftifyMethodName method.selectorName %}{% endif %}{% endmacro %}
{% macro swiftifyMethodName name %}{{ name | replace:"(","_" | replace:")","" | replace:":","_" | replace:"`","" | snakeToCamelCase | lowerFirstWord }}{% endmacro %}
@ -83,7 +84,7 @@ import Combine
{% if method.isStatic %}Self.{% endif %}{% call methodName method %}CallsCount += 1
{% call methodReceivedParameters method %}
{% if method.returnTypeName.isVoid %}
{% if method.throws %}try {% endif %}{% if method.isAsync %}await {% endif %}{% call methodClosureName method %}?({% call methodClosureCallParameters method %})
{% if method.throws %}try {% endif %}{% if method.isAsync %}await {% endif %}{% call methodClosureName method %}!({% call methodClosureCallParameters method %})
{% else %}
if let closure = {% if method.isStatic %}Self.{% endif %}{% call methodClosureName method %} {
return {% if method.throws %}try {% endif %}{% if method.isAsync %}await {% endif %}closure({% call methodClosureCallParameters method %})

View File

@ -12,7 +12,22 @@
@testable import ZcashLightClientKit
extension ZcashRustBackendWelding { }
extension BlockDownloader { }
extension BlockDownloaderService { }
extension BlockEnhancer { }
extension BlockScanner { }
extension BlockValidator { }
extension CompactBlockRepository { }
extension InternalSyncProgressStorage { }
extension LatestBlocksDataProvider { }
extension LightWalletdInfo { }
extension LightWalletService { }
extension Logger { }
extension SaplingParametersHandler { }
extension Synchronizer { }
extension TransactionRepository { }
extension UTXOFetcher { }
extension ZcashFileManager { }
extension ZcashRustBackendWelding { }
// sourcery:end:

View File

@ -209,7 +209,7 @@ extension SynchronizerState {
syncSessionID: .nullID,
shieldedBalance: WalletBalance(verified: Zatoshi(100), total: Zatoshi(200)),
transparentBalance: WalletBalance(verified: Zatoshi(200), total: Zatoshi(300)),
internalSyncStatus: .fetching(0),
internalSyncStatus: .syncing(0),
latestScannedHeight: 111111,
latestBlockHeight: 222222,
latestScannedTime: 12345678

View File

@ -213,29 +213,26 @@ extension TestCoordinator {
try await service.latestBlockHeight()
}
func reset(saplingActivation: BlockHeight, branchID: String, chainName: String) throws {
Task {
await self.synchronizer.blockProcessor.stop()
let config = await self.synchronizer.blockProcessor.config
func reset(saplingActivation: BlockHeight, branchID: String, chainName: String) async throws {
await self.synchronizer.blockProcessor.stop()
let newConfig = CompactBlockProcessor.Configuration(
alias: config.alias,
fsBlockCacheRoot: config.fsBlockCacheRoot,
dataDb: config.dataDb,
spendParamsURL: config.spendParamsURL,
outputParamsURL: config.outputParamsURL,
saplingParamsSourceURL: config.saplingParamsSourceURL,
downloadBatchSize: config.downloadBatchSize,
retries: config.retries,
maxBackoffInterval: config.maxBackoffInterval,
rewindDistance: config.rewindDistance,
walletBirthdayProvider: config.walletBirthdayProvider,
saplingActivation: saplingActivation,
network: config.network
)
let config = await self.synchronizer.blockProcessor.config
let newConfig = CompactBlockProcessor.Configuration(
alias: config.alias,
fsBlockCacheRoot: config.fsBlockCacheRoot,
dataDb: config.dataDb,
spendParamsURL: config.spendParamsURL,
outputParamsURL: config.outputParamsURL,
saplingParamsSourceURL: config.saplingParamsSourceURL,
retries: config.retries,
maxBackoffInterval: config.maxBackoffInterval,
rewindDistance: config.rewindDistance,
walletBirthdayProvider: config.walletBirthdayProvider,
saplingActivation: saplingActivation,
network: config.network
)
await self.synchronizer.blockProcessor.update(config: newConfig)
}
await self.synchronizer.blockProcessor.update(config: newConfig)
try service.reset(saplingActivation: saplingActivation, branchID: branchID, chainName: chainName)
}

View File

@ -0,0 +1,90 @@
@startuml
hide empty description
note as Lines
Green lines are happy paths.
Red lines are error paths.
Blue lines are stop paths.
In general any action for any state can produce error.
And the sync process can be stopped during any action.
end note
[*] -> idle
idle -[#green,bold]-> migrateLegacyCacheDB
migrateLegacyCacheDB : MigrateLegacyCacheDBAction
migrateLegacyCacheDB -[#green,bold]-> validateServer
migrateLegacyCacheDB -[#red]-> failed : Error occured.
migrateLegacyCacheDB -[#blue]-> stopped : Sync was stopped.
validateServer : ValidateServerAction
validateServer -[#green,bold]-> computeSyncRanges
validateServer -[#red]-> failed : Error occured.
validateServer -[#blue]-> stopped : Sync was stopped.
computeSyncRanges : ComputeSyncRangesAction
computeSyncRanges -[#green,bold]-> checksBeforeSync
computeSyncRanges -[#red]-> failed : Error occured.
computeSyncRanges -[#blue]-> stopped : Sync was stopped.
checksBeforeSync : ChecksBeforeSyncAction
checksBeforeSync -[#green,bold]-> fetchUTXO
checksBeforeSync -[#red]-> failed : Error occured.
checksBeforeSync -[#blue]-> stopped : Sync was stopped.
fetchUTXO : FetchUTXOAction
fetchUTXO -[#green,bold]-> handleSaplingParams
fetchUTXO -[#red]-> failed : Error occured.
fetchUTXO -[#blue]-> stopped : Sync was stopped.
handleSaplingParams : SaplingParamsAction
handleSaplingParams -[#green,bold]-> download
handleSaplingParams -[#red]-> failed : Error occured.
handleSaplingParams -[#blue]-> stopped : Sync was stopped.
download : DownloadAction
download -[#green,bold]-> validate
download -[#red]-> failed : Error occured.
download -[#blue]-> stopped : Sync was stopped.
validate : ValidateAction
validate -[#green,bold]-> scan
validate -[#red]-> failed : Error occured.
validate -[#blue]-> stopped : Sync was stopped.
scan : ScanAction
scan -[#green,bold]-> clearAlreadyScannedBlocks
scan -[#red]-> failed : Error occured.
scan -[#blue]-> stopped : Sync was stopped.
clearAlreadyScannedBlocks : ClearAlreadyScannedBlocksAction
clearAlreadyScannedBlocks -[#green,bold]-> enhance
clearAlreadyScannedBlocks -[#red]-> failed : Error occured.
clearAlreadyScannedBlocks -[#blue]-> stopped : Sync was stopped.
enhance : EnhanceAction
enhance -[#green,bold]-> download : Not all blocks in the\nsync range are downloaded\nand scanned yet.
enhance -[#green,bold]-> clearCache : All the blocks in\nthe sync range are downloaded\nand scanned.
enhance -[#red]-> failed : Error occured.
enhance -[#blue]-> stopped : Sync was stopped.
note right of enhance
Enhance transactions in batches of 1000
blocks. Dont't do it for each scan batch
which is usualy 100 blocks.
end note
clearCache : ClearCacheAction
clearCache --> finished
clearCache -[#red]-> failed : Error occured.
clearCache -[#blue]-> stopped : Sync was stopped.
finished --> [*]
failed --> [*]
stopped --> [*]
@enduml

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB