react-native-zcash 0.5.0 → 0.6.0

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 (156) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +6 -3
  3. package/android/build.gradle +5 -4
  4. package/android/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2240000.json +8 -0
  5. package/android/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2250000.json +8 -0
  6. package/android/src/main/java/app/edge/rnzcash/RNZcashModule.kt +94 -121
  7. package/ios/RNZcash.m +1 -12
  8. package/ios/RNZcash.swift +119 -152
  9. package/ios/ZCashLightClientKit/Block/Actions/Action.swift +98 -0
  10. package/ios/ZCashLightClientKit/Block/Actions/ClearAlreadyScannedBlocksAction.swift +35 -0
  11. package/ios/ZCashLightClientKit/Block/Actions/ClearCacheAction.swift +30 -0
  12. package/ios/ZCashLightClientKit/Block/Actions/DownloadAction.swift +67 -0
  13. package/ios/ZCashLightClientKit/Block/Actions/EnhanceAction.swift +97 -0
  14. package/ios/ZCashLightClientKit/Block/Actions/FetchUTXOsAction.swift +33 -0
  15. package/ios/ZCashLightClientKit/Block/Actions/MigrateLegacyCacheDBAction.swift +70 -0
  16. package/ios/ZCashLightClientKit/Block/Actions/ProcessSuggestedScanRangesAction.swift +59 -0
  17. package/ios/ZCashLightClientKit/Block/Actions/RewindAction.swift +48 -0
  18. package/ios/ZCashLightClientKit/Block/Actions/SaplingParamsAction.swift +33 -0
  19. package/ios/ZCashLightClientKit/Block/Actions/ScanAction.swift +95 -0
  20. package/ios/ZCashLightClientKit/Block/Actions/UpdateChainTipAction.swift +55 -0
  21. package/ios/ZCashLightClientKit/Block/Actions/UpdateSubtreeRootsAction.swift +58 -0
  22. package/ios/ZCashLightClientKit/Block/Actions/ValidateServerAction.swift +60 -0
  23. package/ios/ZCashLightClientKit/Block/CompactBlockProcessor.swift +421 -937
  24. package/ios/ZCashLightClientKit/Block/Download/BlockDownloader.swift +31 -17
  25. package/ios/ZCashLightClientKit/Block/Download/BlockDownloaderService.swift +2 -2
  26. package/ios/ZCashLightClientKit/Block/Enhance/BlockEnhancer.swift +46 -15
  27. package/ios/ZCashLightClientKit/Block/FetchUnspentTxOutputs/UTXOFetcher.swift +4 -15
  28. package/ios/ZCashLightClientKit/Block/FilesystemStorage/FSCompactBlockRepository.swift +4 -4
  29. package/ios/ZCashLightClientKit/Block/Scan/BlockScanner.swift +10 -35
  30. package/ios/ZCashLightClientKit/Block/Utils/CompactBlockProgress.swift +24 -0
  31. package/ios/ZCashLightClientKit/Block/Utils/SyncControlData.swift +25 -0
  32. package/ios/ZCashLightClientKit/ClosureSynchronizer.swift +1 -2
  33. package/ios/ZCashLightClientKit/CombineSynchronizer.swift +2 -5
  34. package/ios/ZCashLightClientKit/Constants/ZcashSDK.swift +7 -25
  35. package/ios/ZCashLightClientKit/DAO/TransactionDao.swift +40 -42
  36. package/ios/ZCashLightClientKit/DAO/UnspentTransactionOutputDao.swift +13 -4
  37. package/ios/ZCashLightClientKit/Entity/AccountEntity.swift +9 -0
  38. package/ios/ZCashLightClientKit/Entity/TransactionEntity.swift +7 -10
  39. package/ios/ZCashLightClientKit/Error/Sourcery/generateErrorCode.sh +1 -1
  40. package/ios/ZCashLightClientKit/Error/ZcashError.swift +121 -12
  41. package/ios/ZCashLightClientKit/Error/ZcashErrorCode.swift +43 -5
  42. package/ios/ZCashLightClientKit/Error/ZcashErrorCodeDefinition.swift +72 -6
  43. package/ios/ZCashLightClientKit/Extensions/Bool+ToData.swift +15 -0
  44. package/ios/ZCashLightClientKit/Extensions/Data+ToOtherTypes.swift +18 -0
  45. package/ios/ZCashLightClientKit/Extensions/Int+ToData.swift +15 -0
  46. package/ios/ZCashLightClientKit/Initializer.swift +47 -26
  47. package/ios/ZCashLightClientKit/Metrics/SDKMetrics.swift +0 -12
  48. package/ios/ZCashLightClientKit/Model/Checkpoint.swift +12 -0
  49. package/ios/ZCashLightClientKit/Model/ScanProgress.swift +29 -0
  50. package/ios/ZCashLightClientKit/Model/ScanRange.swift +31 -0
  51. package/ios/ZCashLightClientKit/Modules/Service/GRPC/LightWalletGRPCService.swift +15 -0
  52. package/ios/ZCashLightClientKit/Modules/Service/GRPC/ProtoBuf/compact_formats.pb.swift +150 -46
  53. package/ios/ZCashLightClientKit/Modules/Service/GRPC/ProtoBuf/proto/compact_formats.proto +30 -16
  54. package/ios/ZCashLightClientKit/Modules/Service/GRPC/ProtoBuf/proto/service.proto +32 -6
  55. package/ios/ZCashLightClientKit/Modules/Service/GRPC/ProtoBuf/service.grpc.swift +259 -22
  56. package/ios/ZCashLightClientKit/Modules/Service/GRPC/ProtoBuf/service.pb.swift +193 -7
  57. package/ios/ZCashLightClientKit/Modules/Service/LightWalletService.swift +8 -0
  58. package/ios/ZCashLightClientKit/Providers/LatestBlocksDataProvider.swift +18 -28
  59. package/ios/ZCashLightClientKit/Repository/CompactBlockRepository.swift +1 -1
  60. package/ios/ZCashLightClientKit/Repository/TransactionRepository.swift +2 -6
  61. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2092500.json +8 -0
  62. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2095000.json +8 -0
  63. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2097500.json +8 -0
  64. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2102500.json +8 -0
  65. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2105000.json +8 -0
  66. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2107500.json +8 -0
  67. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2112500.json +8 -0
  68. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2115000.json +8 -0
  69. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2117500.json +8 -0
  70. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2122500.json +8 -0
  71. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2125000.json +8 -0
  72. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2127500.json +8 -0
  73. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2132500.json +8 -0
  74. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2135000.json +8 -0
  75. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2137500.json +8 -0
  76. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2142500.json +8 -0
  77. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2145000.json +8 -0
  78. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2147500.json +8 -0
  79. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2152500.json +8 -0
  80. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2155000.json +8 -0
  81. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2157500.json +8 -0
  82. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2162500.json +8 -0
  83. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2165000.json +8 -0
  84. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2167500.json +8 -0
  85. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2172500.json +8 -0
  86. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2175000.json +8 -0
  87. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2177500.json +8 -0
  88. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2182500.json +8 -0
  89. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2185000.json +8 -0
  90. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2187500.json +8 -0
  91. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2192500.json +8 -0
  92. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2195000.json +8 -0
  93. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2197500.json +8 -0
  94. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2202500.json +8 -0
  95. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2205000.json +8 -0
  96. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2207500.json +8 -0
  97. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2212500.json +8 -0
  98. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2215000.json +8 -0
  99. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2217500.json +8 -0
  100. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2222500.json +8 -0
  101. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2225000.json +8 -0
  102. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2227500.json +8 -0
  103. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2230000.json +8 -0
  104. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2232500.json +8 -0
  105. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2235000.json +8 -0
  106. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2237500.json +8 -0
  107. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2240000.json +8 -0
  108. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2242500.json +8 -0
  109. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2245000.json +8 -0
  110. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2247500.json +8 -0
  111. package/ios/ZCashLightClientKit/Resources/checkpoints/mainnet/2250000.json +8 -0
  112. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2350000.json +8 -0
  113. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2360000.json +8 -0
  114. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2370000.json +8 -0
  115. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2380000.json +8 -0
  116. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2390000.json +8 -0
  117. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2400000.json +8 -0
  118. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2410000.json +8 -0
  119. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2420000.json +8 -0
  120. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2430000.json +8 -0
  121. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2440000.json +8 -0
  122. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2450000.json +8 -0
  123. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2460000.json +8 -0
  124. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2470000.json +8 -0
  125. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2480000.json +8 -0
  126. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2490000.json +8 -0
  127. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2500000.json +8 -0
  128. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2510000.json +8 -0
  129. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2520000.json +8 -0
  130. package/ios/ZCashLightClientKit/Resources/checkpoints/testnet/2530000.json +8 -0
  131. package/ios/ZCashLightClientKit/Rust/ZcashRustBackend.swift +293 -158
  132. package/ios/ZCashLightClientKit/Rust/ZcashRustBackendWelding.swift +58 -64
  133. package/ios/ZCashLightClientKit/Rust/zcashlc.h +618 -512
  134. package/ios/ZCashLightClientKit/Synchronizer/ClosureSDKSynchronizer.swift +2 -8
  135. package/ios/ZCashLightClientKit/Synchronizer/CombineSDKSynchronizer.swift +3 -15
  136. package/ios/ZCashLightClientKit/Synchronizer/Dependencies.swift +11 -30
  137. package/ios/ZCashLightClientKit/Synchronizer/SDKSynchronizer.swift +41 -50
  138. package/ios/ZCashLightClientKit/Synchronizer.swift +51 -65
  139. package/ios/ZCashLightClientKit/Transaction/TransactionEncoder.swift +2 -2
  140. package/ios/ZCashLightClientKit/Transaction/WalletTransactionEncoder.swift +7 -7
  141. package/ios/ZCashLightClientKit/Utils/OSLogger.swift +3 -3
  142. package/ios/ZCashLightClientKit/Utils/ZcashFileManager.swift +16 -0
  143. package/ios/libzcashlc.xcframework/Info.plist +9 -5
  144. package/ios/libzcashlc.xcframework/ios-arm64/libzcashlc.a +0 -0
  145. package/ios/libzcashlc.xcframework/ios-arm64_x86_64-simulator/libzcashlc.a +0 -0
  146. package/lib/rnzcash.rn.js +8 -30
  147. package/lib/rnzcash.rn.js.map +1 -1
  148. package/lib/src/react-native.d.ts +2 -4
  149. package/lib/src/types.d.ts +14 -14
  150. package/package.json +1 -1
  151. package/src/react-native.ts +11 -20
  152. package/src/types.ts +20 -23
  153. package/ios/ZCashLightClientKit/Block/Utils/InternalSyncProgress.swift +0 -200
  154. package/ios/ZCashLightClientKit/Block/Validate/BlockValidator.swift +0 -51
  155. package/ios/ZCashLightClientKit/DAO/BlockDao.swift +0 -112
  156. package/ios/ZCashLightClientKit/Entity/BlockProgress.swift +0 -24
