react-native-zcash 0.6.10 → 0.6.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/android/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2460000.json +8 -0
  3. package/ios/ZCashLightClientKit/Block/CompactBlockProcessor.swift +937 -425
  4. package/ios/ZCashLightClientKit/Block/Download/BlockDownloader.swift +17 -31
  5. package/ios/ZCashLightClientKit/Block/Download/BlockDownloaderService.swift +2 -2
  6. package/ios/ZCashLightClientKit/Block/Enhance/BlockEnhancer.swift +15 -46
  7. package/ios/ZCashLightClientKit/Block/FetchUnspentTxOutputs/UTXOFetcher.swift +15 -4
  8. package/ios/ZCashLightClientKit/Block/FilesystemStorage/FSCompactBlockRepository.swift +4 -4
  9. package/ios/ZCashLightClientKit/Block/Scan/BlockScanner.swift +35 -10
  10. package/ios/ZCashLightClientKit/Block/Utils/InternalSyncProgress.swift +200 -0
  11. package/ios/ZCashLightClientKit/Block/Validate/BlockValidator.swift +51 -0
  12. package/ios/ZCashLightClientKit/ClosureSynchronizer.swift +2 -1
  13. package/ios/ZCashLightClientKit/CombineSynchronizer.swift +5 -2
  14. package/ios/ZCashLightClientKit/Constants/ZcashSDK.swift +26 -13
  15. package/ios/ZCashLightClientKit/DAO/BlockDao.swift +112 -0
  16. package/ios/ZCashLightClientKit/DAO/TransactionDao.swift +42 -40
  17. package/ios/ZCashLightClientKit/DAO/UnspentTransactionOutputDao.swift +4 -13
  18. package/ios/ZCashLightClientKit/Entity/AccountEntity.swift +0 -9
  19. package/ios/ZCashLightClientKit/Entity/BlockProgress.swift +24 -0
  20. package/ios/ZCashLightClientKit/Entity/TransactionEntity.swift +10 -7
  21. package/ios/ZCashLightClientKit/Error/Sourcery/generateErrorCode.sh +1 -1
  22. package/ios/ZCashLightClientKit/Error/ZcashError.swift +12 -121
  23. package/ios/ZCashLightClientKit/Error/ZcashErrorCode.swift +5 -43
  24. package/ios/ZCashLightClientKit/Error/ZcashErrorCodeDefinition.swift +6 -72
  25. package/ios/ZCashLightClientKit/Initializer.swift +26 -47
  26. package/ios/ZCashLightClientKit/Metrics/SDKMetrics.swift +12 -0
  27. package/ios/ZCashLightClientKit/Model/Checkpoint.swift +0 -12
  28. package/ios/ZCashLightClientKit/Modules/Service/GRPC/LightWalletGRPCService.swift +0 -15
  29. package/ios/ZCashLightClientKit/Modules/Service/GRPC/ProtoBuf/compact_formats.pb.swift +46 -150
  30. package/ios/ZCashLightClientKit/Modules/Service/GRPC/ProtoBuf/proto/compact_formats.proto +16 -30
  31. package/ios/ZCashLightClientKit/Modules/Service/GRPC/ProtoBuf/proto/service.proto +6 -32
  32. package/ios/ZCashLightClientKit/Modules/Service/GRPC/ProtoBuf/service.grpc.swift +22 -259
  33. package/ios/ZCashLightClientKit/Modules/Service/GRPC/ProtoBuf/service.pb.swift +7 -193
  34. package/ios/ZCashLightClientKit/Modules/Service/LightWalletService.swift +0 -8
  35. package/ios/ZCashLightClientKit/Providers/LatestBlocksDataProvider.swift +28 -18
  36. package/ios/ZCashLightClientKit/Repository/CompactBlockRepository.swift +1 -1
  37. package/ios/ZCashLightClientKit/Repository/TransactionRepository.swift +6 -2
  38. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2460000.json +8 -0
  39. package/ios/ZCashLightClientKit/Rust/ZcashRustBackend.swift +158 -293
  40. package/ios/ZCashLightClientKit/Rust/ZcashRustBackendWelding.swift +64 -58
  41. package/ios/ZCashLightClientKit/Rust/zcashlc.h +513 -619
  42. package/ios/ZCashLightClientKit/Synchronizer/ClosureSDKSynchronizer.swift +8 -2
  43. package/ios/ZCashLightClientKit/Synchronizer/CombineSDKSynchronizer.swift +15 -3
  44. package/ios/ZCashLightClientKit/Synchronizer/Dependencies.swift +30 -11
  45. package/ios/ZCashLightClientKit/Synchronizer/SDKSynchronizer.swift +50 -41
  46. package/ios/ZCashLightClientKit/Synchronizer.swift +65 -51
  47. package/ios/ZCashLightClientKit/Transaction/TransactionEncoder.swift +2 -2
  48. package/ios/ZCashLightClientKit/Transaction/WalletTransactionEncoder.swift +7 -7
  49. package/ios/ZCashLightClientKit/Utils/OSLogger.swift +3 -3
  50. package/ios/libzcashlc.xcframework/Info.plist +0 -4
  51. package/ios/libzcashlc.xcframework/ios-arm64/libzcashlc.a +0 -0
  52. package/ios/libzcashlc.xcframework/ios-arm64_x86_64-simulator/libzcashlc.a +0 -0
  53. package/package.json +1 -1
  54. package/ios/ZCashLightClientKit/Block/Actions/Action.swift +0 -98
  55. package/ios/ZCashLightClientKit/Block/Actions/ClearAlreadyScannedBlocksAction.swift +0 -35
  56. package/ios/ZCashLightClientKit/Block/Actions/ClearCacheAction.swift +0 -30
  57. package/ios/ZCashLightClientKit/Block/Actions/DownloadAction.swift +0 -67
  58. package/ios/ZCashLightClientKit/Block/Actions/EnhanceAction.swift +0 -97
  59. package/ios/ZCashLightClientKit/Block/Actions/FetchUTXOsAction.swift +0 -33
  60. package/ios/ZCashLightClientKit/Block/Actions/MigrateLegacyCacheDBAction.swift +0 -70
  61. package/ios/ZCashLightClientKit/Block/Actions/ProcessSuggestedScanRangesAction.swift +0 -60
  62. package/ios/ZCashLightClientKit/Block/Actions/RewindAction.swift +0 -48
  63. package/ios/ZCashLightClientKit/Block/Actions/SaplingParamsAction.swift +0 -33
  64. package/ios/ZCashLightClientKit/Block/Actions/ScanAction.swift +0 -95
  65. package/ios/ZCashLightClientKit/Block/Actions/UpdateChainTipAction.swift +0 -55
  66. package/ios/ZCashLightClientKit/Block/Actions/UpdateSubtreeRootsAction.swift +0 -58
  67. package/ios/ZCashLightClientKit/Block/Actions/ValidateServerAction.swift +0 -60
  68. package/ios/ZCashLightClientKit/Block/Utils/CompactBlockProgress.swift +0 -24
  69. package/ios/ZCashLightClientKit/Block/Utils/SyncControlData.swift +0 -25
  70. package/ios/ZCashLightClientKit/Extensions/Bool+ToData.swift +0 -15
  71. package/ios/ZCashLightClientKit/Extensions/Data+ToOtherTypes.swift +0 -18
  72. package/ios/ZCashLightClientKit/Extensions/Int+ToData.swift +0 -15
  73. package/ios/ZCashLightClientKit/Model/ScanProgress.swift +0 -29
  74. package/ios/ZCashLightClientKit/Model/ScanRange.swift +0 -31
  75. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2092500.json +0 -8
  76. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2095000.json +0 -8
  77. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2097500.json +0 -8
  78. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2102500.json +0 -8
  79. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2105000.json +0 -8
  80. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2107500.json +0 -8
  81. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2112500.json +0 -8
  82. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2115000.json +0 -8
  83. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2117500.json +0 -8
  84. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2122500.json +0 -8
  85. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2125000.json +0 -8
  86. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2127500.json +0 -8
  87. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2132500.json +0 -8
  88. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2135000.json +0 -8
  89. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2137500.json +0 -8
  90. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2142500.json +0 -8
  91. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2145000.json +0 -8
  92. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2147500.json +0 -8
  93. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2152500.json +0 -8
  94. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2155000.json +0 -8
  95. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2157500.json +0 -8
  96. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2162500.json +0 -8
  97. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2165000.json +0 -8
  98. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2167500.json +0 -8
  99. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2172500.json +0 -8
  100. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2175000.json +0 -8
  101. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2177500.json +0 -8
  102. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2182500.json +0 -8
  103. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2185000.json +0 -8
  104. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2187500.json +0 -8
  105. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2192500.json +0 -8
  106. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2195000.json +0 -8
  107. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2197500.json +0 -8
  108. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2202500.json +0 -8
  109. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2205000.json +0 -8
  110. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2207500.json +0 -8
  111. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2212500.json +0 -8
  112. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2215000.json +0 -8
  113. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2217500.json +0 -8
  114. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2222500.json +0 -8
  115. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2225000.json +0 -8
  116. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2227500.json +0 -8
  117. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2232500.json +0 -8
  118. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2235000.json +0 -8
  119. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2237500.json +0 -8
  120. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2242500.json +0 -8
  121. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2245000.json +0 -8
  122. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2247500.json +0 -8
  123. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2252500.json +0 -8
  124. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2255000.json +0 -8
  125. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2257500.json +0 -8
  126. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2262500.json +0 -8
  127. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2265000.json +0 -8
  128. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2267500.json +0 -8
  129. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2350000.json +0 -8
  130. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2360000.json +0 -8
  131. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2370000.json +0 -8
  132. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2380000.json +0 -8
  133. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2390000.json +0 -8
  134. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2400000.json +0 -8
  135. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2410000.json +0 -8
  136. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2420000.json +0 -8
  137. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2430000.json +0 -8
  138. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2440000.json +0 -8
  139. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2450000.json +0 -8
  140. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2460000.json +0 -8
  141. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2470000.json +0 -8
  142. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2480000.json +0 -8
  143. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2490000.json +0 -8
  144. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2500000.json +0 -8
  145. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2510000.json +0 -8
  146. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2520000.json +0 -8
  147. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2530000.json +0 -8
  148. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2540000.json +0 -8
  149. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2550000.json +0 -8
  150. package/ios/ZCashLightClientKit/Utils/ZcashFileManager.swift +0 -16
