// // CompactBlockProcessorTests.swift // ZcashLightClientKitTests // // Created by Francisco Gindre on 20/09/2019. // Copyright © 2019 Electric Coin Company. All rights reserved. // import Combine import XCTest @testable import TestUtils @testable import ZcashLightClientKit class CompactBlockProcessorTests: ZcashTestCase { var processorConfig: CompactBlockProcessor.Configuration! var cancellables: [AnyCancellable] = [] var processorEventHandler: CompactBlockProcessorEventHandler! = CompactBlockProcessorEventHandler() var rustBackend: ZcashRustBackendWelding! var processor: CompactBlockProcessor! var syncStartedExpect: XCTestExpectation! var updatedNotificationExpectation: XCTestExpectation! var stopNotificationExpectation: XCTestExpectation! var finishedNotificationExpectation: XCTestExpectation! let network = ZcashNetworkBuilder.network(for: .testnet) let mockLatestHeight = ZcashNetworkBuilder.network(for: .testnet).constants.saplingActivationHeight + 2000 let testFileManager = FileManager() var testTempDirectory: URL! override func setUp() async throws { try await super.setUp() logger = OSLogger(logLevel: .debug) testTempDirectory = Environment.uniqueTestTempDirectory try? FileManager.default.removeItem(at: testTempDirectory) try self.testFileManager.createDirectory(at: testTempDirectory, withIntermediateDirectories: false) let pathProvider = DefaultResourceProvider(network: network) processorConfig = CompactBlockProcessor.Configuration( alias: .default, fsBlockCacheRoot: testTempDirectory, dataDb: pathProvider.dataDbURL, spendParamsURL: pathProvider.spendParamsURL, outputParamsURL: pathProvider.outputParamsURL, saplingParamsSourceURL: SaplingParamsSourceURL.tests, walletBirthdayProvider: { ZcashNetworkBuilder.network(for: .testnet).constants.saplingActivationHeight }, network: ZcashNetworkBuilder.network(for: .testnet) ) await InternalSyncProgress(alias: .default, storage: UserDefaults.standard, logger: logger).rewind(to: 0) let liveService = LightWalletServiceFactory(endpoint: LightWalletEndpointBuilder.eccTestnet).make() let service = MockLightWalletService( latestBlockHeight: mockLatestHeight, service: liveService ) rustBackend = ZcashRustBackend.makeForTests( dbData: processorConfig.dataDb, fsBlockDbRoot: processorConfig.fsBlockCacheRoot, networkType: network.networkType ) let branchID = try rustBackend.consensusBranchIdFor(height: Int32(mockLatestHeight)) service.mockLightDInfo = LightdInfo.with({ info in info.blockHeight = UInt64(mockLatestHeight) info.branch = "asdf" info.buildDate = "today" info.buildUser = "testUser" info.chainName = "test" info.consensusBranchID = branchID.toString() info.estimatedHeight = UInt64(mockLatestHeight) info.saplingActivationHeight = UInt64(network.constants.saplingActivationHeight) }) let transactionRepository = MockTransactionRepository( unminedCount: 0, receivedCount: 0, sentCount: 0, scannedHeight: 0, network: network ) Dependencies.setup( in: mockContainer, urls: Initializer.URLs( fsBlockDbRoot: testTempDirectory, dataDbURL: processorConfig.dataDb, spendParamsURL: processorConfig.spendParamsURL, outputParamsURL: processorConfig.outputParamsURL ), alias: .default, networkType: .testnet, endpoint: LightWalletEndpointBuilder.default, loggingPolicy: .default(.debug) ) mockContainer.mock(type: LatestBlocksDataProvider.self, isSingleton: true) { _ in LatestBlocksDataProviderImpl(service: service, transactionRepository: transactionRepository) } mockContainer.mock(type: ZcashRustBackendWelding.self, isSingleton: true) { _ in self.rustBackend } mockContainer.mock(type: LightWalletService.self, isSingleton: true) { _ in service } try await mockContainer.resolve(CompactBlockRepository.self).create() processor = CompactBlockProcessor(container: mockContainer, config: processorConfig) let dbInit = try await rustBackend.initDataDb(seed: nil) guard case .success = dbInit else { XCTFail("Failed to initDataDb. Expected `.success` got: \(dbInit)") return } syncStartedExpect = XCTestExpectation(description: "\(self.description) syncStartedExpect") stopNotificationExpectation = XCTestExpectation(description: "\(self.description) stopNotificationExpectation") updatedNotificationExpectation = XCTestExpectation(description: "\(self.description) updatedNotificationExpectation") finishedNotificationExpectation = XCTestExpectation(description: "\(self.description) finishedNotificationExpectation") let eventClosure: CompactBlockProcessor.EventClosure = { [weak self] event in switch event { case .failed: self?.processorFailed(event: event) default: break } } await self.processor.updateEventClosure(identifier: "tests", closure: eventClosure) } override func tearDown() async throws { try await super.tearDown() await processor.stop() try FileManager.default.removeItem(at: processorConfig.fsBlockCacheRoot) try? FileManager.default.removeItem(at: processorConfig.dataDb) cancellables = [] processor = nil processorEventHandler = nil rustBackend = nil testTempDirectory = nil } func processorFailed(event: CompactBlockProcessor.Event) { if case let .failed(error) = event { XCTFail("CompactBlockProcessor failed with Error: \(error)") } else { XCTFail("CompactBlockProcessor failed") } } private func startProcessing() async { XCTAssertNotNil(processor) let expectations: [CompactBlockProcessorEventHandler.EventIdentifier: XCTestExpectation] = [ .startedSyncing: syncStartedExpect, .stopped: stopNotificationExpectation, .progressUpdated: updatedNotificationExpectation, .finished: finishedNotificationExpectation ] await processorEventHandler.subscribe(to: processor, expectations: expectations) await processor.start() } func testStartNotifiesSuscriptors() async { await startProcessing() await fulfillment( of: [ syncStartedExpect, finishedNotificationExpectation ], timeout: 30, enforceOrder: false ) } func testProgressNotifications() async { let expectedUpdates = expectedBatches( currentHeight: processorConfig.walletBirthday, targetHeight: mockLatestHeight, batchSize: processorConfig.downloadBatchSize ) updatedNotificationExpectation.expectedFulfillmentCount = expectedUpdates await startProcessing() await fulfillment(of: [updatedNotificationExpectation, finishedNotificationExpectation], timeout: 300) } private func expectedBatches(currentHeight: BlockHeight, targetHeight: BlockHeight, batchSize: Int) -> Int { (abs(currentHeight - targetHeight) / batchSize) } func testNextBatchBlockRange() async { // test first range var latestDownloadedHeight = processorConfig.walletBirthday // this can be either this or Wallet Birthday. var latestBlockchainHeight = BlockHeight(network.constants.saplingActivationHeight + 1000) var expectedSyncRanges = SyncRanges( latestBlockHeight: latestBlockchainHeight, downloadedButUnscannedRange: 1...latestDownloadedHeight, downloadAndScanRange: latestDownloadedHeight...latestBlockchainHeight, enhanceRange: processorConfig.walletBirthday...latestBlockchainHeight, fetchUTXORange: processorConfig.walletBirthday...latestBlockchainHeight, latestScannedHeight: 0, latestDownloadedBlockHeight: latestDownloadedHeight ) var internalSyncProgress = InternalSyncProgress( alias: .default, storage: InternalSyncProgressMemoryStorage(), logger: logger ) await internalSyncProgress.migrateIfNeeded(latestDownloadedBlockHeightFromCacheDB: latestDownloadedHeight) var syncRanges = await internalSyncProgress.computeSyncRanges( birthday: processorConfig.walletBirthday, latestBlockHeight: latestBlockchainHeight, latestScannedHeight: 0 ) XCTAssertEqual( expectedSyncRanges, syncRanges, "Failure when testing first range" ) // Test mid-range latestDownloadedHeight = BlockHeight(network.constants.saplingActivationHeight + ZcashSDK.DefaultDownloadBatch) latestBlockchainHeight = BlockHeight(network.constants.saplingActivationHeight + 1000) expectedSyncRanges = SyncRanges( latestBlockHeight: latestBlockchainHeight, downloadedButUnscannedRange: 1...latestDownloadedHeight, downloadAndScanRange: latestDownloadedHeight + 1...latestBlockchainHeight, enhanceRange: processorConfig.walletBirthday...latestBlockchainHeight, fetchUTXORange: processorConfig.walletBirthday...latestBlockchainHeight, latestScannedHeight: 0, latestDownloadedBlockHeight: latestDownloadedHeight ) internalSyncProgress = InternalSyncProgress( alias: .default, storage: InternalSyncProgressMemoryStorage(), logger: logger ) await internalSyncProgress.migrateIfNeeded(latestDownloadedBlockHeightFromCacheDB: latestDownloadedHeight) syncRanges = await internalSyncProgress.computeSyncRanges( birthday: processorConfig.walletBirthday, latestBlockHeight: latestBlockchainHeight, latestScannedHeight: 0 ) XCTAssertEqual( expectedSyncRanges, syncRanges, "Failure when testing mid range" ) // Test last batch range latestDownloadedHeight = BlockHeight(network.constants.saplingActivationHeight + 950) latestBlockchainHeight = BlockHeight(network.constants.saplingActivationHeight + 1000) expectedSyncRanges = SyncRanges( latestBlockHeight: latestBlockchainHeight, downloadedButUnscannedRange: 1...latestDownloadedHeight, downloadAndScanRange: latestDownloadedHeight + 1...latestBlockchainHeight, enhanceRange: processorConfig.walletBirthday...latestBlockchainHeight, fetchUTXORange: processorConfig.walletBirthday...latestBlockchainHeight, latestScannedHeight: 0, latestDownloadedBlockHeight: latestDownloadedHeight ) internalSyncProgress = InternalSyncProgress( alias: .default, storage: InternalSyncProgressMemoryStorage(), logger: logger ) await internalSyncProgress.migrateIfNeeded(latestDownloadedBlockHeightFromCacheDB: latestDownloadedHeight) syncRanges = await internalSyncProgress.computeSyncRanges( birthday: processorConfig.walletBirthday, latestBlockHeight: latestBlockchainHeight, latestScannedHeight: 0 ) XCTAssertEqual( expectedSyncRanges, syncRanges, "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 let walletBirthday = 781_900 let result = await processor.determineLowerBound(errorHeight: errorHeight, consecutiveErrors: 1, walletBirthday: walletBirthday) let expected = 781_886 XCTAssertEqual(result, expected) } func testDetermineLowerBound() async { let errorHeight = 781_906 let walletBirthday = 780_900 let result = await processor.determineLowerBound(errorHeight: errorHeight, consecutiveErrors: 0, walletBirthday: walletBirthday) let expected = 781_896 XCTAssertEqual(result, expected) } }