@@ -5,144 +5,46 @@
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
9
8
 
10
9
  import Foundation
11
10
  import Combine
12
11
 
13
12
  public typealias RefreshedUTXOs = (inserted: [UnspentTransactionOutputEntity], skipped: [UnspentTransactionOutputEntity])
14
13
 
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
-
104
14
  /// The compact block processor is in charge of orchestrating the download and caching of compact blocks from a LightWalletEndpoint
105
15
  /// when started the processor downloads does a download - validate - scan cycle until it reaches latest height on the blockchain.
106
16
  actor CompactBlockProcessor {
107
- typealias EventClosure = (Event) async -> Void
108
-
109
- enum Event {
110
- /// Event sent when the CompactBlockProcessor presented an error.
111
- case failed (Error)
112
-
113
- /// Event sent when the CompactBlockProcessor has finished syncing the blockchain to latest height
114
- case finished (_ lastScannedHeight: BlockHeight, _ foundBlocks: Bool)
115
-
116
- /// Event sent when the CompactBlockProcessor found a newly mined transaction
117
- case minedTransaction(ZcashTransaction.Overview)
118
-
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]))
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] = [:]
132
21
 
133
- /// Event sent when the CompactBlockProcessor starts enhancing of the transactions.
134
- case startedEnhancing
22
+ private var syncTask: Task<Void, Error>?
135
23
 