@@ -5,46 +5,144 @@
5
5
  // Created by Francisco Gindre on 18/09/2019.
6
6
  // Copyright © 2019 Electric Coin Company. All rights reserved.
7
7
  //
8
+ // swiftlint:disable file_length type_body_length
8
9
 
9
10
  import Foundation
10
11
  import Combine
11
12
 
12
13
  public typealias RefreshedUTXOs = (inserted: [UnspentTransactionOutputEntity], skipped: [UnspentTransactionOutputEntity])
13
14
 
15
+ public enum CompactBlockProgress {
16
+ case syncing(_ progress: BlockProgress)
17
+ case enhance(_ progress: EnhancementProgress)
18
+ case fetch(_ progress: Float)
19
+
20
+ public var progress: Float {
21
+ switch self {
22
+ case .syncing(let blockProgress):
23
+ return blockProgress.progress
24
+ case .enhance(let enhancementProgress):
25
+ return enhancementProgress.progress
26
+ case .fetch(let fetchingProgress):
27
+ return fetchingProgress
28
+ }
29
+ }
30
+
31
+ public var progressHeight: BlockHeight? {
32
+ switch self {
33
+ case .syncing(let blockProgress):
34
+ return blockProgress.progressHeight
35
+ case .enhance(let enhancementProgress):
36
+ return enhancementProgress.lastFoundTransaction?.minedHeight
37
+ default:
38
+ return 0
39
+ }
40
+ }
41
+
42
+ public var blockDate: Date? {
43
+ if case .enhance(let enhancementProgress) = self, let time = enhancementProgress.lastFoundTransaction?.blockTime {
44
+ return Date(timeIntervalSince1970: time)
45
+ }
46
+
47
+ return nil
48
+ }
49
+
50
+ public var targetHeight: BlockHeight? {
51
+ switch self {
52
+ case .syncing(let blockProgress):
53
+ return blockProgress.targetHeight
54
+ default:
55
+ return nil
56
+ }
57
+ }
58
+ }
59
+
60
+ public struct EnhancementProgress: Equatable {
61
+ /// total transactions that were detected in the `range`
62
+ public let totalTransactions: Int
63
+ /// enhanced transactions so far
64
+ public let enhancedTransactions: Int
65
+ /// last found transaction
66
+ public let lastFoundTransaction: ZcashTransaction.Overview?
67
+ /// block range that's being enhanced
68
+ public let range: CompactBlockRange
69
+ /// whether this transaction can be considered `newly mined` and not part of the
70
+ /// wallet catching up to stale and uneventful blocks.
71
+ public let newlyMined: Bool
72
+
73
+ public init(
74
+ totalTransactions: Int,
75
+ enhancedTransactions: Int,
76
+ lastFoundTransaction: ZcashTransaction.Overview?,
77
+ range: CompactBlockRange,
78
+ newlyMined: Bool
79
+ ) {
80
+ self.totalTransactions = totalTransactions
81
+ self.enhancedTransactions = enhancedTransactions
82
+ self.lastFoundTransaction = lastFoundTransaction
83
+ self.range = range
84
+ self.newlyMined = newlyMined
85
+ }
86
+
87
+ public var progress: Float {
88
+ totalTransactions > 0 ? Float(enhancedTransactions) / Float(totalTransactions) : 0
89
+ }
90
+
91
+ public static var zero: EnhancementProgress {
92
+ EnhancementProgress(totalTransactions: 0, enhancedTransactions: 0, lastFoundTransaction: nil, range: 0...0, newlyMined: false)
93
+ }
94
+
95
+ public static func == (lhs: EnhancementProgress, rhs: EnhancementProgress) -> Bool {
96
+ return
97
+ lhs.totalTransactions == rhs.totalTransactions &&
98
+ lhs.enhancedTransactions == rhs.enhancedTransactions &&
99
+ lhs.lastFoundTransaction?.id == rhs.lastFoundTransaction?.id &&
100
+ lhs.range == rhs.range
101
+ }
102
+ }
103
+
14
104
  /// The compact block processor is in charge of orchestrating the download and caching of compact blocks from a LightWalletEndpoint
15
105
  /// when started the processor downloads does a download - validate - scan cycle until it reaches latest height on the blockchain.
