-
Notifications
You must be signed in to change notification settings - Fork 2
feat: one-time stale channel monitor recovery (v2) #501
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -9,6 +9,17 @@ | |||||||||||
| private var node: Node? | ||||||||||||
| var currentWalletIndex: Int = 0 | ||||||||||||
|
|
||||||||||||
| // MARK: - Stale monitor recovery (one-time recovery for channel monitor desync) | ||||||||||||
|
|
||||||||||||
| private static let staleMonitorRecoveryAttemptedKey = "staleMonitorRecoveryAttempted" | ||||||||||||
|
|
||||||||||||
| /// Whether we've already attempted stale monitor recovery (prevents infinite retry). | ||||||||||||
| /// Persisted so the retry only happens once, even across app restarts. | ||||||||||||
| private static var staleMonitorRecoveryAttempted: Bool { | ||||||||||||
| get { UserDefaults.standard.bool(forKey: staleMonitorRecoveryAttemptedKey) } | ||||||||||||
| set { UserDefaults.standard.set(newValue, forKey: staleMonitorRecoveryAttemptedKey) } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| private let syncStatusChangedSubject = PassthroughSubject<UInt64, Never>() | ||||||||||||
|
|
||||||||||||
| private var channelCache: [String: ChannelDetails] = [:] | ||||||||||||
|
|
@@ -124,22 +135,59 @@ | |||||||||||
| builder.setEntropyBip39Mnemonic(mnemonic: mnemonic, passphrase: passphrase) | ||||||||||||
|
|
||||||||||||
| try await ServiceQueue.background(.ldk) { | ||||||||||||
| if !lnurlAuthServerUrl.isEmpty { | ||||||||||||
| self.node = try builder.buildWithVssStore( | ||||||||||||
| vssUrl: vssUrl, | ||||||||||||
| storeId: storeId, | ||||||||||||
| lnurlAuthServerUrl: lnurlAuthServerUrl, | ||||||||||||
| fixedHeaders: [:] | ||||||||||||
| ) | ||||||||||||
| } else { | ||||||||||||
| self.node = try builder.buildWithVssStoreAndFixedHeaders( | ||||||||||||
| vssUrl: vssUrl, | ||||||||||||
| storeId: storeId, | ||||||||||||
| fixedHeaders: [:] | ||||||||||||
| do { | ||||||||||||
| if !lnurlAuthServerUrl.isEmpty { | ||||||||||||
| self.node = try builder.buildWithVssStore( | ||||||||||||
| vssUrl: vssUrl, | ||||||||||||
| storeId: storeId, | ||||||||||||
| lnurlAuthServerUrl: lnurlAuthServerUrl, | ||||||||||||
| fixedHeaders: [:] | ||||||||||||
| ) | ||||||||||||
| } else { | ||||||||||||
| self.node = try builder.buildWithVssStoreAndFixedHeaders( | ||||||||||||
| vssUrl: vssUrl, | ||||||||||||
| storeId: storeId, | ||||||||||||
| fixedHeaders: [:] | ||||||||||||
| ) | ||||||||||||
| } | ||||||||||||
| } catch let error as BuildError { | ||||||||||||
| guard case .ReadFailed = error, !Self.staleMonitorRecoveryAttempted else { | ||||||||||||
| throw error | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Build failed with ReadFailed — likely a stale ChannelMonitor (DangerousValue). | ||||||||||||
| // Retry once with accept_stale_channel_monitors to recover. | ||||||||||||
| Logger.warn( | ||||||||||||
| "Build failed with ReadFailed. Retrying with accept_stale_channel_monitors for one-time recovery.", | ||||||||||||
| context: "Recovery" | ||||||||||||
| ) | ||||||||||||
| Self.staleMonitorRecoveryAttempted = true | ||||||||||||
| builder.setAcceptStaleChannelMonitors(accept: true) | ||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing dependency: This call references bitkit-ios/Bitkit/Services/LightningService.swift Lines 163 to 167 in db59970
|
||||||||||||
|
|
||||||||||||
| if !lnurlAuthServerUrl.isEmpty { | ||||||||||||
| self.node = try builder.buildWithVssStore( | ||||||||||||
| vssUrl: vssUrl, | ||||||||||||
| storeId: storeId, | ||||||||||||
| lnurlAuthServerUrl: lnurlAuthServerUrl, | ||||||||||||
| fixedHeaders: [:] | ||||||||||||
| ) | ||||||||||||
| } else { | ||||||||||||
| self.node = try builder.buildWithVssStoreAndFixedHeaders( | ||||||||||||
| vssUrl: vssUrl, | ||||||||||||
| storeId: storeId, | ||||||||||||
| fixedHeaders: [:] | ||||||||||||
| ) | ||||||||||||
| } | ||||||||||||
| Logger.info("Stale monitor recovery: build succeeded with accept_stale", context: "Recovery") | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Mark recovery as attempted after any successful build (whether recovery was needed or not). | ||||||||||||
| // This ensures unaffected users never trigger the retry path on future startups. | ||||||||||||
| if !Self.staleMonitorRecoveryAttempted { | ||||||||||||
| Self.staleMonitorRecoveryAttempted = true | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| Logger.info("LDK node setup") | ||||||||||||
|
|
||||||||||||
| // Clear memory | ||||||||||||
|
|
@@ -595,7 +643,7 @@ | |||||||||||
| } | ||||||||||||
|
|
||||||||||||
| func closeChannel(_ channel: ChannelDetails, force: Bool = false, forceCloseReason: String? = nil) async throws { | ||||||||||||
| guard let node else { | ||||||||||||
| throw AppError(serviceError: .nodeNotStarted) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
|
|
@@ -1068,7 +1116,7 @@ | |||||||||||
| onEvent?(event) | ||||||||||||
|
|
||||||||||||
| switch event { | ||||||||||||
| case let .paymentSuccessful(paymentId, paymentHash, paymentPreimage, feePaidMsat): | ||||||||||||
| Logger.info("✅ Payment successful: paymentId: \(paymentId ?? "?") paymentHash: \(paymentHash) feePaidMsat: \(feePaidMsat ?? 0)") | ||||||||||||
| Task { | ||||||||||||
| let hash = paymentId ?? paymentHash | ||||||||||||
|
|
@@ -1093,7 +1141,7 @@ | |||||||||||
| Logger.warn("No paymentId or paymentHash available for failed payment", context: "LightningService") | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| case let .paymentReceived(paymentId, paymentHash, amountMsat, feePaidMsat): | ||||||||||||
| Logger.info("🤑 Payment received: paymentId: \(paymentId ?? "?") paymentHash: \(paymentHash) amountMsat: \(amountMsat)") | ||||||||||||
| Task { | ||||||||||||
| let hash = paymentId ?? paymentHash | ||||||||||||
|
|
@@ -1103,7 +1151,7 @@ | |||||||||||
| Logger.error("Failed to handle payment received for \(hash): \(error)", context: "LightningService") | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| case let .paymentClaimable(paymentId, paymentHash, claimableAmountMsat, claimDeadline, customRecords): | ||||||||||||
|
Check warning on line 1154 in Bitkit/Services/LightningService.swift
|
||||||||||||
| Logger.info( | ||||||||||||
| "🫰 Payment claimable: paymentId: \(paymentId) paymentHash: \(paymentHash) claimableAmountMsat: \(claimableAmountMsat)" | ||||||||||||
| ) | ||||||||||||
|
|
@@ -1132,7 +1180,7 @@ | |||||||||||
|
|
||||||||||||
| if let channel { | ||||||||||||
| await registerClosedChannel(channel: channel, reason: reasonString) | ||||||||||||
| await MainActor.run { | ||||||||||||
| channelCache.removeValue(forKey: channelIdString) | ||||||||||||
| } | ||||||||||||
| } else { | ||||||||||||
|
|
@@ -1155,7 +1203,7 @@ | |||||||||||
| Logger.error("Failed to handle transaction received for \(txid): \(error)", context: "LightningService") | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| case let .onchainTransactionConfirmed(txid, blockHash, blockHeight, confirmationTime, details): | ||||||||||||
|
Check warning on line 1206 in Bitkit/Services/LightningService.swift
|
||||||||||||
| Logger.info("✅ Onchain transaction confirmed: txid=\(txid) blockHeight=\(blockHeight) amountSats=\(details.amountSats)") | ||||||||||||
| Task { | ||||||||||||
| do { | ||||||||||||
|
|
@@ -1209,7 +1257,7 @@ | |||||||||||
|
|
||||||||||||
| // MARK: Balance Events | ||||||||||||
|
|
||||||||||||
| case let .balanceChanged(oldSpendableOnchain, newSpendableOnchain, oldTotalOnchain, newTotalOnchain, oldLightning, newLightning): | ||||||||||||
|
Check warning on line 1260 in Bitkit/Services/LightningService.swift
|
||||||||||||
| Logger | ||||||||||||
| .info("💰 Balance changed: onchain=\(oldSpendableOnchain)->\(newSpendableOnchain) lightning=\(oldLightning)->\(newLightning)") | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace with specific exception