136
- /// Event sent when the CompactBlockProcessor starts fetching of the UTXOs.
137
- case startedFetching
24
+ private let actions: [CBPState: Action]
25
+ private var context: ActionContext
138
26
 
139
- /// Event sent when the CompactBlockProcessor starts syncing.
140
- case startedSyncing
27
+ private(set) var config: Configuration
28
+ private let configProvider: ConfigProvider
29
+ private var afterSyncHooksManager = AfterSyncHooksManager()
141
30
 
142
- /// Event sent when the CompactBlockProcessor stops syncing.
143
- case stopped
144
- }
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
145
41
 
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
+
146
48
  /// Compact Block Processor configuration
147
49
  ///
148
50
  /// - parameter fsBlockCacheRoot: absolute root path where the filesystem block cache will be stored.
@@ -156,8 +58,8 @@ actor CompactBlockProcessor {
156
58
  let dataDb: URL
157
59
  let spendParamsURL: URL
158
60
  let outputParamsURL: URL
159
- let downloadBatchSize: Int
160
- let scanningBatchSize: Int
61
+ let enhanceBatchSize: Int
62
+ let batchSize: Int
161
63
  let retries: Int
162
64
  let maxBackoffInterval: TimeInterval
163
65
  let maxReorgSize = ZcashSDK.maxReorgSize
@@ -171,7 +73,7 @@ actor CompactBlockProcessor {
171
73
  var blockPollInterval: TimeInterval {
172
74
  TimeInterval.random(in: ZcashSDK.defaultPollInterval / 2 ... ZcashSDK.defaultPollInterval * 1.5)
173
75
  }
174
-
76
+
175
77
  init(
176
78
  alias: ZcashSynchronizerAlias,
177
79
  cacheDbURL: URL? = nil,
@@ -180,11 +82,11 @@ actor CompactBlockProcessor {
180
82
  spendParamsURL: URL,
181
83
  outputParamsURL: URL,
182
84
  saplingParamsSourceURL: SaplingParamsSourceURL,
183
- downloadBatchSize: Int = ZcashSDK.DefaultDownloadBatch,
85
+ enhanceBatchSize: Int = ZcashSDK.DefaultEnhanceBatch,
86
+ batchSize: Int = ZcashSDK.DefaultBatchSize,
184
87
  retries: Int = ZcashSDK.defaultRetries,
185
88
  maxBackoffInterval: TimeInterval = ZcashSDK.defaultMaxBackOffInterval,
186
89
  rewindDistance: Int = ZcashSDK.defaultRewindDistance,
187
- scanningBatchSize: Int = ZcashSDK.DefaultScanningBatch,
188
90
  walletBirthdayProvider: @escaping () -> BlockHeight,
189
91
  saplingActivation: BlockHeight,
190
92
  network: ZcashNetwork
@@ -196,17 +98,16 @@ actor CompactBlockProcessor {
196
98
  self.outputParamsURL = outputParamsURL
197
99
  self.saplingParamsSourceURL = saplingParamsSourceURL
198
100
  self.network = network
199
- self.downloadBatchSize = downloadBatchSize
101
+ self.enhanceBatchSize = enhanceBatchSize
102
+ self.batchSize = batchSize
200
103
  self.retries = retries
201
104
  self.maxBackoffInterval = maxBackoffInterval
202
105
  self.rewindDistance = rewindDistance
203
- self.scanningBatchSize = scanningBatchSize
204
106
  self.walletBirthdayProvider = walletBirthdayProvider
205
107
  self.saplingActivation = saplingActivation
206
108
  self.cacheDbURL = cacheDbURL
207
- assert(downloadBatchSize >= scanningBatchSize)
208
109
  }
209
-
110
+
210
111
  init(
211
112
  alias: ZcashSynchronizerAlias,
212
113
  fsBlockCacheRoot: URL,
@@ -214,11 +115,11 @@ actor CompactBlockProcessor {
214
115
  spendParamsURL: URL,
215
116
  outputParamsURL: URL,
216
117
  saplingParamsSourceURL: SaplingParamsSourceURL,
217
- downloadBatchSize: Int = ZcashSDK.DefaultDownloadBatch,
118
+ enhanceBatchSize: Int = ZcashSDK.DefaultEnhanceBatch,
119
+ batchSize: Int = ZcashSDK.DefaultBatchSize,
218
120
  retries: Int = ZcashSDK.defaultRetries,
219
121
  maxBackoffInterval: TimeInterval = ZcashSDK.defaultMaxBackOffInterval,
220
122
  rewindDistance: Int = ZcashSDK.defaultRewindDistance,
221
- scanningBatchSize: Int = ZcashSDK.DefaultScanningBatch,
222
123
  walletBirthdayProvider: @escaping () -> BlockHeight,
223
124
  network: ZcashNetwork
224
125
  ) {
@@ -232,124 +133,21 @@ actor CompactBlockProcessor {
232
133
  self.saplingActivation = network.constants.saplingActivationHeight
233
134
  self.network = network
234
135
  self.cacheDbURL = nil
235
- self.downloadBatchSize = downloadBatchSize
136
+ self.enhanceBatchSize = enhanceBatchSize
137
+ self.batchSize = batchSize
236
138
  self.retries = retries
237
139
  self.maxBackoffInterval = maxBackoffInterval
238
140
  self.rewindDistance = rewindDistance
239
- self.scanningBatchSize = scanningBatchSize
240
-
241
- assert(downloadBatchSize >= scanningBatchSize)
242
- }
243
- }
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
141
  }
304
142
  }
305
143
 
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
-
343
144
  /// Initializes a CompactBlockProcessor instance
344
145
  /// - Parameters:
345
146
  /// - service: concrete implementation of `LightWalletService` protocol
346
147
  /// - storage: concrete implementation of `CompactBlockRepository` protocol
347
148
  /// - backend: a class that complies to `ZcashRustBackendWelding`
348
149
  /// - config: `Configuration` struct for this processor
349
- init(
350
- container: DIContainer,
351
- config: Configuration
352
- ) {
150
+ init(container: DIContainer, config: Configuration) {
353
151
  self.init(
354
152
  container: container,
355
153
  config: config,
@@ -376,8 +174,8 @@ actor CompactBlockProcessor {
376
174
  accountRepository: initializer.accountRepository
377
175
  )
378
176
  }
379
-
380
- internal init(
177
+
178
+ init(
381
179
  container: DIContainer,
382
180
  config: Configuration,
383
181
  accountRepository: AccountRepository
@@ -388,172 +186,145 @@ actor CompactBlockProcessor {
388
186
  accountRepository: accountRepository
389
187
  )
390
188
 
189
+ let configProvider = ConfigProvider(config: config)
190
+ context = ActionContextImpl(state: .idle)
191
+ actions = Self.makeActions(container: container, configProvider: configProvider)
192
+
391
193
  self.metrics = container.resolve(SDKMetrics.self)
392
194
  self.logger = container.resolve(Logger.self)
393
195
  self.latestBlocksDataProvider = container.resolve(LatestBlocksDataProvider.self)
394
- self.internalSyncProgress = container.resolve(InternalSyncProgress.self)
395
196
  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)
402
197
  self.service = container.resolve(LightWalletService.self)
403
198
  self.rustBackend = container.resolve(ZcashRustBackendWelding.self)
404
199
  self.storage = container.resolve(CompactBlockRepository.self)
405
200
  self.config = config
406
201
  self.transactionRepository = container.resolve(TransactionRepository.self)
407
202
  self.accountRepository = accountRepository
408
- }
409
-
410
- deinit {
411
- cancelableTask?.cancel()
203
+ self.fileManager = container.resolve(ZcashFileManager.self)
204
+ self.configProvider = configProvider
412
205
  }
413
206
 
414
- func update(config: Configuration) async {
415
- self.config = config
416
- await stop()
417
- }
207
+ 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
+ }
418
246
 
419
- func updateState(_ newState: State) async -> Void {
420
- let oldState = state
421
- state = newState
422
- await transitionState(from: oldState, to: newState)
423
- }
247
+ return (state, action)
248
+ }
424
249
 
425
- func updateEventClosure(identifier: String, closure: @escaping (Event) async -> Void) async {
426
- eventClosures[identifier] = closure
250
+ return Dictionary(uniqueKeysWithValues: actionsDefinition)
427
251
  }
428
252
 
429
- func send(event: Event) async {
430
- for item in eventClosures {
431
- await item.value(event)
432
- }
253
+ // This is currently used only in tests. And it should be used only in tests.
254
+ func update(config: Configuration) async {
255
+ self.config = config
256
+ await configProvider.update(config: config)
433
257
  }
258
+ }
434
259
 
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
- }
260
+ // MARK: - "Public" API
465
261
 
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
262
+ extension CompactBlockProcessor {
471
263
  func start(retry: Bool = false) async {
472
264
  if retry {
473
265
  self.retryAttempts = 0
474
- self.processingError = nil
475
266
  self.backoffTimer?.invalidate()
476
267
  self.backoffTimer = nil
477
268
  }
478
269
 
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:
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 {
495
275
  logger.debug("Warning: compact block processor was started while busy!!!!")
496
276
  afterSyncHooksManager.insert(hook: .anotherSync)
497
277
  }
498
278
  return
499
279
  }
500
280
 
501
- do {
502
- if let legacyCacheDbURL = self.config.cacheDbURL {
503
- try await self.migrateCacheDb(legacyCacheDbURL)
504
- }
505
- } catch {
506
- await self.fail(error)
281
+ syncTask = Task(priority: .userInitiated) {
282
+ await run()
507
283
  }
508
-
509
- await self.nextBatch()
510
284
  }
511
285
 
512
- /**
513
- Stops the CompactBlockProcessor
514
-
515
- Note: retry count is reset
516
- */
517
286
  func stop() async {
287
+ syncTask?.cancel()
518
288
  self.backoffTimer?.invalidate()
519
289
  self.backoffTimer = nil
290
+ await stopAllActions()
291
+ retryAttempts = 0
292
+ }
520
293
 
521
- cancelableTask?.cancel()
522
- await blockDownloader.stopDownload()
523
-
524
- self.retryAttempts = 0
294
+ func latestHeight() async throws -> BlockHeight {
295
+ try await blockDownloaderService.latestBlockHeight()
525
296
  }
297
+ }
526
298
 
527
- // MARK: Rewind
299
+ // MARK: - Rewind
528
300
 
301
+ extension CompactBlockProcessor {
529
302
  /// Rewinds to provided height.
530
303
  /// - Parameter height: height to rewind to. If nil is provided, it will rescan to nearest height (quick rescan)
531
304
  ///
532
305
  /// - Note: If this is called while sync is in progress then the sync process is stopped first and then rewind is executed.
533
- func rewind(context: AfterSyncHooksManager.RewindContext) async {
306
+ func rewind(context: AfterSyncHooksManager.RewindContext) async throws {
534
307
  logger.debug("Starting rewind")
535
- switch self.state {
536
- case .syncing, .enhancing, .fetching, .handlingSaplingFiles:
308
+ if await isIdle() {
309
+ logger.debug("Sync doesn't run. Executing rewind.")
310
+ try await doRewind(context: context)
311
+ } else {
537
312
  logger.debug("Stopping sync because of rewind")
538
313
  afterSyncHooksManager.insert(hook: .rewind(context))
539
314
  await stop()
540
-
541
- case .stopped, .error, .synced:
542
- logger.debug("Sync doesn't run. Executing rewind.")
543
- await doRewind(context: context)
544
315
  }
545
316
  }
546
317
 
547
- private func doRewind(context: AfterSyncHooksManager.RewindContext) async {
318
+ private func doRewind(context: AfterSyncHooksManager.RewindContext) async throws {
548
319
  logger.debug("Executing rewind.")
549
- let lastDownloaded = await internalSyncProgress.latestDownloadedBlockHeight
320
+ let lastDownloaded = await latestBlocksDataProvider.maxScannedHeight
550
321
  let height = Int32(context.height ?? lastDownloaded)
551
322
 
552
323
  let nearestHeight: Int32
553
324
  do {
554
325
  nearestHeight = try await rustBackend.getNearestRewindHeight(height: height)
555
326
  } catch {
556
- await fail(error)
327
+ await failure(error)
557
328
  return await context.completion(.failure(error))
558
329
  }
559
330
 
@@ -561,9 +332,10 @@ actor CompactBlockProcessor {
561
332
  let rewindHeight = max(Int32(nearestHeight - 1), Int32(config.walletBirthday))
562
333
 
563
334
  do {
335
+ try await rewindDownloadBlockAction(to: BlockHeight(rewindHeight))
564
336
  try await rustBackend.rewindToHeight(height: rewindHeight)
565
337
  } catch {
566
- await fail(error)
338
+ await failure(error)
567
339
  return await context.completion(.failure(error))
568
340
  }
569
341
 
@@ -575,37 +347,43 @@ actor CompactBlockProcessor {
575
347
  return await context.completion(.failure(error))
576
348
  }
577
349
 
578
- await internalSyncProgress.rewind(to: rewindBlockHeight)
579
-
580
- self.lastChainValidationFailure = nil
581
350
  await context.completion(.success(rewindBlockHeight))
582
351
  }
352
+ }
353
+
354
+ // MARK: - Actions
355
+
356
+ private extension CompactBlockProcessor {
357
+ func rewindDownloadBlockAction(to rewindHeight: BlockHeight?) async throws {
358
+ if let downloadAction = actions[.download] as? DownloadAction {
359
+ await downloadAction.downloader.rewind(latestDownloadedBlockHeight: rewindHeight)
360
+ } else {
361
+ throw ZcashError.compactBlockProcessorDownloadBlockActionRewind
362
+ }
363
+ }
364
+ }
583
365
 
584
- // MARK: Wipe
366
+ // MARK: - Wipe
585
367
 
586
- func wipe(context: AfterSyncHooksManager.WipeContext) async {
368
+ extension CompactBlockProcessor {
369
+ func wipe(context: AfterSyncHooksManager.WipeContext) async throws {
587
370
  logger.debug("Starting wipe")
588
- switch self.state {
589
- case .syncing, .enhancing, .fetching, .handlingSaplingFiles:
371
+ if await isIdle() {
372
+ logger.debug("Sync doesn't run. Executing wipe.")
373
+ try await doWipe(context: context)
374
+ } else {
590
375
  logger.debug("Stopping sync because of wipe")
591
376
  afterSyncHooksManager.insert(hook: .wipe(context))
592
377
  await stop()
593
-
594
- case .stopped, .error, .synced:
595
- logger.debug("Sync doesn't run. Executing wipe.")
596
- await doWipe(context: context)
597
378
  }
598
379
  }
599
380
 
600
- private func doWipe(context: AfterSyncHooksManager.WipeContext) async {
381
+ private func doWipe(context: AfterSyncHooksManager.WipeContext) async throws {
601
382
  logger.debug("Executing wipe.")
602
383
  context.prewipe()
603
384
 
604
- await updateState(.stopped)
605
-
606
385
  do {
607
386
  try await self.storage.clear()
608
- await internalSyncProgress.rewind(to: 0)
609
387
 
610
388
  wipeLegacyCacheDbIfNeeded()
611
389
 
@@ -614,437 +392,306 @@ actor CompactBlockProcessor {
614
392
  try fileManager.removeItem(at: config.dataDb)
615
393
  }
616
394
 
395
+ try await rewindDownloadBlockAction(to: nil)
396
+
617
397
  await context.completion(nil)
618
398
  } catch {
619
399
  await context.completion(error)
620
400
  }
621
401
  }
622
402
 
623
- // MARK: Sync
624
-
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
- }
403
+ private func wipeLegacyCacheDbIfNeeded() {
404
+ guard let cacheDbURL = config.cacheDbURL else { return }
405
+ guard fileManager.isDeletableFile(atPath: cacheDbURL.pathExtension) else { return }
406
+ try? fileManager.removeItem(at: cacheDbURL)
637
407
  }
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
- }
408
+ }
669
409
 
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
- }
410
+ // MARK: - Events
681
411
 
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
- }
412
+ extension CompactBlockProcessor {
413
+ typealias EventClosure = (Event) async -> Void
689
414
 
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
- }
415
+ enum Event {
416
+ /// Event sent when the CompactBlockProcessor presented an error.
417
+ case failed(Error)
708
418
 
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
- }
419
+ /// Event sent when the CompactBlockProcessor has finished syncing the blockchain to latest height
420
+ case finished(_ lastScannedHeight: BlockHeight)
718
421
 
719
- logger.debug("Fetching sapling parameters")
720
- await updateState(.handlingSaplingFiles)
721
- try await saplingParametersHandler.handleIfNeeded()
422
+ /// Event sent when the CompactBlockProcessor found a newly mined transaction
423
+ case minedTransaction(ZcashTransaction.Overview)
722
424
 
723
- logger.debug("Clearing cache")
724
- try await clearCompactBlockCache()
425
+ /// Event sent when the CompactBlockProcessor enhanced a bunch of transactions in some range.
426
+ case foundTransactions([ZcashTransaction.Overview], CompactBlockRange)
725
427
 
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)")
428
+ /// Event sent when the CompactBlockProcessor handled a ReOrg.
429
+ /// `reorgHeight` is the height on which the reorg was detected.
430
+ /// `rewindHeight` is the height that the processor backed to in order to solve the Reorg.
431
+ case handledReorg(_ reorgHeight: BlockHeight, _ rewindHeight: BlockHeight)
734
432
 
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
- }
749
- }
433
+ /// Event sent when progress of some specific action happened.
434
+ case syncProgress(Float)
750
435
 
751
- private func handleAfterSyncHooks() async {
752
- let afterSyncHooksManager = self.afterSyncHooksManager
753
- self.afterSyncHooksManager = AfterSyncHooksManager()
436
+ /// Event sent when progress of the sync process changes.
437
+ case progressUpdated(Float)
754
438
 
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()
762
- }
439
+ /// Event sent when the CompactBlockProcessor fetched utxos from lightwalletd attempted to store them.
440
+ case storedUTXOs((inserted: [UnspentTransactionOutputEntity], skipped: [UnspentTransactionOutputEntity]))
441
+
442
+ /// Event sent when the CompactBlockProcessor starts enhancing of the transactions.
443
+ case startedEnhancing
444
+
445
+ /// Event sent when the CompactBlockProcessor starts fetching of the UTXOs.
446
+ case startedFetching
447
+
448
+ /// Event sent when the CompactBlockProcessor starts syncing.
449
+ case startedSyncing
450
+
451
+ /// Event sent when the CompactBlockProcessor stops syncing.
452
+ case stopped
763
453
  }
764
454
 
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
455
+ func updateEventClosure(identifier: String, closure: @escaping (Event) async -> Void) async {
456
+ eventClosures[identifier] = closure
457
+ }
458
+
459
+ private func send(event: Event) async {
460
+ for item in eventClosures {
461
+ await item.value(event)
772
462
  }
463
+ }
464
+ }
773
465
 
