Skip to content

Commit

Permalink
Merge pull request #224 from synonymdev/force-close-recovery-script
Browse files Browse the repository at this point in the history
Retry spending outputs after force close
  • Loading branch information
Jasonvdb authored Mar 11, 2024
2 parents 12f4da1 + bc61b29 commit eff283d
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 9 deletions.
61 changes: 61 additions & 0 deletions lib/android/src/main/java/com/reactnativeldk/LdkModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions lib/ios/Ldk.m
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
82 changes: 73 additions & 9 deletions lib/ios/Ldk.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -495,7 +495,7 @@ class Ldk: NSObject {
currentBlockchainTipHash = blockHash
currentBlockchainHeight = blockHeight
addForegroundObserver()

return handleResolve(resolve, .channel_manager_init_success)
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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 {
Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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))
}

Expand Down Expand Up @@ -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)
Expand All @@ -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))
Expand Down
20 changes: 20 additions & 0 deletions lib/src/ldk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
TBackedUpFileList,
TDownloadScorer,
TInitKeysManager,
TSpendRecoveredForceCloseOutputsReq,
} from './utils/types';
import { extractPaymentRequest } from './utils/helpers';

Expand Down Expand Up @@ -1209,6 +1210,25 @@ class LDK {
}
}

async spendRecoveredForceCloseOutputs({
transaction,
confirmationHeight,
changeDestinationScript,
}: TSpendRecoveredForceCloseOutputsReq): Promise<Result<string[]>> {
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.
Expand Down
44 changes: 44 additions & 0 deletions lib/src/lightning-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1526,6 +1526,50 @@ class LightningManager {
return results;
}

async recoverOutputsFromForceClose(): Promise<Result<string>> {
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.
Expand Down
6 changes: 6 additions & 0 deletions lib/src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit eff283d

Please sign in to comment.