From ffe6e71a3333f52ee511ba19b67f1f7d48071470 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 6 Mar 2024 17:10:22 +0200 Subject: [PATCH 1/2] ios: recovery tool for unspent outputs from past force closes --- lib/ios/Ldk.m | 5 +++ lib/ios/Ldk.swift | 82 ++++++++++++++++++++++++++++++++---- lib/src/ldk.ts | 20 +++++++++ lib/src/lightning-manager.ts | 44 +++++++++++++++++++ lib/src/utils/types.ts | 6 +++ 5 files changed, 148 insertions(+), 9 deletions(-) diff --git a/lib/ios/Ldk.m b/lib/ios/Ldk.m index 0b3b2711..a8444df2 100644 --- a/lib/ios/Ldk.m +++ b/lib/ios/Ldk.m @@ -167,6 +167,11 @@ @interface RCT_EXTERN_MODULE(Ldk, NSObject) changeDestinationScript:(NSString *)changeDestinationScript resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(spendRecoveredForceCloseOutputs:(NSString *)transaction + confirmationHeight:(NSInteger *)confirmationHeight + changeDestinationScript:(NSString *)changeDestinationScript + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) RCT_EXTERN_METHOD(nodeSign:(NSString *)message resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) diff --git a/lib/ios/Ldk.swift b/lib/ios/Ldk.swift index 029df0e5..e0667477 100644 --- a/lib/ios/Ldk.swift +++ b/lib/ios/Ldk.swift @@ -427,7 +427,7 @@ class Ldk: NSObject { ) LdkEventEmitter.shared.send(withEvent: .native_log, body: "Enabled P2P gossip: \(enableP2PGossip)") - + // print("\(String(cString: strerror(22)))") let scoreParams = ProbabilisticScoringFeeParameters.initWithDefault() @@ -495,7 +495,7 @@ class Ldk: NSObject { currentBlockchainTipHash = blockHash currentBlockchainHeight = blockHeight addForegroundObserver() - + return handleResolve(resolve, .channel_manager_init_success) } @@ -538,7 +538,7 @@ class Ldk: NSObject { channelManager = nil peerManager = nil peerHandler = nil - + LdkEventEmitter.shared.send(withEvent: .native_log, body: "Starting LDK background tasks again") initChannelManager(currentNetwork, blockHash: currentBlockchainTipHash, blockHeight: currentBlockchainHeight) { success in //Notify JS that a sync is required @@ -1183,6 +1183,7 @@ class Ldk: NSObject { let output = TxOut(scriptPubkey: String(outputScriptPubKey).hexaBytes, value: UInt64(outputValue)) let outpoint = OutPoint(txidArg: String(outpointTxId).hexaBytes.reversed(), indexArg: UInt16(outpointIndex)) + let descriptor = SpendableOutputDescriptor.initWithStaticOutput(outpoint: outpoint, output: output) let res = keysManager.spendSpendableOutputs( @@ -1200,6 +1201,69 @@ class Ldk: NSObject { return resolve(Data(res.getValue()!).hexEncodedString()) } + @objc + func spendRecoveredForceCloseOutputs(_ transaction: NSString, confirmationHeight: NSInteger, changeDestinationScript: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + //TODO check which ones are not open channels and try spend them again + guard let channelStoragePath = Ldk.channelStoragePath, let keysManager, let channelManager else { + return handleReject(reject, .init_storage_path) + } + + let openChannelIds = channelManager.listChannels().map { Data($0.getChannelId() ?? []).hexEncodedString() }.filter { $0 != "" } + + let channelFiles = try! FileManager.default.contentsOfDirectory(at: channelStoragePath, includingPropertiesForKeys: nil) + + var txs: [String] = [] + + for channelFile in channelFiles { + let channelId = channelFile.lastPathComponent.replacingOccurrences(of: ".bin", with: "") + + //Ignore open channels + guard !openChannelIds.contains(channelId) else { + continue + } + + LdkEventEmitter.shared.send(withEvent: .native_log, body: "Loading channel from file to attempt sweep \(channelId)") + + let channelMonitorResult = Bindings.readThirtyTwoBytesChannelMonitor( + ser: [UInt8](try! Data(contentsOf: channelFile.standardizedFileURL)), + argA: keysManager.inner.asEntropySource(), + argB: keysManager.signerProvider + ) + + guard let (_, channelMonitor) = channelMonitorResult.getValue() else { + LdkEventEmitter.shared.send(withEvent: .native_log, body: "Loading channel error. No channel value.") + continue + } + + let descriptors = channelMonitor.getSpendableOutputs( + tx: String(transaction).hexaBytes, + confirmationHeight: UInt32(confirmationHeight) + ) + + guard descriptors.count > 0 else { + LdkEventEmitter.shared.send(withEvent: .native_log, body: "No spendable outputs found in \(channelId)") + continue + } + + let res = keysManager.spendSpendableOutputs( + descriptors: descriptors, + outputs: [], + changeDestinationScript: String(changeDestinationScript).hexaBytes, + feerateSatPer1000Weight: feeEstimator.getEstSatPer1000Weight(confirmationTarget: .OnChainSweep), + locktime: nil + ) + + guard res.isOk() else { + LdkEventEmitter.shared.send(withEvent: .native_log, body: "Failed to spend output from closed channel: \(channelId).") + continue + } + + txs.append(Data(res.getValue()!).hexEncodedString()) + } + + resolve(txs) + } + @objc func nodeSign(_ message: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { guard let keysManager = keysManager else { @@ -1244,7 +1308,7 @@ class Ldk: NSObject { if let syncTimestamp = channelManagerConstructor?.netGraph?.getLastRapidGossipSyncTimestamp() { let date = Date(timeIntervalSince1970: TimeInterval(syncTimestamp)) - + let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" @@ -1286,7 +1350,7 @@ class Ldk: NSObject { guard seedBytes.count == 32 else { return handleReject(reject, .invalid_seed_hex) } - + let seconds = UInt64(NSDate().timeIntervalSince1970) let nanoSeconds = UInt32.init(truncating: NSNumber(value: seconds * 1000 * 1000)) let keysManager = KeysManager( @@ -1328,7 +1392,7 @@ class Ldk: NSObject { guard let channelStoragePath = Ldk.channelStoragePath else { return handleReject(reject, .init_storage_path) } - + do { if !overwrite { let fileManager = FileManager.default @@ -1355,7 +1419,7 @@ class Ldk: NSObject { if let ldkFileName = LdkFileNames.allCases.first(where: { $0.rawValue.contains(key) }) { fileName = ldkFileName.rawValue } - + try file.value.write(to: accountStoragePath.appendingPathComponent(fileName)) } @@ -1411,7 +1475,7 @@ class Ldk: NSObject { } let bytes = [UInt8](data) - + BackupClient.addToPersistQueue(.misc(fileName: String(fileName)), bytes) { error in if let error { handleReject(reject, .backup_file_failed, error, error.localizedDescription) @@ -1426,7 +1490,7 @@ class Ldk: NSObject { guard !BackupClient.requiresSetup else { return handleReject(reject, .backup_setup_required) } - + do { let data = try BackupClient.retrieve(.misc(fileName: String(fileName))) resolve(String.init(data: data, encoding: .utf8)) diff --git a/lib/src/ldk.ts b/lib/src/ldk.ts index d9de28de..0fd5a2ca 100644 --- a/lib/src/ldk.ts +++ b/lib/src/ldk.ts @@ -37,6 +37,7 @@ import { TBackedUpFileList, TDownloadScorer, TInitKeysManager, + TSpendRecoveredForceCloseOutputsReq, } from './utils/types'; import { extractPaymentRequest } from './utils/helpers'; @@ -1209,6 +1210,25 @@ class LDK { } } + async spendRecoveredForceCloseOutputs({ + transaction, + confirmationHeight, + changeDestinationScript, + }: TSpendRecoveredForceCloseOutputsReq): Promise> { + try { + const res = await NativeLDK.spendRecoveredForceCloseOutputs( + transaction, + confirmationHeight, + changeDestinationScript, + ); + this.writeDebugToLog('spendRecoveredForceCloseOutputs', res); + return ok(res); + } catch (e) { + this.writeErrorToLog('spendRecoveredForceCloseOutputs', e); + return err(e); + } + } + /** * Creates a digital signature of a message the node's secret key. * A receiver knowing the PublicKey (e.g. the node's id) and the message can be sure that the signature was generated by the caller. diff --git a/lib/src/lightning-manager.ts b/lib/src/lightning-manager.ts index 8aacf1b2..96918ae3 100644 --- a/lib/src/lightning-manager.ts +++ b/lib/src/lightning-manager.ts @@ -1526,6 +1526,50 @@ class LightningManager { return results; } + async recoverOutputsFromForceClose(): Promise> { + const address = await this.getAddress(); + const changeDestinationScript = this.getChangeDestinationScript( + address.address, + ); + if (!changeDestinationScript) { + await ldk.writeToLogFile( + 'error', + 'Unable to retrieve change_destination_script.', + ); + return err('Unable to retrieve change_destination_script.'); + } + + const txs = await this.getLdkBroadcastedTxs(); + if (!txs.length) { + return ok('No outputs to reconstruct as no cached transactions found.'); + } + + let txsToBroadcast = 0; + for (const hexTx of txs) { + const tx = bitcoin.Transaction.fromHex(hexTx); + const txData = await this.getTransactionData(tx.getId()); + + const txsRes = await ldk.spendRecoveredForceCloseOutputs({ + transaction: hexTx, + confirmationHeight: txData?.height ?? 0, + changeDestinationScript, + }); + + if (txsRes.isErr()) { + await ldk.writeToLogFile('error', txsRes.error.message); + console.error(txsRes.error.message); + continue; + } + + for (const createdTx of txsRes.value) { + txsToBroadcast++; + await this.broadcastTransaction(createdTx); + } + } + + return ok(`Attempting to reconstruct ${txsToBroadcast} transactions.`); + } + /** * Attempts to recover outputs from stored spendable outputs. * Also attempts to recreate outputs that were not previously stored but failed to be spent. diff --git a/lib/src/utils/types.ts b/lib/src/utils/types.ts index 32894bf3..7aeb44db 100644 --- a/lib/src/utils/types.ts +++ b/lib/src/utils/types.ts @@ -592,6 +592,12 @@ export type TReconstructAndSpendOutputsReq = { changeDestinationScript: string; }; +export type TSpendRecoveredForceCloseOutputsReq = { + transaction: string; + confirmationHeight: number; + changeDestinationScript: string; +}; + export type TBackupServerDetails = { host: string; serverPubKey: string; From bc61b29f5b7bbb73b2d7e65bd4f78e6a47aa2166 Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 11 Mar 2024 17:45:48 +0200 Subject: [PATCH 2/2] android: recovery tool for unspent outputs from past force closes --- .../main/java/com/reactnativeldk/LdkModule.kt | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/lib/android/src/main/java/com/reactnativeldk/LdkModule.kt b/lib/android/src/main/java/com/reactnativeldk/LdkModule.kt index 983e1402..5d7f4816 100644 --- a/lib/android/src/main/java/com/reactnativeldk/LdkModule.kt +++ b/lib/android/src/main/java/com/reactnativeldk/LdkModule.kt @@ -14,6 +14,7 @@ import org.ldk.impl.bindings.get_ldk_version import org.ldk.structs.* import org.ldk.structs.Result_Bolt11InvoiceParseOrSemanticErrorZ.Result_Bolt11InvoiceParseOrSemanticErrorZ_OK import org.ldk.structs.Result_Bolt11InvoiceSignOrCreationErrorZ.Result_Bolt11InvoiceSignOrCreationErrorZ_OK +import org.ldk.structs.Result_C2Tuple_ThirtyTwoBytesChannelMonitorZDecodeErrorZ.Result_C2Tuple_ThirtyTwoBytesChannelMonitorZDecodeErrorZ_OK import org.ldk.structs.Result_PublicKeyNoneZ.Result_PublicKeyNoneZ_OK import org.ldk.structs.Result_StrSecp256k1ErrorZ.Result_StrSecp256k1ErrorZ_OK import org.ldk.util.UInt128 @@ -1109,6 +1110,66 @@ class LdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMod promise.resolve((res as Result_TransactionNoneZ.Result_TransactionNoneZ_OK).res.hexEncodedString()) } + @ReactMethod + fun spendRecoveredForceCloseOutputs(transaction: String, confirmationHeight: Double, changeDestinationScript: String, promise: Promise) { + if (channelStoragePath == "") { + return handleReject(promise, LdkErrors.init_storage_path) + } + + if (channelManager == null) { + return handleReject(promise, LdkErrors.init_channel_manager) + } + + if (keysManager == null) { + return handleReject(promise, LdkErrors.init_keys_manager) + } + + val openChannelIds = channelManager!!.list_channels().map { it._channel_id.hexEncodedString() } + + //Get list of files in this path + val channelFiles = File(channelStoragePath).listFiles() + + val txs = Arguments.createArray() + + for (channelFile in channelFiles) { + val channelId = channelFile.nameWithoutExtension + + //Ignore open channels + if (openChannelIds.contains(channelId)) { + continue + } + + LdkEventEmitter.send(EventTypes.native_log, "Loading channel from file to attempt sweep " + channelId) + + //byte[] ser, EntropySource arg_a, SignerProvider arg_b) + val channelMonitor = UtilMethods.C2Tuple_ThirtyTwoBytesChannelMonitorZ_read(channelFile.readBytes(), keysManager!!.inner.as_EntropySource(), SignerProvider.new_impl(keysManager!!.signerProvider)) + + if (channelMonitor.is_ok) { + val monitor = (channelMonitor as Result_C2Tuple_ThirtyTwoBytesChannelMonitorZDecodeErrorZ_OK).res._b + + val descriptors = monitor.get_spendable_outputs(transaction.hexa(), confirmationHeight.toInt()) + if (descriptors.isEmpty()) { + LdkEventEmitter.send(EventTypes.native_log, "No spendable outputs found for channel $channelId") + continue + } + + val res = keysManager!!.spend_spendable_outputs( + descriptors, + emptyArray(), + changeDestinationScript.hexa(), + feeEstimator.onChainSweep, + Option_u32Z.none() + ) + + if (res.is_ok) { + txs.pushHexString((res as Result_TransactionNoneZ.Result_TransactionNoneZ_OK).res) + } + } + } + + promise.resolve(txs) + } + @ReactMethod fun nodeSign(message: String, promise: Promise) { keysManager ?: return handleReject(promise, LdkErrors.init_keys_manager)