774
- var lastScannedHeight: BlockHeight = .zero
775
- for i in 0..<loopsCount {
776
- let processingRange = computeSingleLoopDownloadRange(fullRange: range, loopCounter: i, batchSize: batchSize)
466
+ // MARK: - Main loop
777
467
 
778
- logger.debug("Sync loop #\(i + 1) range: \(processingRange.lowerBound)...\(processingRange.upperBound)")
468
+ extension CompactBlockProcessor {
469
+ // 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
470
+ // 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
471
+ // of sync process without any side effects.
472
+ //
473
+ // 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
474
+ // tree in the code update this documentation. Image is generated by plantuml tool.
475
+ //
476
+ // swiftlint:disable:next cyclomatic_complexity
477
+ private func run() async {
478
+ logger.debug("Starting run")
479
+ await resetContext()
480
+
481
+ while true {
482
+ // Sync is starting when the state is `idle`.
483
+ if await context.state == .idle {
484
+ // Side effect of calling stop is to delete last used download stream. To be sure that it doesn't keep any data in memory.
485
+ await stopAllActions()
486
+ // Update state to the first state in state machine that can be handled by action.
487
+ await context.update(state: .migrateLegacyCacheDB)
488
+ await syncStarted()
779
489
 
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()
490
+ if backoffTimer == nil {
491
+ await setTimer()
492
+ }
493
+ }
783
494
 
784
- do {
785
- await blockDownloader.setDownloadLimit(processingRange.upperBound + (2 * batchSize))
786
- await blockDownloader.startDownload(maxBlockBufferSize: config.downloadBufferSize)
495
+ let state = await context.state
496
+ logger.debug("Handling state: \(state)")
787
497
 
788
- try await blockDownloader.waitUntilRequestedBlocksAreDownloaded(in: processingRange)
789
- } catch {
790
- await ifTaskIsNotCanceledClearCompactBlockCache(lastScannedHeight: lastScannedHeight)
791
- throw error
498
+ // Try to find action for state.
499
+ guard let action = actions[state] else {
500
+ // Side effect of calling stop is to delete last used download stream. To be sure that it doesn't keep any data in memory.
501
+ await stopAllActions()
502
+ if await syncFinished() {
503
+ await resetContext()
504
+ continue
505
+ } else {
506
+ break
507
+ }
792
508
  }
793
509
 
794
510
  do {
795
- try await blockValidator.validate()
796
- } catch {
797
- await ifTaskIsNotCanceledClearCompactBlockCache(lastScannedHeight: lastScannedHeight)
798
- logger.error("Block validation failed with error: \(error)")
799
- throw error
800
- }
511
+ try Task.checkCancellation()
512
+
513
+ // Execute action.
514
+ context = try await action.run(with: context) { [weak self] event in
515
+ await self?.send(event: event)
516
+ if let progressChanged = await self?.compactBlockProgress.hasProgressUpdated(event), progressChanged {
517
+ if let progress = await self?.compactBlockProgress.progress {
518
+ await self?.send(event: .progressUpdated(progress))
519
+ }
520
+ }
521
+ }
801
522
 
802
- // Without this `stop()` would work. But this line improves support for Task cancelation.
803
- try Task.checkCancellation()
523
+ await didFinishAction()
524
+ } catch {
525
+ // Side effect of calling stop is to delete last used download stream. To be sure that it doesn't keep any data in memory.
526
+ await stopAllActions()
527
+ logger.error("Sync failed with error: \(error)")
804
528
 
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))
529
+ if Task.isCancelled {
530
+ logger.info("Processing cancelled.")
531
+ do {
532
+ if try await syncTaskWasCancelled() {
533
+ // Start sync all over again
534
+ await resetContext()
535
+ } else {
536
+ // end the sync loop
537
+ break
538
+ }
539
+ } catch {
540
+ await failure(error)
541
+ break
542
+ }
543
+ } else {
544
+ if await handleSyncFailure(action: action, error: error) {
545
+ // Start sync all over again
546
+ await resetContext()
547
+ } else {
548
+ // end the sync loop
549
+ break
550
+ }
816
551
  }
817
- } catch {
818
- logger.error("Scanning failed with error: \(error)")
819
- await ifTaskIsNotCanceledClearCompactBlockCache(lastScannedHeight: lastScannedHeight)
820
- throw error
821
552
  }
822
-
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
553
  }
554
+
555
+ logger.debug("Run ended")
556
+ syncTask = nil
832
557
  }
