Repository navigation
fix: keep rn channels on restore - #1396
Conversation
A seed restore was opening the node before the React Native channel backup finished, and a late channel file or early tag was discarded. Co-authored-by: Cursor <cursoragent@cursor.com>
A failed restart after stopping the node was only logged, so migration continued with the node down and no warning. Co-authored-by: Cursor <cursoragent@cursor.com>
A failed stop left the node running, and the following start could succeed without loading the channel file and then discard it. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
|
Regtest APKDownload bitkit-dev-debug universal APK (expires in 30 days). |
This comment has been minimized.
This comment has been minimized.
talosmachina
left a comment
There was a problem hiding this comment.
No findings. Waits out an RN remote restore before starting the node, applies the channel file on the post-migration restart instead of discarding it, and retries tags whose activity is not there yet without holding the migration open. Reviewed 824ab61, full tier, from the code and CI: this reviewer does not build Android branches.
What I checked, and 5 candidates I ruled out
Read in full: the changed hunks of MigrationService.kt with restoreFromRNRemoteBackup, reapplyMetadataAfterSync, the cleanup gate and the persistence helpers; handleSyncCompleted, retryPendingMigrationData, completeMigration, completeRNRemoteBackupRestore, applyPendingChannelMigration and the finish paths in AppViewModel.kt; start, waitForRestoreIfNeeded, restoreFromBackup and startNode in WalletViewModel.kt
Call sites traced: peekPendingChannelMigration, consumePendingChannelMigration, lockChannelMigration, canCleanupAfterMigration, needsPostMigrationSync
CI: build and lint still pending on 824ab61 at review time
Ruled out
- An orphan tag keeps
needsPostMigrationSyncset forever: true at83c8d60, fixed by824ab61, which clears the flag after the first pass in every completion path and leaves leftover tags toretryPendingMigrationData. That path skips the sweep and the node restart, asbackground tag retry does not sweep new activities or restart the nodechecks. - Unbounded wait in
waitForRestoreIfNeeded:restoreFromBackupalways leavesInProgress(CompletedorBackupFailed, both outsideisOngoing), so the wait ends with the restore. - Channel file applied twice:
startNodeconsumes it on a successful start under the same mutex, andapplyPendingChannelMigrationre-reads it after taking the lock, so it returns early. Checked bymigration completion rechecks pending channels after startup releases the lock. completeMigrationdrops the file on error: the old unconditionalconsumePendingChannelMigration()is gone and a failed restart keeps it, aspending channel migration is kept when the node stays running after stopchecks.- Cleanup with a pending channel file: the new
peekPendingChannelMigration()check incanCleanupAfterMigrationholds cleanup back.
Merge confidence: 4/5, no findings, but CI was still running and the channel-restart path is covered by unit tests only, not by a passing rn_restore migration run.
|
@greptileai Please re-review the latest head 824ab61. Migration completion now clears its one-time flag even when metadata remains pending; later syncs retry metadata without sweeping activities or suppressing new receive notices. Wallet startup and channel application share a lock, with pending channel data read after acquiring it. Regression tests, compilation and lint passed locally. |
|
@greptileai Please re-review latest head 10f1435. The late transfer/boost finding is now fixed with separately persisted marker retries, retention on missing activities or failed writes, and regression tests. Direct reply to the finding includes implementation and validation evidence. The twin iOS PR carries the same fix. |
jvsena42
left a comment
There was a problem hiding this comment.
One MEDIUM inline at 10f1435, and one LOW as a reply on the double-import thread. Both are reproduced by unit tests.
Checked and clean:
- The wait before node start is unbounded only while an RN remote restore is running, and it terminates: the restore always leaves the in-progress state, the lock is taken after the wait and released in
finally. - A repeated import cannot roll state back: ldk-node rc.66 never overwrites an existing channel manager and only writes a monitor with a higher update id. Read at the pinned tag.
applyPendingChannelMigrationconsumes the file only after a successful start, keeps it when the node will not stop, and unlocks infinally.- The first
SyncCompletedafter a remote restore takescompleteMigration, which clears both flags; later tag retries never mark activities as seen or restart the node. - The MMKV tag replay is gone and tags still arrive through the persisted metadata.
- No key material or channel bytes in the new log lines.
- For users who never migrated, the per-sync retry check returns before doing any work.
10f1435only changes how unapplied transfer and boost markers are retained inMigrationService.kt;WalletViewModel.ktandAppViewModel.ktare unchanged from what I reviewed.
Not posted: pending tags whose activity never appears are re-persisted on every sync with no cap. The retry itself is what the linked issue asks for; a guard that skips the write when nothing changed would remove the cost.
|
@greptileai Please review latest head 4b82c1d. New review follow-up fixes the two uncovered Android startup paths: install-on-top migration now holds the shared channel lock through consumption; restore Retry rebuilds with retained channel data under that lock and retains the backup on stop/start failures. Direct replies include evidence. Compilation, all 3644 unit tests, and lint passed. |
|
@talosmachina Please re-review latest head 4b82c1d. It includes retained transfer/boost markers from 10f1435 and now covers install-on-top startup plus restore Retry under the shared migration lock. Retry passes the channel backup before node startup, and failed stop/start retains it. All 3644 unit tests, compilation, and lint passed. Please confirm any remaining actionable findings on this exact head. |
talosmachina
left a comment
There was a problem hiding this comment.
Re-review of 4b82c1d after 824ab61. No findings. 0 earlier findings of mine to carry. The two commits since keep unapplied transfer and boost markers in the retry queue until their write succeeds. They also put install-on-top startup and restore Retry under the channel-migration lock, and Retry now builds the node with the pending channel file. Full tier, reasoned from the code and CI: this reviewer does not build Android branches.
What I checked, and 5 candidates I ruled out
Read in full: git diff 824ab61..4b82c1d. In MigrationService.kt: reapplyMetadataAfterSync, applyRemoteTransfers, applyBoostTransactions, the persist and clear helpers, canCleanupAfterMigration. In WalletViewModel.kt: checkAndPerformRNMigration, onRestoreRetry, start, startNode. In AppViewModel.kt: applyPendingChannelMigration, completeMigration, retryPendingMigrationData. In LightningRepo.kt: stop/stopLocked, startNode, restartNode
Call sites traced: lockChannelMigration (WalletViewModel.kt:184, :297, :355 and AppViewModel.kt:1940, nothing else), consumePendingChannelMigration, restartNode, onRestoreRetry (ContentView.kt:422, only from the ErrorStarting error view)
CI on 4b82c1d: build (including Run tests), build-local, lint and detekt green
@jvsena42's two threads
- Install-on-top start outside the lock: fixed.
checkAndPerformRNMigrationnow reads the file and starts the node while it holds the lock (WalletViewModel.kt:184-190), so aSyncCompletedemitted during that start waits. Once it has the lock it sees the file already consumed. - Retry starts without the channel file: fixed.
onRestoreRetryreads the retained file under the lock and runsstop()thenstartNode(0, channelMigration)instead ofrestartNode(). The file is consumed only instartNode'sonSuccess, so a failed stop or start leaves it pending. Covered by the threerestore retry ...tests.
Ruled out
- Deadlock on the new lock sites: the
Mutexis not reentrant, but nothing reached fromstartNode's success path (connectMigrationPeers,cleanupInvalidMigrationTransfers,syncBalances) takes it. LDK events are handled inviewModelScope.launchper event, so a completion waiting on the lock never blocks the start that holds it. - Retry's
stop()on a node that never built: after a failed setuplightningService.nodeis null, sostopLockedonly moves the state toStoppedand the start goes ahead. - Local markers re-applied after they landed: they are merged into the queue only while
needsPostMigrationSync()is true, and re-applying one is idempotent (isTransfer/boostTxIdsare set, not appended twice). - Lost boost on a partial CPFP write:
remaining.removesits after bothgetOrThrow()calls, so if either write fails the marker stays for the next sync.failed transfer and boost writes remain pendingcovers this. runCatchingswallowing cancellation in the boost writes: the switch torunSuspendCatchingrethrows it.
Merge confidence: 4/5. No findings and CI green, but the channel-restart path is still covered only by unit tests: the rn_restore migration run linked in the body did not pass.
|
@greptileai Please review latest head eab911c. The new background-stop finding is fixed by passing channel migration data through LightningRepo.restartNode, preserving its existing yield-to-stop behavior during bounded retries. Failed or deferred restarts retain the channel backup. Direct reply includes evidence; compilation, all 3647 unit tests, and lint passed. |
|
@talosmachina Please review latest head eab911c. All prior findings have fixes and direct replies. The latest follow-up routes channel-aware restore Retry through the existing lifecycle-aware restart path so background stops still win; install-on-top remains locked through channel consumption. Compilation, all 3647 unit tests, and lint passed. |
jvsena42
left a comment
There was a problem hiding this comment.
Approving at eab911c: both points from my last review are fixed and I have no new finding in the two commits since.
- Restore Retry and the install-on-top start now hold the channel-migration lock from the peek to the consume, and release it in
finally. - Lock order: every holder takes the migration lock before the lifecycle mutex and nothing takes them the other way round, so migration completion waiting on the lock cannot block the start that holds it.
- The retained file is consumed only after a successful start, on both consume sites.
- The background-stop point raised by the other review holds at head: Retry no longer calls
start(), a stop requested during the failed first attempt makes the retry yield, and one requested during the second attempt is not cancelled. restartNode()without a file behaves as before for its existing callers.- The new tests pin the fix: the Retry and install-on-top tests fail on the previous head.
Read only for this round; unit tests, lint and e2e are green in CI at this head.
talosmachina
left a comment
There was a problem hiding this comment.
Re-review of eab911c after 4b82c1d. No findings. 0 earlier findings of mine to carry. The one commit since routes a restore Retry that has a retained channel file through LightningRepo.restartNode(channelMigration), so the retry yields to a background stop instead of cancelling it. Full tier, reasoned from the code and CI: this reviewer does not build Android branches.
What I checked, and 4 candidates I ruled out
Read in full: git diff 4b82c1d..eab911c. In LightningRepo.kt: restartNode, start, the private startNode including its retry and graph-reset branches. In WalletViewModel.kt: onRestoreRetry, startNode
CI on eab911c: build, build-local, detekt, the eight e2e-tests-local shards and e2e-status green; the migration jobs were skipped
Ruled out
- Retry loses the channel file:
restartNodehandschannelMigrationto the first attempt and the bounded retry passes it again (LightningRepo.kt:484-493), so both attempts build the node with it. Covered bychannel migration restart keeps the backup across bounded start attempts. - Backup consumed on a deferred restart: a yield returns
NodeStartYieldedToStopErrorfromrestartNode, soWalletViewModel.startNodetakesonFailure, skipsconsumePendingChannelMigrationand shows no toast. Covered byrestore retry keeps pending channels when restart yields to background stop. - A stop requested during the retry gets cancelled: the retry runs with
shouldCancelPendingStop = !shouldRetryYieldToStop, which isfalseon this path. Covered bychannel migration restart retry does not cancel a stop requested during the retry. - Failed stop now unreported: the old inline
stop()failure toast is gone, butrestartNodereturns the stop failure asResult.failure, which reaches the sameLogger.errorandToastEventBus.sendinstartNode'sonFailure, and the file stays pending.
Merge confidence: 4/5, no findings and the new path is unit-tested, but the rn_restore migration scenario did not run on this head.
|
Scheduled the standard migration test matrix, including
I will inspect the results and investigate any failed scenarios. Slack reporting is disabled. |
|
Migration matrix passed on current app head Completed migration run. Job checkout logs confirm E2E Retry caveat: |
Fixes #1258
Twin: synonymdev/bitkit-ios#852
Companion: synonymdev/bitkit-e2e-tests#263
This PR keeps a React Native remote restore from opening the Lightning node before the channel backup arrives, and from throwing that backup away if the restart fails.
Description
Out of Scope
WalletViewModel: install-on-top and a normal VSS restore still use the 30s timeout.AppViewModel: the channel file is not persisted across process death.Design
N/A — no UI changes.
Preview
N/A
Migration runs
Runs of this change:
rn_restorestill failed in both. Android stopped on the receive celebration. iOS passed the first balance check and stopped on the QuickPay intro after relaunch. Those two are covered by the companion.QA Notes
Journeys
N/A — not drivable; see Manual Tests.
Manual Tests
Automated Checks
WalletViewModelTest.kt— an RN remote restore still running after 30s does not start the node; restore Retry uses the channel file and retains it on failed or deferred restarts; install-on-top startup holds the migration lock until consumptionLightningRepoTest.kt— channel-aware restarts yield to a background stop during a failed first attempt, preserve stops requested during the retry, and keep channel data across bounded startup attemptsMigrationServiceTest.kt— activity tags that are not ready yet are kept for a later syncAppViewModelSendFlowTest.kt— a channel file is kept when the node is still running after stop; background retries do not mark new activities seen; completion rechecks channel data after startup releases its lock