16
106
  actor CompactBlockProcessor {
17
- // It would be better to use Combine here but Combine doesn't work great with async. When this runs regularly only one closure is stored here
18
- // and that is one provided by `SDKSynchronizer`. But while running tests more "subscribers" is required here. Therefore it's required to handle
19
- // more closures here.
20
- private var eventClosures: [String: EventClosure] = [:]
107
+ typealias EventClosure = (Event) async -> Void
21
108
 
22
- private var syncTask: Task<Void, Error>?
109
+ enum Event {
110
+ /// Event sent when the CompactBlockProcessor presented an error.
111
+ case failed (Error)
23
112
 
24
- private let actions: [CBPState: Action]
25
- var context: ActionContext
113
+ /// Event sent when the CompactBlockProcessor has finished syncing the blockchain to latest height
114
+ case finished (_ lastScannedHeight: BlockHeight, _ foundBlocks: Bool)
26
115
 
27
- private(set) var config: Configuration
28
- private let configProvider: ConfigProvider
29
- private var afterSyncHooksManager = AfterSyncHooksManager()
116
+ /// Event sent when the CompactBlockProcessor found a newly mined transaction
117
+ case minedTransaction(ZcashTransaction.Overview)
30
118
 
31
- private let accountRepository: AccountRepository
32
- let blockDownloaderService: BlockDownloaderService
33
- private let latestBlocksDataProvider: LatestBlocksDataProvider
34
- private let logger: Logger
35
- private let metrics: SDKMetrics
36
- private let rustBackend: ZcashRustBackendWelding
37
- let service: LightWalletService
38
- let storage: CompactBlockRepository
39
- private let transactionRepository: TransactionRepository
40
- private let fileManager: ZcashFileManager
119
+ /// Event sent when the CompactBlockProcessor enhanced a bunch of transactions in some range.
120
+ case foundTransactions ([ZcashTransaction.Overview], CompactBlockRange)
121
+
122
+ /// Event sent when the CompactBlockProcessor handled a ReOrg.
123
+ /// `reorgHeight` is the height on which the reorg was detected.
124
+ /// `rewindHeight` is the height that the processor backed to in order to solve the Reorg.
125
+ case handledReorg (_ reorgHeight: BlockHeight, _ rewindHeight: BlockHeight)
126
+
127
+ /// Event sent when progress of the sync process changes.
128
+ case progressUpdated (CompactBlockProgress)
129
+
130
+ /// Event sent when the CompactBlockProcessor fetched utxos from lightwalletd attempted to store them.
131
+ case storedUTXOs ((inserted: [UnspentTransactionOutputEntity], skipped: [UnspentTransactionOutputEntity]))
132
+
133
+ /// Event sent when the CompactBlockProcessor starts enhancing of the transactions.
134
+ case startedEnhancing
135
+
136
+ /// Event sent when the CompactBlockProcessor starts fetching of the UTXOs.
137
+ case startedFetching
138
+
139
+ /// Event sent when the CompactBlockProcessor starts syncing.
140
+ case startedSyncing
141
+
142
+ /// Event sent when the CompactBlockProcessor stops syncing.
143
+ case stopped
144
+ }
41
145
 
42
- private var retryAttempts: Int = 0
43
- private var backoffTimer: Timer?
44
- private var consecutiveChainValidationErrors: Int = 0
45
-
46
- private var compactBlockProgress: CompactBlockProgress = .zero
47
-
48
146
  /// Compact Block Processor configuration
49
147
  ///
50
148
  /// - parameter fsBlockCacheRoot: absolute root path where the filesystem block cache will be stored.
@@ -58,8 +156,8 @@ actor CompactBlockProcessor {
58
156
  let dataDb: URL
59
157
  let spendParamsURL: URL
60
158
  let outputParamsURL: URL
61
- let enhanceBatchSize: Int
62
- let batchSize: Int
159
+ let downloadBatchSize: Int
160
+ let scanningBatchSize: Int
63
161
  let retries: Int
64
162
  let maxBackoffInterval: TimeInterval
65
163
  let maxReorgSize = ZcashSDK.maxReorgSize
@@ -73,7 +171,7 @@ actor CompactBlockProcessor {
73
171
  var blockPollInterval: TimeInterval {
74
172
  TimeInterval.random(in: ZcashSDK.defaultPollInterval / 2 ... ZcashSDK.defaultPollInterval * 1.5)
75
173
  }
76
-
174
+
77
175
  init(
78
176
  alias: ZcashSynchronizerAlias,
79
177
  cacheDbURL: URL? = nil,
@@ -82,11 +180,11 @@ actor CompactBlockProcessor {
82
180
  spendParamsURL: URL,
83
181
  outputParamsURL: URL,
84
182
  saplingParamsSourceURL: SaplingParamsSourceURL,
85
- enhanceBatchSize: Int = ZcashSDK.DefaultEnhanceBatch,
86
- batchSize: Int = ZcashSDK.DefaultBatchSize,
183
+ downloadBatchSize: Int = ZcashSDK.DefaultDownloadBatch,
87
184
  retries: Int = ZcashSDK.defaultRetries,
88
185
  maxBackoffInterval: TimeInterval = ZcashSDK.defaultMaxBackOffInterval,
89
186
  rewindDistance: Int = ZcashSDK.defaultRewindDistance,
187
+ scanningBatchSize: Int = ZcashSDK.DefaultScanningBatch,
90
188
  walletBirthdayProvider: @escaping () -> BlockHeight,
91
189
  saplingActivation: BlockHeight,
92
190
  network: ZcashNetwork
@@ -98,16 +196,17 @@ actor CompactBlockProcessor {
98
196
  self.outputParamsURL = outputParamsURL
99
197
  self.saplingParamsSourceURL = saplingParamsSourceURL
100
198
  self.network = network
101
- self.enhanceBatchSize = enhanceBatchSize
102
- self.batchSize = batchSize
199
+ self.downloadBatchSize = downloadBatchSize
103
200
  self.retries = retries
104
201
  self.maxBackoffInterval = maxBackoffInterval
105
202
  self.rewindDistance = rewindDistance
203
+ self.scanningBatchSize = scanningBatchSize
106
204
  self.walletBirthdayProvider = walletBirthdayProvider
107
205
  self.saplingActivation = saplingActivation
108
206
  self.cacheDbURL = cacheDbURL
207
+ assert(downloadBatchSize >= scanningBatchSize)
109
208
  }
110
-
209
+
111
210
  init(
112
211
  alias: ZcashSynchronizerAlias,
113
212
  fsBlockCacheRoot: URL,
@@ -115,11 +214,11 @@ actor CompactBlockProcessor {
115
214
  spendParamsURL: URL,
116
215
  outputParamsURL: URL,
117
216
  saplingParamsSourceURL: SaplingParamsSourceURL,
118
- enhanceBatchSize: Int = ZcashSDK.DefaultEnhanceBatch,
119
- batchSize: Int = ZcashSDK.DefaultBatchSize,
217
+ downloadBatchSize: Int = ZcashSDK.DefaultDownloadBatch,
120
218
  retries: Int = ZcashSDK.defaultRetries,
121
219
  maxBackoffInterval: TimeInterval = ZcashSDK.defaultMaxBackOffInterval,
122
220
  rewindDistance: Int = ZcashSDK.defaultRewindDistance,
221
+ scanningBatchSize: Int = ZcashSDK.DefaultScanningBatch,
123
222
  walletBirthdayProvider: @escaping () -> BlockHeight,
124
223
  network: ZcashNetwork
125
224
  ) {
@@ -133,21 +232,124 @@ actor CompactBlockProcessor {
133
232
  self.saplingActivation = network.constants.saplingActivationHeight
134
233
  self.network = network
135
234
  self.cacheDbURL = nil
136
- self.enhanceBatchSize = enhanceBatchSize
137
- self.batchSize = batchSize
235
+ self.downloadBatchSize = downloadBatchSize
138
236
  self.retries = retries
139
237
  self.maxBackoffInterval = maxBackoffInterval
140
238
  self.rewindDistance = rewindDistance
239
+ self.scanningBatchSize = scanningBatchSize
240
+
241
+ assert(downloadBatchSize >= scanningBatchSize)
141
242
  }
142
243
  }
143
244
 
245
+ /**
246
+ Represents the possible states of a CompactBlockProcessor
247
+ */
248
+ enum State {
249
+ /**
250
+ connected and downloading blocks
251
+ */
252
+ case syncing
253
+
254
+ /**
255
+ was doing something but was paused
256
+ */
257
+ case stopped
258
+
259
+ /**
260
+ Processor is Enhancing transactions
261
+ */
262
+ case enhancing
263
+
264
+ /**
265
+ fetching utxos
266
+ */
267
+ case fetching
268
+
269
+ /**
270
+ was processing but erred
271
+ */
272
+ case error(_ error: Error)
273
+
274
+ /// Download sapling param files if needed.
275
+ case handlingSaplingFiles
276
+
277
+ /**
278
+ Processor is up to date with the blockchain and you can now make transactions.
279
+ */
280
+ case synced
281
+ }
282
+
283
+ private var afterSyncHooksManager = AfterSyncHooksManager()
284
+
285
+ let metrics: SDKMetrics
286
+ let logger: Logger
287
+
288
+ /// Don't update this variable directly. Use `updateState()` method.
289
+ var state: State = .stopped
290
+
291
+ private(set) var config: Configuration
292
+
293
+ var maxAttemptsReached: Bool {
294
+ self.retryAttempts >= self.config.retries
295
+ }
296
+
297
+ var shouldStart: Bool {
298
+ switch self.state {
299
+ case .stopped, .synced, .error:
300
+ return !maxAttemptsReached
301
+ default:
302
+ return false
303
+ }
304
+ }
305
+
306
+ // It would be better to use Combine here but Combine doesn't work great with async. When this runs regularly only one closure is stored here
307
+ // and that is one provided by `SDKSynchronizer`. But while running tests more "subscribers" is required here. Therefore it's required to handle
308
+ // more closures here.
309
+ var eventClosures: [String: EventClosure] = [:]
310
+
311
+ let blockDownloaderService: BlockDownloaderService
312
+ let blockDownloader: BlockDownloader
313
+ let blockValidator: BlockValidator
314
+ let blockScanner: BlockScanner
315
+ let blockEnhancer: BlockEnhancer
316
+ let utxoFetcher: UTXOFetcher
317
+ let saplingParametersHandler: SaplingParametersHandler
318
+ private let latestBlocksDataProvider: LatestBlocksDataProvider
319
+
320
+ let service: LightWalletService
321
+ let storage: CompactBlockRepository
322
+ let transactionRepository: TransactionRepository
323
+ let accountRepository: AccountRepository
324
+ let rustBackend: ZcashRustBackendWelding
325
+ private var retryAttempts: Int = 0
326
+ private var backoffTimer: Timer?
327
+ private var lastChainValidationFailure: BlockHeight?
328
+ private var consecutiveChainValidationErrors: Int = 0
329
+ var processingError: Error?
330
+ private var foundBlocks = false
331
+ private var maxAttempts: Int {
332
+ config.retries
333
+ }
334
+
335
+ var batchSize: BlockHeight {
336
+ BlockHeight(self.config.downloadBatchSize)
337
+ }
338
+
339
+ private var cancelableTask: Task<Void, Error>?
340
+
341
+ private let internalSyncProgress: InternalSyncProgress
342
+
144
343
  /// Initializes a CompactBlockProcessor instance
145
344
  /// - Parameters:
146
345
  /// - service: concrete implementation of `LightWalletService` protocol
147
346
  /// - storage: concrete implementation of `CompactBlockRepository` protocol
148
347
  /// - backend: a class that complies to `ZcashRustBackendWelding`
149
348
  /// - config: `Configuration` struct for this processor
150
- init(container: DIContainer, config: Configuration) {
349
+ init(
350
+ container: DIContainer,
351
+ config: Configuration
352
+ ) {
151
353
  self.init(
152
354
  container: container,
153
355
  config: config,
@@ -174,8 +376,8 @@ actor CompactBlockProcessor {
174
376
  accountRepository: initializer.accountRepository
175
377
  )
176
378
  }
177
-
178
- init(
379
+
380
+ internal init(
179
381
  container: DIContainer,
180
382
  config: Configuration,
181
383
  accountRepository: AccountRepository
@@ -186,145 +388,172 @@ actor CompactBlockProcessor {
186
388
  accountRepository: accountRepository
187
389
  )
188
390
 
189
- let configProvider = ConfigProvider(config: config)
190
- context = ActionContextImpl(state: .idle)
191
- actions = Self.makeActions(container: container, configProvider: configProvider)
192
-
193
391
  self.metrics = container.resolve(SDKMetrics.self)
194
392
  self.logger = container.resolve(Logger.self)
195
393
  self.latestBlocksDataProvider = container.resolve(LatestBlocksDataProvider.self)
394
+ self.internalSyncProgress = container.resolve(InternalSyncProgress.self)
196
395
  self.blockDownloaderService = container.resolve(BlockDownloaderService.self)
396
+ self.blockDownloader = container.resolve(BlockDownloader.self)
397
+ self.blockValidator = container.resolve(BlockValidator.self)
398
+ self.blockScanner = container.resolve(BlockScanner.self)
399
+ self.blockEnhancer = container.resolve(BlockEnhancer.self)
400
+ self.utxoFetcher = container.resolve(UTXOFetcher.self)
401
+ self.saplingParametersHandler = container.resolve(SaplingParametersHandler.self)
197
402
  self.service = container.resolve(LightWalletService.self)
198
403
  self.rustBackend = container.resolve(ZcashRustBackendWelding.self)
199
404
  self.storage = container.resolve(CompactBlockRepository.self)
200
405
  self.config = config
201
406
  self.transactionRepository = container.resolve(TransactionRepository.self)
202
407
  self.accountRepository = accountRepository
203
- self.fileManager = container.resolve(ZcashFileManager.self)
204
- self.configProvider = configProvider
205
408
  }
206
-
409
+
207
410
  deinit {
208
- syncTask?.cancel()
209
- syncTask = nil
210
- }
211
-
212
- // swiftlint:disable:next cyclomatic_complexity
213
- private static func makeActions(container: DIContainer, configProvider: ConfigProvider) -> [CBPState: Action] {
214
- let actionsDefinition = CBPState.allCases.compactMap { state -> (CBPState, Action)? in
215
- let action: Action
216
- switch state {
217
- case .migrateLegacyCacheDB:
218
- action = MigrateLegacyCacheDBAction(container: container, configProvider: configProvider)
219
- case .validateServer:
220
- action = ValidateServerAction(container: container, configProvider: configProvider)
221
- case .updateSubtreeRoots:
222
- action = UpdateSubtreeRootsAction(container: container, configProvider: configProvider)
223
- case .updateChainTip:
224
- action = UpdateChainTipAction(container: container)
225
- case .processSuggestedScanRanges:
226
- action = ProcessSuggestedScanRangesAction(container: container)
227
- case .rewind:
228
- action = RewindAction(container: container)
229
- case .download:
230
- action = DownloadAction(container: container, configProvider: configProvider)
231
- case .scan:
232
- action = ScanAction(container: container, configProvider: configProvider)
233
- case .clearAlreadyScannedBlocks:
234
- action = ClearAlreadyScannedBlocksAction(container: container)
235
- case .enhance:
236
- action = EnhanceAction(container: container, configProvider: configProvider)
237
- case .fetchUTXO:
238
- action = FetchUTXOsAction(container: container)
239
- case .handleSaplingParams:
240
- action = SaplingParamsAction(container: container)
241
- case .clearCache:
242
- action = ClearCacheAction(container: container)
243
- case .finished, .failed, .stopped, .idle:
244
- return nil
245
- }
246
-
247
- return (state, action)
248
- }
249
-
250
- return Dictionary(uniqueKeysWithValues: actionsDefinition)
411
+ cancelableTask?.cancel()
251
412
  }
252
413
 
253
- // This is currently used only in tests. And it should be used only in tests.
254
414
  func update(config: Configuration) async {
255
415
  self.config = config
256
- await configProvider.update(config: config)
416
+ await stop()
257
417
  }
258
- }
259
418
 
260
- // MARK: - "Public" API
419
+ func updateState(_ newState: State) async -> Void {
420
+ let oldState = state
421
+ state = newState
422
+ await transitionState(from: oldState, to: newState)
423
+ }
261
424
 
262
- extension CompactBlockProcessor {
425
+ func updateEventClosure(identifier: String, closure: @escaping (Event) async -> Void) async {
426
+ eventClosures[identifier] = closure
427
+ }
428
+
429
+ func send(event: Event) async {
430
+ for item in eventClosures {
431
+ await item.value(event)
432
+ }
433
+ }
434
+
435
+ static func validateServerInfo(
436
+ _ info: LightWalletdInfo,
437
+ saplingActivation: BlockHeight,
438
+ localNetwork: ZcashNetwork,
439
+ rustBackend: ZcashRustBackendWelding
440
+ ) async throws {
441
+ // check network types
442
+ guard let remoteNetworkType = NetworkType.forChainName(info.chainName) else {
443
+ throw ZcashError.compactBlockProcessorChainName(info.chainName)
444
+ }
445
+
446
+ guard remoteNetworkType == localNetwork.networkType else {
447
+ throw ZcashError.compactBlockProcessorNetworkMismatch(localNetwork.networkType, remoteNetworkType)
448
+ }
449
+
450
+ guard saplingActivation == info.saplingActivationHeight else {
451
+ throw ZcashError.compactBlockProcessorSaplingActivationMismatch(saplingActivation, BlockHeight(info.saplingActivationHeight))
452
+ }
453
+
454
+ // check branch id
455
+ let localBranch = try rustBackend.consensusBranchIdFor(height: Int32(info.blockHeight))
456
+
457
+ guard let remoteBranchID = ConsensusBranchID.fromString(info.consensusBranchID) else {
458
+ throw ZcashError.compactBlockProcessorConsensusBranchID
459
+ }
460
+
461
+ guard remoteBranchID == localBranch else {
462
+ throw ZcashError.compactBlockProcessorWrongConsensusBranchId(localBranch, remoteBranchID)
463
+ }
464
+ }
465
+
466
+ /// Starts the CompactBlockProcessor instance and starts downloading and processing blocks
467
+ ///
468
+ /// triggers the blockProcessorStartedDownloading notification
469
+ ///
470
+ /// - Important: subscribe to the notifications before calling this method
263
471
  func start(retry: Bool = false) async {
264
472
  if retry {
265
473
  self.retryAttempts = 0
474
+ self.processingError = nil
266
475
  self.backoffTimer?.invalidate()
267
476
  self.backoffTimer = nil
268
477
  }
269
478
 
270
- guard await canStartSync() else {
271
- if await isIdle() {
272
- logger.warn("max retry attempts reached on \(await context.state) state")
273
- await send(event: .failed(ZcashError.compactBlockProcessorMaxAttemptsReached(config.retries)))
274
- } else {
479
+ guard shouldStart else {
480
+ switch self.state {
481
+ case .error(let error):
482
+ // max attempts have been reached
483
+ logger.info("max retry attempts reached with error: \(error)")
484
+ await notifyError(ZcashError.compactBlockProcessorMaxAttemptsReached(self.maxAttempts))
485
+ await updateState(.stopped)
486
+ case .stopped:
487
+ // max attempts have been reached
488
+ logger.info("max retry attempts reached")
489
+ await notifyError(ZcashError.compactBlockProcessorMaxAttemptsReached(self.maxAttempts))
490
+ case .synced:
491
+ // max attempts have been reached
492
+ logger.warn("max retry attempts reached on synced state, this indicates malfunction")
493
+ await notifyError(ZcashError.compactBlockProcessorMaxAttemptsReached(self.maxAttempts))
494
+ case .syncing, .enhancing, .fetching, .handlingSaplingFiles:
275
495
  logger.debug("Warning: compact block processor was started while busy!!!!")
276
496
  afterSyncHooksManager.insert(hook: .anotherSync)
277
497
  }
278
498
  return
279
499
  }
280
500
 
281
- syncTask = Task(priority: .userInitiated) {
282
- await run()
501
+ do {
502
+ if let legacyCacheDbURL = self.config.cacheDbURL {
503
+ try await self.migrateCacheDb(legacyCacheDbURL)
504
+ }
505
+ } catch {
506
+ await self.fail(error)
283
507
  }
508
+
509
+ await self.nextBatch()
284
510
  }
285
511
 
512
+ /**
513
+ Stops the CompactBlockProcessor
514
+
515
+ Note: retry count is reset
516
+ */
286
517
  func stop() async {
287
- syncTask?.cancel()
288
518
  self.backoffTimer?.invalidate()
289
519
  self.backoffTimer = nil
290
- await stopAllActions()
291
- retryAttempts = 0
292
- }
293
520
 
294
- func latestHeight() async throws -> BlockHeight {
295
- try await blockDownloaderService.latestBlockHeight()
521
+ cancelableTask?.cancel()
522
+ await blockDownloader.stopDownload()
523
+
524
+ self.retryAttempts = 0
296
525
  }
297
- }
298
526
 
299
- // MARK: - Rewind
527
+ // MARK: Rewind
300
528
 
301
- extension CompactBlockProcessor {
302
529
  /// Rewinds to provided height.
303
530
  /// - Parameter height: height to rewind to. If nil is provided, it will rescan to nearest height (quick rescan)
304
531
  ///
305
532
  /// - Note: If this is called while sync is in progress then the sync process is stopped first and then rewind is executed.
306
- func rewind(context: AfterSyncHooksManager.RewindContext) async throws {
533
+ func rewind(context: AfterSyncHooksManager.RewindContext) async {
307
534
  logger.debug("Starting rewind")
308
- if await isIdle() {
309
- logger.debug("Sync doesn't run. Executing rewind.")
310
- try await doRewind(context: context)
311
- } else {
535
+ switch self.state {
536
+ case .syncing, .enhancing, .fetching, .handlingSaplingFiles:
312
537
  logger.debug("Stopping sync because of rewind")
313
538
  afterSyncHooksManager.insert(hook: .rewind(context))
314
539
  await stop()
540
+
541
+ case .stopped, .error, .synced:
542
+ logger.debug("Sync doesn't run. Executing rewind.")
543
+ await doRewind(context: context)
315
544
  }
316
545
  }
317
546
 
318
- private func doRewind(context: AfterSyncHooksManager.RewindContext) async throws {
547
+ private func doRewind(context: AfterSyncHooksManager.RewindContext) async {
319
548
  logger.debug("Executing rewind.")
320
- let lastDownloaded = await latestBlocksDataProvider.maxScannedHeight
549
+ let lastDownloaded = await internalSyncProgress.latestDownloadedBlockHeight
321
550
  let height = Int32(context.height ?? lastDownloaded)
322
551
 
323
552
  let nearestHeight: Int32
324
553
  do {
325
554
  nearestHeight = try await rustBackend.getNearestRewindHeight(height: height)
326
555
  } catch {
327
- await failure(error)
556
+ await fail(error)
328
557
  return await context.completion(.failure(error))
329
558
  }
330
559
 
@@ -332,10 +561,9 @@ extension CompactBlockProcessor {
332
561
  let rewindHeight = max(Int32(nearestHeight - 1), Int32(config.walletBirthday))
333
562
 
334
563
  do {
335
- try await rewindDownloadBlockAction(to: BlockHeight(rewindHeight))
336
564
  try await rustBackend.rewindToHeight(height: rewindHeight)
337
565
  } catch {
338
- await failure(error)
566
+ await fail(error)
339
567
  return await context.completion(.failure(error))
340
568
  }
341
569
 
@@ -346,46 +574,38 @@ extension CompactBlockProcessor {
346
574
  } catch {
347
575
  return await context.completion(.failure(error))
348
576
  }
349
-
350
- await resetContext(restoreLastEnhancedHeight: false)
351
-
352
- await context.completion(.success(rewindBlockHeight))
353
- }
354
- }
355
577
 
356
- // MARK: - Actions
578
+ await internalSyncProgress.rewind(to: rewindBlockHeight)
357
579
 
358
- private extension CompactBlockProcessor {
359
- func rewindDownloadBlockAction(to rewindHeight: BlockHeight?) async throws {
360
- if let downloadAction = actions[.download] as? DownloadAction {
361
- await downloadAction.downloader.rewind(latestDownloadedBlockHeight: rewindHeight)
362
- } else {
363
- throw ZcashError.compactBlockProcessorDownloadBlockActionRewind
364
- }
580
+ self.lastChainValidationFailure = nil
581
+ await context.completion(.success(rewindBlockHeight))
365
582
  }
366
- }
367
583
 
368
- // MARK: - Wipe
584
+ // MARK: Wipe
369
585
 
370
- extension CompactBlockProcessor {
371
- func wipe(context: AfterSyncHooksManager.WipeContext) async throws {
586
+ func wipe(context: AfterSyncHooksManager.WipeContext) async {
372
587
  logger.debug("Starting wipe")
373
- if await isIdle() {
374
- logger.debug("Sync doesn't run. Executing wipe.")
375
- try await doWipe(context: context)
376
- } else {
588
+ switch self.state {
589
+ case .syncing, .enhancing, .fetching, .handlingSaplingFiles:
377
590
  logger.debug("Stopping sync because of wipe")
378
591
  afterSyncHooksManager.insert(hook: .wipe(context))
379
592
  await stop()
593
+
594
+ case .stopped, .error, .synced:
595
+ logger.debug("Sync doesn't run. Executing wipe.")
596
+ await doWipe(context: context)
380
597
  }
381
598
  }
382
599
 
383
- private func doWipe(context: AfterSyncHooksManager.WipeContext) async throws {
600
+ private func doWipe(context: AfterSyncHooksManager.WipeContext) async {
384
601
  logger.debug("Executing wipe.")
385
602
  context.prewipe()
386
603
 
604
+ await updateState(.stopped)
605
+
387
606
  do {
388
607
  try await self.storage.clear()
608
+ await internalSyncProgress.rewind(to: 0)
389
609
 
390
610
  wipeLegacyCacheDbIfNeeded()
391
611
 
@@ -394,308 +614,437 @@ extension CompactBlockProcessor {
394
614
  try fileManager.removeItem(at: config.dataDb)
395
615
  }
396
616
 
397
- try await rewindDownloadBlockAction(to: nil)
398
-
399
617
  await context.completion(nil)
400
618
  } catch {
401
619
  await context.completion(error)
402
620
  }
403
621
  }
404
622
 
405
- private func wipeLegacyCacheDbIfNeeded() {
406
- guard let cacheDbURL = config.cacheDbURL else { return }
407
- guard fileManager.isDeletableFile(atPath: cacheDbURL.pathExtension) else { return }
408
- try? fileManager.removeItem(at: cacheDbURL)
409
- }
410
- }
411
-
412
- // MARK: - Events
413
-
414
- extension CompactBlockProcessor {
415
- typealias EventClosure = (Event) async -> Void
416
-
417
- enum Event {
418
- /// Event sent when the CompactBlockProcessor presented an error.
419
- case failed(Error)
420
-
421
- /// Event sent when the CompactBlockProcessor has finished syncing the blockchain to latest height
422
- case finished(_ lastScannedHeight: BlockHeight)
423
-
424
- /// Event sent when the CompactBlockProcessor found a newly mined transaction
425
- case minedTransaction(ZcashTransaction.Overview)
623
+ // MARK: Sync
426
624
 
427
- /// Event sent when the CompactBlockProcessor enhanced a bunch of transactions in some range.
428
- case foundTransactions([ZcashTransaction.Overview], CompactBlockRange)
625
+ func validateServer() async {
626
+ do {
627
+ let info = try await self.service.getInfo()
628
+ try await Self.validateServerInfo(
629
+ info,
630
+ saplingActivation: self.config.saplingActivation,
631
+ localNetwork: self.config.network,
632
+ rustBackend: self.rustBackend
633
+ )
634
+ } catch {
635
+ await self.severeFailure(error)
636
+ }
637
+ }
638
+
639
+ /// Processes new blocks on the given range based on the configuration set for this instance
640
+ func processNewBlocks(ranges: SyncRanges) async {
641
+ self.foundBlocks = true
642
+
643
+ cancelableTask = Task(priority: .userInitiated) {
644
+ do {
645
+ let totalProgressRange = computeTotalProgressRange(from: ranges)
646
+
647
+ logger.debug("""
648
+ Syncing with ranges:
649
+ downloaded but not scanned: \
650
+ \(ranges.downloadedButUnscannedRange?.lowerBound ?? -1)...\(ranges.downloadedButUnscannedRange?.upperBound ?? -1)
651
+ download and scan: \(ranges.downloadAndScanRange?.lowerBound ?? -1)...\(ranges.downloadAndScanRange?.upperBound ?? -1)
652
+ enhance range: \(ranges.enhanceRange?.lowerBound ?? -1)...\(ranges.enhanceRange?.upperBound ?? -1)
653
+ fetchUTXO range: \(ranges.fetchUTXORange?.lowerBound ?? -1)...\(ranges.fetchUTXORange?.upperBound ?? -1)
654
+ total progress range: \(totalProgressRange.lowerBound)...\(totalProgressRange.upperBound)
655
+ """)
656
+
657
+ var anyActionExecuted = false
658
+
659
+ // clear any present cached state if needed.
660
+ // this checks if there was a sync in progress that was
661
+ // interrupted abruptly and cache was not able to be cleared
662
+ // properly and internal state set to the appropriate value
663
+ if let newLatestDownloadedHeight = ranges.shouldClearBlockCacheAndUpdateInternalState() {
664
+ try await storage.clear()
665
+ await internalSyncProgress.set(newLatestDownloadedHeight, .latestDownloadedBlockHeight)
666
+ } else {
667
+ try await storage.create()
668
+ }
429
669
 
430
- /// Event sent when the CompactBlockProcessor handled a ReOrg.
431
- /// `reorgHeight` is the height on which the reorg was detected.
432
- /// `rewindHeight` is the height that the processor backed to in order to solve the Reorg.
433
- case handledReorg(_ reorgHeight: BlockHeight, _ rewindHeight: BlockHeight)
670
+ if let range = ranges.downloadedButUnscannedRange {
671
+ logger.debug("Starting scan with downloaded but not scanned blocks with range: \(range.lowerBound)...\(range.upperBound)")
672
+ try await blockScanner.scanBlocks(at: range, totalProgressRange: totalProgressRange) { [weak self] lastScannedHeight in
673
+ let progress = BlockProgress(
674
+ startHeight: totalProgressRange.lowerBound,
675
+ targetHeight: totalProgressRange.upperBound,
676
+ progressHeight: lastScannedHeight
677
+ )
678
+ await self?.notifyProgress(.syncing(progress))
679
+ }
680
+ }
434
681
 
435
- /// Event sent when progress of some specific action happened.
436
- case syncProgress(Float)
682
+ if let range = ranges.downloadAndScanRange {
683
+ logger.debug("Starting sync with range: \(range.lowerBound)...\(range.upperBound)")
684
+ try await blockDownloader.setSyncRange(range, batchSize: batchSize)
685
+ try await downloadAndScanBlocks(at: range, totalProgressRange: totalProgressRange)
686
+ // Side effect of calling stop is to delete last used download stream. To be sure that it doesn't keep any data in memory.
687
+ await blockDownloader.stopDownload()
688
+ }
437
689
 
438
- /// Event sent when progress of the sync process changes.
439
- case progressUpdated(Float)
690
+ if let range = ranges.enhanceRange {
691
+ anyActionExecuted = true
692
+ logger.debug("Enhancing with range: \(range.lowerBound)...\(range.upperBound)")
693
+ await updateState(.enhancing)
694
+ if let transactions = try await blockEnhancer.enhance(
695
+ at: range,
696
+ didEnhance: { [weak self] progress in
697
+ await self?.notifyProgress(.enhance(progress))
698
+ if
699
+ let foundTx = progress.lastFoundTransaction,
700
+ progress.newlyMined {
701
+ await self?.notifyMinedTransaction(foundTx)
702
+ }
703
+ }
704
+ ) {
705
+ await notifyTransactions(transactions, in: range)
706
+ }
707
+ }
440
708
 
441
- /// Event sent when the CompactBlockProcessor fetched utxos from lightwalletd attempted to store them.
442
- case storedUTXOs((inserted: [UnspentTransactionOutputEntity], skipped: [UnspentTransactionOutputEntity]))
709
+ if let range = ranges.fetchUTXORange {
710
+ anyActionExecuted = true
711
+ logger.debug("Fetching UTXO with range: \(range.lowerBound)...\(range.upperBound)")
712
+ await updateState(.fetching)
713
+ let result = try await utxoFetcher.fetch(at: range) { [weak self] progress in
714
+ await self?.notifyProgress(.fetch(progress))
715
+ }
716
+ await send(event: .storedUTXOs(result))
717
+ }
443
718
 
444
- /// Event sent when the CompactBlockProcessor starts enhancing of the transactions.
445
- case startedEnhancing
719
+ logger.debug("Fetching sapling parameters")
720
+ await updateState(.handlingSaplingFiles)
721
+ try await saplingParametersHandler.handleIfNeeded()
446
722
 
447
- /// Event sent when the CompactBlockProcessor starts fetching of the UTXOs.
448
- case startedFetching
723
+ logger.debug("Clearing cache")
724
+ try await clearCompactBlockCache()
449
725
 
450
- /// Event sent when the CompactBlockProcessor starts syncing.
451
- case startedSyncing
726
+ if !Task.isCancelled {
727
+ let newBlocksMined = await ranges.latestBlockHeight < latestBlocksDataProvider.latestBlockHeight
728
+ await processBatchFinished(height: (anyActionExecuted && !newBlocksMined) ? ranges.latestBlockHeight : nil)
729
+ }
730
+ } catch {
731
+ // Side effect of calling stop is to delete last used download stream. To be sure that it doesn't keep any data in memory.
732
+ await blockDownloader.stopDownload()
733
+ logger.error("Sync failed with error: \(error)")
452
734
 
453
- /// Event sent when the CompactBlockProcessor stops syncing.
454
- case stopped
735
+ if Task.isCancelled {
736
+ logger.info("Processing cancelled.")
737
+ await updateState(.stopped)
738
+ await handleAfterSyncHooks()
739
+ } else {
740
+ if case let ZcashError.rustValidateCombinedChainInvalidChain(height) = error {
741
+ await validationFailed(at: BlockHeight(height))
742
+ } else {
743
+ logger.error("processing failed with error: \(error)")
744
+ await fail(error)
745
+ }
746
+ }
747
+ }
748
+ }
455
749
  }
456
750
 
457
- func updateEventClosure(identifier: String, closure: @escaping (Event) async -> Void) async {
458
- eventClosures[identifier] = closure
459
- }
751
+ private func handleAfterSyncHooks() async {
752
+ let afterSyncHooksManager = self.afterSyncHooksManager
753
+ self.afterSyncHooksManager = AfterSyncHooksManager()
460
754
 
461
- private func send(event: Event) async {
462
- for item in eventClosures {
463
- await item.value(event)
755
+ if let wipeContext = afterSyncHooksManager.shouldExecuteWipeHook() {
756
+ await doWipe(context: wipeContext)
757
+ } else if let rewindContext = afterSyncHooksManager.shouldExecuteRewindHook() {
758
+ await doRewind(context: rewindContext)
759
+ } else if afterSyncHooksManager.shouldExecuteAnotherSyncHook() {
760
+ logger.debug("Starting new sync.")
761
+ await nextBatch()
464
762
  }
465
763
  }
466
- }
467
764
 
468
- // MARK: - Main loop
765
+ private func downloadAndScanBlocks(at range: CompactBlockRange, totalProgressRange: CompactBlockRange) async throws {
766
+ // Divide `range` by `batchSize` and compute how many time do we need to run to download and scan all the blocks.
767
+ // +1 must be done here becase `range` is closed range. So even if upperBound and lowerBound are same there is one block to sync.
768
+ let blocksCountToSync = (range.upperBound - range.lowerBound) + 1
769
+ var loopsCount = blocksCountToSync / batchSize
770
+ if blocksCountToSync % batchSize != 0 {
771
+ loopsCount += 1
772
+ }
469
773
 
470
- extension CompactBlockProcessor {
471
- // This is main loop of the sync process. It simply takes state and try to find action which handles it. If action is found it executes the
472
- // action. If action is not found then loop finishes. Thanks to this it's super easy to identify start point of sync process and end points
473
- // of sync process without any side effects.
474
- //
475
- // Check `docs/cbp_state_machine.puml` file and `docs/images/cbp_state_machine.png` file to see all the state tree. Also when you update state
476
- // tree in the code update this documentation. Image is generated by plantuml tool.
477
- //
478
- // swiftlint:disable:next cyclomatic_complexity
479
- private func run() async {
480
- logger.debug("Starting run")
481
- await resetContext()
482
-
483
- while true {
484
- // Sync is starting when the state is `idle`.
485
- if await context.state == .idle {
486
- // Side effect of calling stop is to delete last used download stream. To be sure that it doesn't keep any data in memory.
487
- await stopAllActions()
488
- // Update state to the first state in state machine that can be handled by action.
489
- await context.update(state: .migrateLegacyCacheDB)
490
- await syncStarted()
774
+ var lastScannedHeight: BlockHeight = .zero
775
+ for i in 0..<loopsCount {
776
+ let processingRange = computeSingleLoopDownloadRange(fullRange: range, loopCounter: i, batchSize: batchSize)
491
777
 
492
- if backoffTimer == nil {
493
- await setTimer()
494
- }
495
- }
778
+ logger.debug("Sync loop #\(i + 1) range: \(processingRange.lowerBound)...\(processingRange.upperBound)")
496
779
 
497
- let state = await context.state
498
- logger.debug("Handling state: \(state)")
780
+ // This is important. We must be sure that no new download is executed when this Task is canceled. Without this line `stop()` doesn't
781
+ // work.
782
+ try Task.checkCancellation()
499
783
 
500
- // Try to find action for state.
501
- guard let action = actions[state] else {
502
- // Side effect of calling stop is to delete last used download stream. To be sure that it doesn't keep any data in memory.
503
- await stopAllActions()
504
- if await syncFinished() {
505
- await resetContext()
506
- continue
507
- } else {
508
- break
509
- }
784
+ do {
785
+ await blockDownloader.setDownloadLimit(processingRange.upperBound + (2 * batchSize))
786
+ await blockDownloader.startDownload(maxBlockBufferSize: config.downloadBufferSize)
787
+
788
+ try await blockDownloader.waitUntilRequestedBlocksAreDownloaded(in: processingRange)
789
+ } catch {
790
+ await ifTaskIsNotCanceledClearCompactBlockCache(lastScannedHeight: lastScannedHeight)
791
+ throw error
510
792
  }
511
793
 
512
794
  do {
513
- try Task.checkCancellation()
514
-
515
- // Execute action.
516
- context = try await action.run(with: context) { [weak self] event in
517
- await self?.send(event: event)
518
- if let progressChanged = await self?.compactBlockProgress.hasProgressUpdated(event), progressChanged {
519
- if let progress = await self?.compactBlockProgress.progress {
520
- await self?.send(event: .progressUpdated(progress))
521
- }
522
- }
523
- }
524
-
525
- await didFinishAction()
795
+ try await blockValidator.validate()
526
796
  } catch {
527
- // Side effect of calling stop is to delete last used download stream. To be sure that it doesn't keep any data in memory.
528
- await stopAllActions()
529
- logger.error("Sync failed with error: \(error)")
797
+ await ifTaskIsNotCanceledClearCompactBlockCache(lastScannedHeight: lastScannedHeight)
798
+ logger.error("Block validation failed with error: \(error)")
799
+ throw error
800
+ }
530
801
 
531
- if Task.isCancelled {
532
- logger.info("Processing cancelled.")
533
- do {
534
- if try await syncTaskWasCancelled() {
535
- // Start sync all over again
536
- await resetContext()
537
- } else {
538
- // end the sync loop
539
- break
540
- }
541
- } catch {
542
- await failure(error)
543
- break
544
- }
545
- } else {
546
- if await handleSyncFailure(action: action, error: error) {
547
- // Start sync all over again
548
- await resetContext()
549
- } else {
550
- // end the sync loop
551
- break
552
- }
802
+ // Without this `stop()` would work. But this line improves support for Task cancelation.
803
+ try Task.checkCancellation()
804
+
805
+ do {
806
+ lastScannedHeight = try await blockScanner.scanBlocks(
807
+ at: processingRange,
808
+ totalProgressRange: totalProgressRange
809
+ ) { [weak self] lastScannedHeight in
810
+ let progress = BlockProgress(
811
+ startHeight: totalProgressRange.lowerBound,
812
+ targetHeight: totalProgressRange.upperBound,
813
+ progressHeight: lastScannedHeight
814
+ )
815
+ await self?.notifyProgress(.syncing(progress))
553
816
  }
817
+ } catch {
818
+ logger.error("Scanning failed with error: \(error)")
819
+ await ifTaskIsNotCanceledClearCompactBlockCache(lastScannedHeight: lastScannedHeight)
820
+ throw error
554
821
  }
555
- }
556
822
 
557
- logger.debug("Run ended")
558
- syncTask = nil
823
+ try await clearCompactBlockCache(upTo: lastScannedHeight)
824
+
825
+ let progress = BlockProgress(
826
+ startHeight: totalProgressRange.lowerBound,
827
+ targetHeight: totalProgressRange.upperBound,
828
+ progressHeight: processingRange.upperBound
829
+ )
830
+ await notifyProgress(.syncing(progress))
831
+ }
559
832
  }
560
833
 
561
- private func syncTaskWasCancelled() async throws -> Bool {
562
- logger.info("Sync cancelled.")
563
- await context.update(state: .stopped)
564
- await send(event: .stopped)
565
- return try await handleAfterSyncHooks()
834
+ /*
835
+ Here range for one batch is computed. For example if we want to sync blocks 0...1000 with batchSize 100 we want to generage blocks like
836
+ this:
837
+ 0...99
838
+ 100...199
839
+ 200...299
840
+ 300...399
841
+ ...
842
+ 900...999
843
+ 1000...1000
844
+ */
845
+ func computeSingleLoopDownloadRange(fullRange: CompactBlockRange, loopCounter: Int, batchSize: BlockHeight) -> CompactBlockRange {
846
+ let lowerBound = fullRange.lowerBound + (loopCounter * batchSize)
847
+ let upperBound = min(fullRange.lowerBound + ((loopCounter + 1) * batchSize) - 1, fullRange.upperBound)
848
+ return lowerBound...upperBound
566
849
  }
567
850
 
568
- private func handleSyncFailure(action: Action, error: Error) async -> Bool {
569
- if action.removeBlocksCacheWhenFailed {
570
- await ifTaskIsNotCanceledClearCompactBlockCache()
851
+ /// It may happen that sync process start with syncing blocks that were downloaded but not synced in previous run of the sync process. This
852
+ /// methods analyses what must be done and computes range that should be used to compute reported progress.
853
+ private func computeTotalProgressRange(from syncRanges: SyncRanges) -> CompactBlockRange {
854
+ guard syncRanges.downloadedButUnscannedRange != nil || syncRanges.downloadAndScanRange != nil else {
855
+ // 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
856
+ // this "fake" range than to handle nil.
857
+ return 0...0
571
858
  }
572
859
 
573
- logger.error("Sync failed with error: \(error)")
574
- await failure(error)
860
+ // Thanks to guard above we can be sure that one of these two ranges is not nil.
861
+ let lowerBound = syncRanges.downloadedButUnscannedRange?.lowerBound ?? syncRanges.downloadAndScanRange?.lowerBound ?? 0
862
+ let upperBound = syncRanges.downloadAndScanRange?.upperBound ?? syncRanges.downloadedButUnscannedRange?.upperBound ?? 0
575
863
 
576
- return false
864
+ return lowerBound...upperBound
577
865
  }
578
866
 
579
- // swiftlint:disable:next cyclomatic_complexity
580
- private func didFinishAction() async {
581
- // This is evalution of the state setup by previous action.
582
- switch await context.state {
583
- case .idle:
584
- break
585
- case .migrateLegacyCacheDB:
586
- break
587
- case .validateServer:
588
- break
589
- case .updateSubtreeRoots:
590
- break
591
- case .updateChainTip:
592
- break
593
- case .processSuggestedScanRanges:
594
- break
595
- case .rewind:
596
- break
597
- case .download:
598
- break
599
- case .scan:
600
- break
601
- case .clearAlreadyScannedBlocks:
602
- break
603
- case .enhance:
604
- await send(event: .startedEnhancing)
605
- case .fetchUTXO:
606
- await send(event: .startedFetching)
607
- case .handleSaplingParams:
608
- break
609
- case .clearCache:
610
- break
611
- case .finished:
612
- break
613
- case .failed:
614
- break
615
- case .stopped:
616
- break
617
- }
867
+ func notifyMinedTransaction(_ tx: ZcashTransaction.Overview) async {
868
+ logger.debug("notify mined transaction: \(tx)")
869
+ await send(event: .minedTransaction(tx))
618
870
  }
619
871
 
620
- func resetContext(restoreLastEnhancedHeight: Bool = true) async {
621
- let lastEnhancedHeight = await context.lastEnhancedHeight
622
- context = ActionContextImpl(state: .idle)
623
- if restoreLastEnhancedHeight {
624
- await context.update(lastEnhancedHeight: lastEnhancedHeight)
625
- }
872
+ func notifyProgress(_ progress: CompactBlockProgress) async {
873
+ logger.debug("progress: \(progress)")
874
+ await send(event: .progressUpdated(progress))
875
+ }
876
+
877
+ func notifyTransactions(_ txs: [ZcashTransaction.Overview], in range: CompactBlockRange) async {
878
+ await send(event: .foundTransactions(txs, range))
626
879
  }
627
880
 
628
- private func syncStarted() async {
629
- logger.debug("Sync started")
630
- // handle start of the sync process
631
- await send(event: .startedSyncing)
881
+ func determineLowerBound(
882
+ errorHeight: Int,
883
+ consecutiveErrors: Int,
884
+ walletBirthday: BlockHeight
885
+ ) -> BlockHeight {
886
+ let offset = min(ZcashSDK.maxReorgSize, ZcashSDK.defaultRewindDistance * (consecutiveErrors + 1))
887
+ return max(errorHeight - offset, walletBirthday - ZcashSDK.maxReorgSize)
632
888
  }
633
889
 
634
- private func syncFinished() async -> Bool {
635
- logger.debug("Sync finished")
636
- let latestBlockHeightWhenSyncing = await context.syncControlData.latestBlockHeight
637
- let latestBlockHeight = await latestBlocksDataProvider.latestBlockHeight
638
- // If `latestBlockHeightWhenSyncing` is 0 then it means that there was nothing to sync in last sync process.
639
- let newerBlocksWereMinedDuringSync =
640
- latestBlockHeightWhenSyncing > 0 && latestBlockHeightWhenSyncing < latestBlockHeight
890
+ func severeFailure(_ error: Error) async {
891
+ cancelableTask?.cancel()
892
+ await blockDownloader.stopDownload()
893
+ logger.error("show stopper failure: \(error)")
894
+ self.backoffTimer?.invalidate()
895
+ self.retryAttempts = config.retries
896
+ self.processingError = error
897
+ await updateState(.error(error))
898
+ await self.notifyError(error)
899
+ }
641
900
 
642
- retryAttempts = 0
643
- consecutiveChainValidationErrors = 0
901
+ func fail(_ error: Error) async {
902
+ // TODO: [#713] specify: failure. https://github.com/zcash/ZcashLightClientKit/issues/713
903
+ logger.error("\(error)")
904
+ cancelableTask?.cancel()
905
+ await blockDownloader.stopDownload()
906
+ self.retryAttempts += 1
907
+ self.processingError = error
908
+ switch self.state {
909
+ case .error:
910
+ await notifyError(error)
911
+ default:
912
+ break
913
+ }
914
+ await updateState(.error(error))
915
+ guard self.maxAttemptsReached else { return }
916
+ // don't set a new timer if there are no more attempts.
917
+ await self.setTimer()
918
+ }
644
919
 
645
- let lastScannedHeight = await latestBlocksDataProvider.maxScannedHeight
646
- // Some actions may not run. For example there are no transactions to enhance and therefore there is no enhance progress. And in
647
- // cases like this computation of final progress won't work properly. So let's fake 100% progress at the end of the sync process.
648
- await send(event: .progressUpdated(1))
649
- await send(event: .finished(lastScannedHeight))
650
- await context.update(state: .finished)
920
+ private func validateConfiguration() throws {
921
+ guard FileManager.default.isReadableFile(atPath: config.fsBlockCacheRoot.absoluteString) else {
922
+ throw ZcashError.compactBlockProcessorMissingDbPath(config.fsBlockCacheRoot.absoluteString)
923
+ }
651
924
 
652
- // If new blocks were mined during previous sync run the sync process again
653
- if newerBlocksWereMinedDuringSync {
654
- return true
655
- } else {
656
- await setTimer()
657
- return false
925
+ guard FileManager.default.isReadableFile(atPath: config.dataDb.absoluteString) else {
926
+ throw ZcashError.compactBlockProcessorMissingDbPath(config.dataDb.absoluteString)
927
+ }
928
+ }
929
+
930
+ private func nextBatch() async {
931
+ await updateState(.syncing)
932
+ if backoffTimer == nil { await setTimer() }
933
+ do {
934
+ let nextState = try await NextStateHelper.nextState(
935
+ service: self.service,
936
+ downloaderService: blockDownloaderService,
937
+ latestBlocksDataProvider: latestBlocksDataProvider,
938
+ config: self.config,
939
+ rustBackend: self.rustBackend,
940
+ internalSyncProgress: internalSyncProgress
941
+ )
942
+ switch nextState {
943
+ case .finishProcessing(let height):
944
+ await self.processingFinished(height: height)
945
+ case .processNewBlocks(let ranges):
946
+ await self.processNewBlocks(ranges: ranges)
947
+ case let .wait(latestHeight, latestDownloadHeight):
948
+ // Lightwalletd might be syncing
949
+ logger.info(
950
+ "Lightwalletd might be syncing: latest downloaded block height is: \(latestDownloadHeight) " +
951
+ "while latest blockheight is reported at: \(latestHeight)"
952
+ )
953
+ await self.processingFinished(height: latestDownloadHeight)
954
+ }
955
+ } catch {
956
+ await self.severeFailure(error)
658
957
  }
659
958
  }
660
959
 
661
- private func failure(_ error: Error) async {
662
- await context.update(state: .failed)
960
+ internal func validationFailed(at height: BlockHeight) async {
961
+ // cancel all Tasks
962
+ cancelableTask?.cancel()
963
+ await blockDownloader.stopDownload()
663
964
 
664
- logger.error("Fail with error: \(error)")
965
+ // register latest failure
966
+ self.lastChainValidationFailure = height
967
+
968
+ // rewind
969
+ let rewindHeight = determineLowerBound(
970
+ errorHeight: height,
971
+ consecutiveErrors: consecutiveChainValidationErrors,
972
+ walletBirthday: self.config.walletBirthday
973
+ )
665
974
 
666
- self.retryAttempts += 1
667
- await send(event: .failed(error))
975
+ self.consecutiveChainValidationErrors += 1
668
976
 
669
- // don't set a new timer if there are no more attempts.
670
- if hasRetryAttempt() {
671
- await self.setTimer()
977
+ do {
978
+ try await rustBackend.rewindToHeight(height: Int32(rewindHeight))
979
+ } catch {
980
+ await fail(error)
981
+ return
982
+ }
983
+
984
+ do {
985
+ try await blockDownloaderService.rewind(to: rewindHeight)
986
+ await internalSyncProgress.rewind(to: rewindHeight)
987
+
988
+ await send(event: .handledReorg(height, rewindHeight))
989
+
990
+ // process next batch
991
+ await self.nextBatch()
992
+ } catch {
993
+ await self.fail(error)
672
994
  }
673
995
  }
674
996
 
675
- private func handleAfterSyncHooks() async throws -> Bool {
676
- let afterSyncHooksManager = self.afterSyncHooksManager
677
- self.afterSyncHooksManager = AfterSyncHooksManager()
997
+ internal func processBatchFinished(height: BlockHeight?) async {
998
+ retryAttempts = 0
999
+ consecutiveChainValidationErrors = 0
678
1000
 
679
- if let wipeContext = afterSyncHooksManager.shouldExecuteWipeHook() {
680
- try await doWipe(context: wipeContext)
681
- return false
682
- } else if let rewindContext = afterSyncHooksManager.shouldExecuteRewindHook() {
683
- try await doRewind(context: rewindContext)
684
- return false
685
- } else if afterSyncHooksManager.shouldExecuteAnotherSyncHook() {
686
- logger.debug("Starting new sync.")
687
- return true
1001
+ if let height {
1002
+ await processingFinished(height: height)
688
1003
  } else {
689
- return false
1004
+ await nextBatch()
690
1005
  }
691
1006
  }
692
- }
1007
+
1008
+ private func processingFinished(height: BlockHeight) async {
1009
+ await send(event: .finished(height, foundBlocks))
1010
+ await updateState(.synced)
1011
+ await setTimer()
1012
+ }
693
1013
 
694
- // MARK: - Utils
1014
+ private func ifTaskIsNotCanceledClearCompactBlockCache(lastScannedHeight: BlockHeight) async {
1015
+ guard !Task.isCancelled else { return }
1016
+ do {
1017
+ // Blocks download work in parallel with scanning. So imagine this scenario:
1018
+ //
1019
+ // Scanning is done until height 10300. Blocks are downloaded until height 10400.
1020
+ // And now validation fails and this method is called. And `.latestDownloadedBlockHeight` in `internalSyncProgress` is set to 10400. And
1021
+ // all the downloaded blocks are removed here.
1022
+ //
1023
+ // If this line doesn't happen then when sync starts next time it thinks that all the blocks are downloaded until 10400. But all were
1024
+ // removed. So blocks between 10300 and 10400 wouldn't ever be scanned.
1025
+ //
1026
+ // Scanning is done until 10300 so the SDK can be sure that blocks with height below 10300 are not required. So it makes sense to set
1027
+ // `.latestDownloadedBlockHeight` to `lastScannedHeight`. And sync will work fine in next run.
1028
+ await internalSyncProgress.set(lastScannedHeight, .latestDownloadedBlockHeight)
1029
+ try await clearCompactBlockCache()
1030
+ } catch {
1031
+ logger.error("`clearCompactBlockCache` failed after error: \(error)")
1032
+ }
1033
+ }
695
1034
 
696
- extension CompactBlockProcessor {
1035
+ private func clearCompactBlockCache(upTo height: BlockHeight) async throws {
1036
+ try await storage.clear(upTo: height)
1037
+ logger.info("Cache removed upTo \(height)")
1038
+ }
1039
+
1040
+ private func clearCompactBlockCache() async throws {
1041
+ await blockDownloader.stopDownload()
1042
+ try await storage.clear()
1043
+ logger.info("Cache removed")
1044
+ }
1045
+
697
1046
  private func setTimer() async {
698
- let interval = config.blockPollInterval
1047
+ let interval = self.config.blockPollInterval
699
1048
  self.backoffTimer?.invalidate()
700
1049
  let timer = Timer(
701
1050
  timeInterval: interval,
@@ -703,65 +1052,77 @@ extension CompactBlockProcessor {
703
1052
  block: { [weak self] _ in
704
1053
  Task { [weak self] in
705
1054
  guard let self else { return }
706
- if await self.isIdle() {
707
- if await self.canStartSync() {
1055
+ switch await self.state {
1056
+ case .syncing, .enhancing, .fetching, .handlingSaplingFiles:
1057
+ await self.latestBlocksDataProvider.updateBlockData()
1058
+ case .stopped, .error, .synced:
1059
+ if await self.shouldStart {
708
1060
  self.logger.debug(
709
1061
  """
710
1062
  Timer triggered: Starting compact Block processor!.
711
- Processor State: \(await self.context.state)
1063
+ Processor State: \(await self.state)
1064
+ latestHeight: \(try await self.transactionRepository.lastScannedHeight())
712
1065
  attempts: \(await self.retryAttempts)
713
1066
  """
714
1067
  )
715
1068
  await self.start()
716
- } else if await self.hasRetryAttempt() {
717
- await self.failure(ZcashError.compactBlockProcessorMaxAttemptsReached(self.config.retries))
1069
+ } else if await self.maxAttemptsReached {
1070
+ await self.fail(ZcashError.compactBlockProcessorMaxAttemptsReached(self.config.retries))
718
1071
  }
719
- } else {
720
- await self.latestBlocksDataProvider.updateBlockData()
721
1072
  }
722
1073
  }
723
1074
  }
724
1075
  )
725
1076
  RunLoop.main.add(timer, forMode: .default)
1077
+
726
1078
  self.backoffTimer = timer
727
1079
  }
1080
+
1081
+ private func transitionState(from oldValue: State, to newValue: State) async {
1082
+ guard oldValue != newValue else {
1083
+ return
1084
+ }
728
1085
 
729
- private func isIdle() async -> Bool {
730
- return syncTask == nil
731
- }
732
-
733
- private func canStartSync() async -> Bool {
734
- return await isIdle() && hasRetryAttempt()
735
- }
736
-
737
- private func hasRetryAttempt() -> Bool {
738
- retryAttempts < config.retries
739
- }
740
-
741
- func determineLowerBound(errorHeight: Int, consecutiveErrors: Int, walletBirthday: BlockHeight) -> BlockHeight {
742
- let offset = min(ZcashSDK.maxReorgSize, ZcashSDK.defaultRewindDistance * (consecutiveErrors + 1))
743
- return max(errorHeight - offset, walletBirthday - ZcashSDK.maxReorgSize)
744
- }
745
-
746
- private func stopAllActions() async {
747
- for action in actions.values {
748
- await action.stop()
1086
+ switch newValue {
1087
+ case .error(let err):
1088
+ await notifyError(err)
1089
+ case .stopped:
1090
+ await send(event: .stopped)
1091
+ case .enhancing:
1092
+ await send(event: .startedEnhancing)
1093
+ case .fetching:
1094
+ await send(event: .startedFetching)
1095
+ case .handlingSaplingFiles:
1096
+ // We don't report this to outside world as separate phase for now.
1097
+ break
1098
+ case .synced:
1099
+ // transition to this state is handled by `processingFinished(height: BlockHeight)`
1100
+ break
1101
+ case .syncing:
1102
+ await send(event: .startedSyncing)
749
1103
  }
750
1104
  }
751
1105
 
752
- private func ifTaskIsNotCanceledClearCompactBlockCache() async {
753
- guard !Task.isCancelled else { return }
754
- do {
755
- try await clearCompactBlockCache()
756
- } catch {
757
- logger.error("`clearCompactBlockCache` failed after error: \(error)")
758
- }
1106
+ private func notifyError(_ err: Error) async {
1107
+ await send(event: .failed(err))
759
1108
  }
1109
+ // TODO: [#713] encapsulate service errors better, https://github.com/zcash/ZcashLightClientKit/issues/713
1110
+ }
760
1111
 
761
- private func clearCompactBlockCache() async throws {
762
- await stopAllActions()
763
- try await storage.clear()
764
- logger.info("Cache removed")
1112
+ extension CompactBlockProcessor.State: Equatable {
1113
+ public static func == (lhs: CompactBlockProcessor.State, rhs: CompactBlockProcessor.State) -> Bool {
1114
+ switch (lhs, rhs) {
1115
+ case
1116
+ (.syncing, .syncing),
1117
+ (.stopped, .stopped),
1118
+ (.error, .error),
1119
+ (.synced, .synced),
1120
+ (.enhancing, .enhancing),
1121
+ (.fetching, .fetching):
1122
+ return true
1123
+ default:
1124
+ return false
1125
+ }
765
1126
  }
766
1127
  }
767
1128
 
@@ -837,17 +1198,168 @@ extension CompactBlockProcessor {
837
1198
  }
838
1199
  }
839
1200
 
840
- // MARK: - Config provider
1201
+ extension CompactBlockProcessor {
1202
+ enum NextState: Equatable {
1203
+ case finishProcessing(height: BlockHeight)
1204
+ case processNewBlocks(ranges: SyncRanges)
1205
+ case wait(latestHeight: BlockHeight, latestDownloadHeight: BlockHeight)
1206
+ }
1207
+
1208
+ @discardableResult
1209
+ func figureNextBatch(
1210
+ downloaderService: BlockDownloaderService
1211
+ ) async throws -> NextState {
1212
+ try Task.checkCancellation()
1213
+
1214
+ do {
1215
+ return try await CompactBlockProcessor.NextStateHelper.nextState(
1216
+ service: service,
1217
+ downloaderService: downloaderService,
1218
+ latestBlocksDataProvider: latestBlocksDataProvider,
1219
+ config: config,
1220
+ rustBackend: rustBackend,
1221
+ internalSyncProgress: internalSyncProgress
1222
+ )
1223
+ } catch {
1224
+ throw error
1225
+ }
1226
+ }
1227
+ }
1228
+
1229
+ extension CompactBlockProcessor {
1230
+ enum NextStateHelper {
1231
+ // swiftlint:disable:next function_parameter_count
1232
+ static func nextState(
1233
+ service: LightWalletService,
1234
+ downloaderService: BlockDownloaderService,
1235
+ latestBlocksDataProvider: LatestBlocksDataProvider,
1236
+ config: Configuration,
1237
+ rustBackend: ZcashRustBackendWelding,
1238
+ internalSyncProgress: InternalSyncProgress
1239
+ ) async throws -> CompactBlockProcessor.NextState {
1240
+ // It should be ok to not create new Task here because this method is already async. But for some reason something not good happens
1241
+ // when Task is not created here. For example tests start failing. Reason is unknown at this time.
1242
+ let task = Task(priority: .userInitiated) {
1243
+ let info = try await service.getInfo()
1244
+
1245
+ try await CompactBlockProcessor.validateServerInfo(
1246
+ info,
1247
+ saplingActivation: config.saplingActivation,
1248
+ localNetwork: config.network,
1249
+ rustBackend: rustBackend
1250
+ )
1251
+
1252
+ let latestDownloadHeight = try await downloaderService.lastDownloadedBlockHeight()
1253
+
1254
+ await internalSyncProgress.migrateIfNeeded(latestDownloadedBlockHeightFromCacheDB: latestDownloadHeight)
1255
+
1256
+ await latestBlocksDataProvider.updateScannedData()
1257
+ await latestBlocksDataProvider.updateBlockData()
1258
+
1259
+ return await internalSyncProgress.computeNextState(
1260
+ latestBlockHeight: latestBlocksDataProvider.latestBlockHeight,
1261
+ latestScannedHeight: latestBlocksDataProvider.latestScannedHeight,
1262
+ walletBirthday: config.walletBirthday
1263
+ )
1264
+ }
841
1265
 
1266
+ return try await task.value
1267
+ }
1268
+ }
1269
+ }
1270
+
1271
+ /// This extension contains asociated types and functions needed to clean up the
1272
+ /// `cacheDb` in favor of `FsBlockDb`. Once this cleanup functionality is deprecated,
1273
+ /// delete the whole extension and reference to it in other parts of the code including tests.
842
1274
  extension CompactBlockProcessor {
843
- actor ConfigProvider {
844
- var config: Configuration
845
- init(config: Configuration) {
846
- self.config = config
1275
+ public enum CacheDbMigrationError: Error {
1276
+ case fsCacheMigrationFailedSameURL
1277
+ case failedToDeleteLegacyDb(Error)
1278
+ case failedToInitFsBlockDb(Error)
1279
+ case failedToSetDownloadHeight(Error)
1280
+ }
1281
+
1282
+ /// Deletes the SQLite cacheDb and attempts to initialize the fsBlockDbRoot
1283
+ /// - parameter legacyCacheDbURL: the URL where the cache Db used to be stored.
1284
+ /// - Throws: `InitializerError.fsCacheInitFailedSameURL` when the given URL
1285
+ /// is the same URL than the one provided as `self.fsBlockDbRoot` assuming that's a
1286
+ /// programming error being the `legacyCacheDbURL` a sqlite database file and not a
1287
+ /// directory. Also throws errors from initializing the fsBlockDbRoot.
1288
+ ///
1289
+ /// - Note: Errors from deleting the `legacyCacheDbURL` won't be throwns.
1290
+ func migrateCacheDb(_ legacyCacheDbURL: URL) async throws {
1291
+ guard legacyCacheDbURL != config.fsBlockCacheRoot else {
1292
+ throw ZcashError.compactBlockProcessorCacheDbMigrationFsCacheMigrationFailedSameURL
1293
+ }
1294
+
1295
+ // Instance with alias `default` is same as instance before the Alias was introduced. So it makes sense that only this instance handles
1296
+ // legacy cache DB. Any instance with different than `default` alias was created after the Alias was introduced and at this point legacy
1297
+ // cache DB is't anymore. So there is nothing to migrate for instances with not default Alias.
1298
+ guard config.alias == .default else {
1299
+ return
1300
+ }
1301
+
1302
+ // if the URL provided is not readable, it means that the client has a reference
1303
+ // to the cacheDb file but it has been deleted in a prior sync cycle. there's
1304
+ // nothing to do here.
1305
+ guard FileManager.default.isReadableFile(atPath: legacyCacheDbURL.path) else {
1306
+ return
1307
+ }
1308
+
1309
+ do {
1310
+ // if there's a readable file at the provided URL, delete it.
1311
+ try FileManager.default.removeItem(at: legacyCacheDbURL)
1312
+ } catch {
1313
+ throw ZcashError.compactBlockProcessorCacheDbMigrationFailedToDeleteLegacyDb(error)
1314
+ }
1315
+
1316
+ // create the storage
1317
+ try await self.storage.create()
1318
+
1319
+ // The database has been deleted, so we have adjust the internal state of the
1320
+ // `CompactBlockProcessor` so that it doesn't rely on download heights set
1321
+ // by a previous processing cycle.
1322
+ let lastScannedHeight = try await transactionRepository.lastScannedHeight()
1323
+
1324
+ await internalSyncProgress.set(lastScannedHeight, .latestDownloadedBlockHeight)
1325
+ }
1326
+
1327
+ func wipeLegacyCacheDbIfNeeded() {
1328
+ guard let cacheDbURL = config.cacheDbURL else {
1329
+ return
1330
+ }
1331
+
1332
+ guard FileManager.default.isDeletableFile(atPath: cacheDbURL.pathExtension) else {
1333
+ return
847
1334
  }
848
1335
 
849
- func update(config: Configuration) async {
850
- self.config = config
1336
+ try? FileManager.default.removeItem(at: cacheDbURL)
1337
+ }
1338
+ }
1339
+
1340
+ extension SyncRanges {
1341
+ /// Tells whether the state represented by these sync ranges evidence some sort of
1342
+ /// outdated state on the cache or the internal state of the compact block processor.
1343
+ ///
1344
+ /// - Note: this can mean that the processor has synced over the height that the internal
1345
+ /// state knows of because the sync process was interrupted before it could reflect
1346
+ /// it in the internal state storage. This could happen because of many factors, the
1347
+ /// most feasible being OS shutting down a background process or the user abruptly
1348
+ /// exiting the app.
1349
+ /// - Returns: an ``Optional<BlockHeight>`` where Some represents what's the
1350
+ /// new state the internal state should reflect and indicating that the cache should be cleared
1351
+ /// as well. c`None` means that no action is required.
1352
+ func shouldClearBlockCacheAndUpdateInternalState() -> BlockHeight? {
1353
+ guard self.downloadedButUnscannedRange != nil else {
1354
+ return nil
851
1355
  }
1356
+
1357
+ guard
1358
+ let latestScannedHeight = self.latestScannedHeight,
1359
+ let latestDownloadedHeight = self.latestDownloadedBlockHeight,
1360
+ latestScannedHeight > latestDownloadedHeight
1361
+ else { return nil }
1362
+
1363
+ return latestScannedHeight
852
1364
  }
853
1365
  }