833
558
 
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
559
+ private func syncTaskWasCancelled() async throws -> Bool {
560
+ logger.info("Sync cancelled.")
561
+ await context.update(state: .stopped)
562
+ await send(event: .stopped)
563
+ return try await handleAfterSyncHooks()
849
564
  }
850
565
 
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
566
+ private func handleSyncFailure(action: Action, error: Error) async -> Bool {
567
+ if action.removeBlocksCacheWhenFailed {
568
+ await ifTaskIsNotCanceledClearCompactBlockCache()
858
569
  }
859
570
 
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
571
+ logger.error("Sync failed with error: \(error)")
572
+ await failure(error)
863
573
 
864
- return lowerBound...upperBound
574
+ return false
865
575
  }
866
576
 
867
- func notifyMinedTransaction(_ tx: ZcashTransaction.Overview) async {
868
- logger.debug("notify mined transaction: \(tx)")
869
- await send(event: .minedTransaction(tx))
870
- }
871
-
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))
577
+ // swiftlint:disable:next cyclomatic_complexity
578
+ private func didFinishAction() async {
579
+ // This is evalution of the state setup by previous action.
580
+ switch await context.state {
581
+ case .idle:
582
+ break
583
+ case .migrateLegacyCacheDB:
584
+ break
585
+ case .validateServer:
586
+ break
587
+ case .updateSubtreeRoots:
588
+ break
589
+ case .updateChainTip:
590
+ break
591
+ case .processSuggestedScanRanges:
592
+ break
593
+ case .rewind:
594
+ break
595
+ case .download:
596
+ break
597
+ case .scan:
598
+ break
599
+ case .clearAlreadyScannedBlocks:
600
+ break
601
+ case .enhance:
602
+ await send(event: .startedEnhancing)
603
+ case .fetchUTXO:
604
+ await send(event: .startedFetching)
605
+ case .handleSaplingParams:
606
+ break
607
+ case .clearCache:
608
+ break
609
+ case .finished:
610
+ break
611
+ case .failed:
612
+ break
613
+ case .stopped:
614
+ break
615
+ }
879
616
  }
