Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,37 +22,65 @@ class LdkPersister {
val file = File(LdkModule.channelStoragePath + "/" + channelId + ".bin")

val isNew = !file.exists()
val serialized = data.write()

// If we're not remotely backing up, write locally and return
if (BackupClient.skipRemoteBackup) {
file.writeBytes(data.write())
file.writeBytes(serialized)
if (isNew) {
LdkEventEmitter.send(EventTypes.new_channel, body)
}
return ChannelMonitorUpdateStatus.LDKChannelMonitorUpdateStatus_Completed
}

BackupClient.addToPersistQueue(BackupClient.Label.CHANNEL_MONITOR(channelId=channelId), data.write()) { error ->
// For new channels: write locally first to prevent loss if app is killed during backup
// Then try remote backup asynchronously
if (isNew) {
file.writeBytes(serialized)

// Update chain monitor on main thread
LdkModule.reactContext?.runOnUiThread {
val res = LdkModule.chainMonitor?.channel_monitor_updated(channelFundingOutpoint, data._latest_update_id)
if (res == null || !res.is_ok) {
LdkEventEmitter.send(EventTypes.native_log, "Failed to update chain monitor with persisted channel (${channelId})")
} else {
LdkEventEmitter.send(EventTypes.native_log, "Persisted channel (${channelId}) to disk")
LdkEventEmitter.send(EventTypes.new_channel, body)
}
}

// Kick off remote backup asynchronously (non-blocking)
BackupClient.addToPersistQueue(BackupClient.Label.CHANNEL_MONITOR(channelId=channelId), serialized) { error ->
if (error != null) {
LdkEventEmitter.send(EventTypes.native_log, "Warning. Remote backup failed for new channel (${channelId}), but channel was persisted locally. $error")
}
}

return ChannelMonitorUpdateStatus.LDKChannelMonitorUpdateStatus_Completed
}

// For updates: try remote backup first, then write locally on success (original behavior)
BackupClient.addToPersistQueue(BackupClient.Label.CHANNEL_MONITOR(channelId=channelId), serialized) { error ->
if (error != null) {
LdkEventEmitter.send(EventTypes.native_log, "Failed to persist channel (${channelId}) to remote backup: $error")
return@addToPersistQueue
}

try {
file.writeBytes(data.write())
file.writeBytes(serialized)
} catch (e: Exception) {
//If this fails we can't do much but LDK will retry on startup
LdkEventEmitter.send(EventTypes.native_log, "Failed to locally persist channel (${channelId}) to disk")
return@addToPersistQueue
}

//Update chain monitor with successful persist
val res = LdkModule.chainMonitor?.channel_monitor_updated(channelFundingOutpoint, data._latest_update_id)
if (res == null || !res.is_ok) {
LdkEventEmitter.send(EventTypes.native_log, "Failed to update chain monitor with persisted channel (${channelId})")
} else {
LdkEventEmitter.send(EventTypes.native_log, "Persisted channel (${channelId}) to disk")
if (isNew) {
LdkEventEmitter.send(EventTypes.new_channel, body)
//Update chain monitor with successful persist on main thread
LdkModule.reactContext?.runOnUiThread {
val res = LdkModule.chainMonitor?.channel_monitor_updated(channelFundingOutpoint, data._latest_update_id)
if (res == null || !res.is_ok) {
LdkEventEmitter.send(EventTypes.native_log, "Failed to update chain monitor with persisted channel (${channelId})")
} else {
LdkEventEmitter.send(EventTypes.native_log, "Persisted channel (${channelId}) to disk")
}
}
}
Expand Down
56 changes: 43 additions & 13 deletions lib/ios/Classes/LdkPersist.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,39 +27,69 @@ class LdkPersister: Persist {
}

let isNew = !FileManager().fileExists(atPath: channelStoragePath.path)
let serialized = Data(data.write())

// If we're not remotely backing up no need to update status later
if BackupClient.skipRemoteBackup {
try Data(data.write()).write(to: channelStoragePath)
try serialized.write(to: channelStoragePath)
if isNew {
LdkEventEmitter.shared.send(withEvent: .new_channel, body: body)
}
return ChannelMonitorUpdateStatus.Completed
}

BackupClient.addToPersistQueue(.channelMonitor(id: channelIdHex), data.write()) { error in

// For new channels: write locally first to prevent loss if app is killed during backup
// Then try remote backup asynchronously
if isNew {
try serialized.write(to: channelStoragePath)

// Notify chain monitor on main thread
Copy link
Contributor

@Jasonvdb Jasonvdb Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't need to call channelMonitorUpdated on main thread. Not sure it matters if you do but it shouldn't be required.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If main thread is needed then do the same below for the other call to channelMonitorUpdated

Copy link
Contributor Author

@pwltr pwltr Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The threading wrapper is indeed necessary, after another test. It only coincidentally works in the persistQueue because of the timing not conflicting with LDK's read-write lock, I guess. cc @ovitrif

Short write-up by the agent:

The channelMonitorUpdated() method must be called on the main thread due to LDK's internal thread safety requirements.

Crash occurred in std::sys_common::rwlock::MovableRwLock::read when called from a background thread. LDK's ChainMonitor uses internal read-write locks that are not thread-safe when accessed from background threads.

Call Stack:

Thread 13 (crashed):
├── LdkPersister.handleChannel() 
├── channelMonitorUpdated() 
├── LDK internal ChainMonitor code
└── Read-write lock panic

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the other call to channelMonitorUpdated() to also run on the main thread, as suggested.

DispatchQueue.main.async {
let res = Ldk.chainMonitor?.channelMonitorUpdated(
fundingTxo: channelFundingOutpoint,
completedUpdateId: data.getLatestUpdateId()
)
if let error = res?.getError() {
LdkEventEmitter.shared.send(withEvent: .native_log, body: "Error. Failed to update chain monitor for channel (\(channelIdHex)) Error \(error.getValueType()).")
} else {
LdkEventEmitter.shared.send(withEvent: .native_log, body: "Persisted channel \(channelIdHex). Update ID: \(data.getLatestUpdateId())")
LdkEventEmitter.shared.send(withEvent: .new_channel, body: body)
}
}

// Kick off remote backup asynchronously (non-blocking)
BackupClient.addToPersistQueue(.channelMonitor(id: channelIdHex), [UInt8](serialized)) { error in
if let error {
LdkEventEmitter.shared.send(withEvent: .native_log, body: "Warning. Remote backup failed for new channel (\(channelIdHex)), but channel was persisted locally. \(error.localizedDescription)")
}
}

return ChannelMonitorUpdateStatus.Completed
}

// For updates: try remote backup first, then write locally on success (original behavior)
BackupClient.addToPersistQueue(.channelMonitor(id: channelIdHex), [UInt8](serialized)) { error in
if let error {
LdkEventEmitter.shared.send(withEvent: .native_log, body: "Error. Failed persist channel on remote server (\(channelIdHex)). \(error.localizedDescription)")
return
}

// Callback for when the persist queue queue entry is processed
// Callback for when the persist queue entry is processed
do {
try Data(data.write()).write(to: channelStoragePath)
try serialized.write(to: channelStoragePath)
} catch {
// If this fails we can't do much but LDK will retry on startup
LdkEventEmitter.shared.send(withEvent: .native_log, body: "Error. Failed to locally persist channel (\(channelIdHex)). \(error.localizedDescription)")
return
}

// Update chainmonitor with successful persist
let res = Ldk.chainMonitor?.channelMonitorUpdated(fundingTxo: channelFundingOutpoint, completedUpdateId: data.getLatestUpdateId())
if let error = res?.getError() {
LdkEventEmitter.shared.send(withEvent: .native_log, body: "Error. Failed to update chain monitor for channel (\(channelIdHex)) Error \(error.getValueType()).")
} else {
LdkEventEmitter.shared.send(withEvent: .native_log, body: "Persisted channel \(channelIdHex). Update ID: \(data.getLatestUpdateId())")
if isNew {
LdkEventEmitter.shared.send(withEvent: .new_channel, body: body)
// Update chainmonitor with successful persist on main thread
DispatchQueue.main.async {
let res = Ldk.chainMonitor?.channelMonitorUpdated(fundingTxo: channelFundingOutpoint, completedUpdateId: data.getLatestUpdateId())
if let error = res?.getError() {
LdkEventEmitter.shared.send(withEvent: .native_log, body: "Error. Failed to update chain monitor for channel (\(channelIdHex)) Error \(error.getValueType()).")
} else {
LdkEventEmitter.shared.send(withEvent: .native_log, body: "Persisted channel \(channelIdHex). Update ID: \(data.getLatestUpdateId())")
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion lib/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@synonymdev/react-native-ldk",
"title": "React Native LDK",
"version": "0.0.160",
"version": "0.0.161",
"description": "React Native wrapper for LDK",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down
Loading