880
617
 
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)
618
+ private func resetContext() async {
619
+ let lastEnhancedheight = await context.lastEnhancedHeight
620
+ context = ActionContextImpl(state: .idle)
621
+ await context.update(lastEnhancedHeight: lastEnhancedheight)
888
622
  }
889
623
 
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)
624
+ private func syncStarted() async {
625
+ logger.debug("Sync started")
626
+ // handle start of the sync process
627
+ await send(event: .startedSyncing)
899
628
  }
900
629
 
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
- }
630
+ private func syncFinished() async -> Bool {
631
+ logger.debug("Sync finished")
632
+ let latestBlockHeightWhenSyncing = await context.syncControlData.latestBlockHeight
633
+ let latestBlockHeight = await latestBlocksDataProvider.latestBlockHeight
634
+ // If `latestBlockHeightWhenSyncing` is 0 then it means that there was nothing to sync in last sync process.
635
+ let newerBlocksWereMinedDuringSync =
636
+ latestBlockHeightWhenSyncing > 0 && latestBlockHeightWhenSyncing < latestBlockHeight
919
637
 
920
- private func validateConfiguration() throws {
921
- guard FileManager.default.isReadableFile(atPath: config.fsBlockCacheRoot.absoluteString) else {
922
- throw ZcashError.compactBlockProcessorMissingDbPath(config.fsBlockCacheRoot.absoluteString)
923
- }
638
+ retryAttempts = 0
639
+ consecutiveChainValidationErrors = 0
924
640
 
925
- guard FileManager.default.isReadableFile(atPath: config.dataDb.absoluteString) else {
926
- throw ZcashError.compactBlockProcessorMissingDbPath(config.dataDb.absoluteString)
927
- }
928
- }
641
+ let lastScannedHeight = await latestBlocksDataProvider.maxScannedHeight
642
+ // Some actions may not run. For example there are no transactions to enhance and therefore there is no enhance progress. And in
643
+ // cases like this computation of final progress won't work properly. So let's fake 100% progress at the end of the sync process.
644
+ await send(event: .progressUpdated(1))
645
+ await send(event: .finished(lastScannedHeight))
646
+ await context.update(state: .finished)
929
647
 
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)
648
+ // If new blocks were mined during previous sync run the sync process again
649
+ if newerBlocksWereMinedDuringSync {
650
+ return true
651
+ } else {
652
+ await setTimer()
653
+ return false
957
654
  }
958
655
  }
959
656
 
960
- internal func validationFailed(at height: BlockHeight) async {
961
- // cancel all Tasks
962
- cancelableTask?.cancel()
963
- await blockDownloader.stopDownload()
657
+ private func failure(_ error: Error) async {
658
+ await context.update(state: .failed)
964
659
 
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
- )
974
-
975
- self.consecutiveChainValidationErrors += 1
976
-
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)
660
+ logger.error("Fail with error: \(error)")
987
661
 
988
- await send(event: .handledReorg(height, rewindHeight))
662
+ self.retryAttempts += 1
663
+ await send(event: .failed(error))
989
664
 
990
- // process next batch
991
- await self.nextBatch()
992
- } catch {
993
- await self.fail(error)
665
+ // don't set a new timer if there are no more attempts.
666
+ if hasRetryAttempt() {
667
+ await self.setTimer()
994
668
  }
995
669
  }
996
670
 
997
- internal func processBatchFinished(height: BlockHeight?) async {
998
- retryAttempts = 0
999
- consecutiveChainValidationErrors = 0
671
+ private func handleAfterSyncHooks() async throws -> Bool {
672
+ let afterSyncHooksManager = self.afterSyncHooksManager
673
+ self.afterSyncHooksManager = AfterSyncHooksManager()
1000
674
 
1001
- if let height {
1002
- await processingFinished(height: height)
675
+ if let wipeContext = afterSyncHooksManager.shouldExecuteWipeHook() {
676
+ try await doWipe(context: wipeContext)
677
+ return false
678
+ } else if let rewindContext = afterSyncHooksManager.shouldExecuteRewindHook() {
679
+ try await doRewind(context: rewindContext)
680
+ return false
681
+ } else if afterSyncHooksManager.shouldExecuteAnotherSyncHook() {
682
+ logger.debug("Starting new sync.")
683
+ return true
1003
684
  } else {
1004
- await nextBatch()
1005
- }
1006
- }
1007
-
1008
- private func processingFinished(height: BlockHeight) async {
1009
- await send(event: .finished(height, foundBlocks))
1010
- await updateState(.synced)
1011
- await setTimer()
1012
- }
1013
-
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)")
685
+ return false
1032
686
  }
1033
687
  }
688
+ }
1034
689
 
1035
- private func clearCompactBlockCache(upTo height: BlockHeight) async throws {
1036
- try await storage.clear(upTo: height)
1037
- logger.info("Cache removed upTo \(height)")
1038
- }
690
+ // MARK: - Utils
1039
691
 
1040
- private func clearCompactBlockCache() async throws {
1041
- await blockDownloader.stopDownload()
1042
- try await storage.clear()
1043
- logger.info("Cache removed")
1044
- }
1045
-
692
+ extension CompactBlockProcessor {
1046
693
  private func setTimer() async {
1047
- let interval = self.config.blockPollInterval
694
+ let interval = config.blockPollInterval
1048
695
  self.backoffTimer?.invalidate()
1049
696
  let timer = Timer(
1050
697
  timeInterval: interval,
@@ -1052,78 +699,66 @@ actor CompactBlockProcessor {
1052
699
  block: { [weak self] _ in
1053
700
  Task { [weak self] in
1054
701
  guard let self else { return }
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 {
702
+ if await self.isIdle() {
703
+ if await self.canStartSync() {
1060
704
  self.logger.debug(
1061
705
  """
1062
706
  Timer triggered: Starting compact Block processor!.
1063
- Processor State: \(await self.state)
1064
- latestHeight: \(try await self.transactionRepository.lastScannedHeight())
707
+ Processor State: \(await self.context.state)
1065
708
  attempts: \(await self.retryAttempts)
1066
709
  """
1067
710
  )
1068
711
  await self.start()
1069
- } else if await self.maxAttemptsReached {
1070
- await self.fail(ZcashError.compactBlockProcessorMaxAttemptsReached(self.config.retries))
712
+ } else if await self.hasRetryAttempt() {
713
+ await self.failure(ZcashError.compactBlockProcessorMaxAttemptsReached(self.config.retries))
1071
714
  }
715
+ } else {
716
+ await self.latestBlocksDataProvider.updateBlockData()
1072
717
  }
1073
718
  }
1074
719
  }
1075
720
  )
1076
721
  RunLoop.main.add(timer, forMode: .default)
1077
-
1078
722
  self.backoffTimer = timer
1079
723
  }
1080
-
1081
- private func transitionState(from oldValue: State, to newValue: State) async {
1082
- guard oldValue != newValue else {
1083
- return
1084
- }
1085
724
 
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)
1103
- }
725
+ private func isIdle() async -> Bool {
726
+ return syncTask == nil
1104
727
  }
1105
728
 
1106
- private func notifyError(_ err: Error) async {
1107
- await send(event: .failed(err))
729
+ private func canStartSync() async -> Bool {
730
+ return await isIdle() && hasRetryAttempt()
1108
731
  }
1109
- // TODO: [#713] encapsulate service errors better, https://github.com/zcash/ZcashLightClientKit/issues/713
1110
- }
1111
732
 
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
733
+ private func hasRetryAttempt() -> Bool {
734
+ retryAttempts < config.retries
735
+ }
736
+
737
+ func determineLowerBound(errorHeight: Int, consecutiveErrors: Int, walletBirthday: BlockHeight) -> BlockHeight {
738
+ let offset = min(ZcashSDK.maxReorgSize, ZcashSDK.defaultRewindDistance * (consecutiveErrors + 1))
739
+ return max(errorHeight - offset, walletBirthday - ZcashSDK.maxReorgSize)
740
+ }
741
+
742
+ private func stopAllActions() async {
743
+ for action in actions.values {
744
+ await action.stop()
745
+ }
746
+ }
747
+
748
+ private func ifTaskIsNotCanceledClearCompactBlockCache() async {
749
+ guard !Task.isCancelled else { return }
750
+ do {
751
+ try await clearCompactBlockCache()
752
+ } catch {
753
+ logger.error("`clearCompactBlockCache` failed after error: \(error)")
1125
754
  }
1126
755
  }
756
+
757
+ private func clearCompactBlockCache() async throws {
758
+ await stopAllActions()
759
+ try await storage.clear()
760
+ logger.info("Cache removed")
761
+ }
1127
762
  }
1128
763
 
1129
764
  extension CompactBlockProcessor {
@@ -1198,168 +833,17 @@ extension CompactBlockProcessor {
1198
833
  }
1199
834
  }
1200
835
 
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
- }
836
+ // MARK: - Config provider
1265
837
 
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.
1274
838
  extension CompactBlockProcessor {
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
839
+ actor ConfigProvider {
840
+ var config: Configuration
841
+ init(config: Configuration) {
842
+ self.config = config
1334
843
  }
1335
844
 
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
845
+ func update(config: Configuration) async {
846
+ self.config = config
1355
847
  }
1356
-
1357
- guard
1358
- let latestScannedHeight = self.latestScannedHeight,
1359
- let latestDownloadedHeight = self.latestDownloadedBlockHeight,
1360
- latestScannedHeight > latestDownloadedHeight
1361
- else { return nil }
1362
-
1363
- return latestScannedHeight
1364
848
  }
1365
849
  }