Repository navigation
fix: keep wallet backups consistent - #950
ben-kaufman wants to merge 2 commits into
Conversation
[Medium impact] The PR appears safe to merge; no actionable issues were established. SummaryThis PR checks that wallet state stays consistent while a backup is captured and keeps newer changes pending after upload.
No actionable issues were established. Native tests were not run during this review.
Diagram%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Wallet backup requested] --> B{Backup already running?}
B -->|Yes| C[Return busy; scheduled wallet work stays pending]
B -->|No| D[Record current backup requirement]
D --> E[Export SDK state and version under operation lock]
E --> F[Read remaining wallet state]
F --> G{Identity and state still match?}
G -->|State changed| H{Three attempts used?}
H -->|No| E
H -->|Yes| I[Keep requirement pending for scheduled follow-up]
G -->|Identity changed or other error| J[Stop without acknowledging backup]
G -->|Yes| K[Check cancellation and restore or wipe state]
K --> L[Upload snapshot]
L --> M[Acknowledge only the starting requirement]
M --> N{Newer changes pending?}
N -->|Yes| I
N -->|No| O[Backup complete]
Reviews (1) · Last reviewed commit: "fix: keep wallet backups consistent duri..." · Reviewed by Greptile |
talosmachina
left a comment
There was a problem hiding this comment.
1 finding (1 P1). Pairs the Paykit SDK export with its version, recaptures changing wallet snapshots up to three times, and acknowledges only the backup requirement that was captured, so changes during capture or upload stay pending. Reviewed cccba95, full tier.
What I checked, and 4 candidates I ruled out
Read in full: BackupService.swift (the scheduling, status and wallet capture paths), PrivatePaykitService+Backup.swift, the changed PaykitSdkService hunks and PaykitSdkOperationLock.withLock, BackupItemStatus.swift, the backup snapshot and change publishers of every store WalletBackupChangeTracker merges
Call sites traced: triggerBackup (scheduled loop, BackupViewModel, rewriteMigratedBackups), persistWalletBackup (runBackup, UsdtVssBackup), scheduleFullBackup, exportBackupState
CI: Run Tests, Run Integration Tests and build-local still pending when this was posted; validate green. iOS does not build on this machine, so this review is reasoned from the code and CI is the gate.
Ruled out
- Mixed wallet snapshot slips through: every store read in the capture closure emits on one of the merged publishers on save, and the SDK side is checked again under the operation lock via
backupStateVersion, so a change between reads fails the version comparison. - Lock-order inversion in
validateBackupSnapshot: it takes only the SDK operation lock after the proofmutationLockhas been released, matching the comment. status.required - 1underflow:runBackupsetsrequired = max(..., synced + 1, ...)at start andmarkBackupRequirednever lowers it, sorequired >= 1on the success path.- Wallet follow-up lost while a manual backup is busy:
.busykeeps the scheduled loop alive and the trailing reschedule covers.completed, matchingtestScheduledBackupRetainsFollowupWhileManualUploadIsBusy. - Cold reader: the second-model check of the P1 did not return before this was posted.
Merge confidence: 3/5, the P1 on non-wallet backup scheduling.
talosmachina
left a comment
There was a problem hiding this comment.
Re-review of 0387da3 after cccba95. 1 earlier finding: 1 fixed. 0 new findings. Full tier on the diff since my last review (BackupService.swift, BackupServiceTests.swift, the journey note).
What I checked
Earlier finding
- Non-wallet categories stranded when a change lands mid-upload: fixed.
runScheduledBackupno longer gates the retry break or the trailingscheduleBackupon.wallet(BackupService.swift:690,:696). A non-wallet change that arrives during an upload now leavessynced < required, the loop'scanRetryPendingBackupcheck keeps the job alive for.completedand.busy, and the post-loop re-check catches a change whose ownscheduleBackupreturned at thebackupJobs[category] == nilguard.
Ruled out in the new hunks
- Retry loop on a genuine failure:
.failedis still not incanRetryPendingBackup, so an export or upload error leaves the loop and nothing reschedules it. - Tight loop while a manual backup is busy: every iteration waits
waitForBackupDelay()first, the same as the wallet path before this push. lightningConnectionsleft out of the new test: nothing callsmarkBackupRequiredfor it; its status is only written directly atBackupService.swift:601, and the sameallCases where != .lightningConnectionsfilter already exists at:742.
Tests: testScheduledBackupUploadsChangesFromEveryCategory loops over every scheduled category, and the scheduled/manual overlap test now covers .settings as well as .wallet.
CI: Run Tests, Run Integration Tests and build-local still pending when this was posted; validate green. iOS does not build on this machine, so this re-review is reasoned from the code and CI is the gate.
Merge confidence: 4/5, 0 open findings; held at 4 until Run Tests and Run Integration Tests finish green on 0387da3, and the device restore checks in the PR body are still unchecked.
Closes #951
Fixes the iOS counterpart of synonymdev/bitkit-android#1487. Android fix: synonymdev/bitkit-android#1507.
This PR keeps wallet backups consistent during Paykit activity and preserves backup requirements for changes made while a backup is running.
Description
Out of Scope
Design
N/A - no UI changes.
Preview
N/A - no UI changes.
QA Notes
Journeys
N/A - not drivable; see Manual Tests.
Manual Tests
wallet-snapshot-consistency.mdfor the shared platform checks.Automated Checks
BackupServiceTests.swift- covers mixed-state recapture, bounded contention, capture/upload watermarks, automatic follow-up across all six uploaded categories, scheduled/manual overlap, cancellation and genuine failures without repeated automatic attempts.PaykitSdkOperationLockTests.swift- verifies queued mutations are included in the paired export/version, later mutations reject stale capture, and identity fencing survives recapture.Final local validation: all 10 focused
BackupServiceTestsand the 2,612-test native suite passed using the repository CI unit-test exclusions, with zero failures and no missing baseline tests. Simulator test builds passed. Complete format diagnostics retain the same 58 findings in 13 unchanged files as the verified merge-base baseline, none in changed files. All 16 emitted compiler diagnostics match that baseline, with none introduced. The complete translation output remains identical: 139 warnings, zero errors. Independent final correctness/native/polish review found no remaining actionable issues.The capture/upload requirement regression failed on the original implementation. Controlled tests also verified mixed-state rejection, busy/manual scheduling and cancellation precedence. All five non-wallet categories and the settings manual-upload overlap failed before the all-category scheduler correction, then passed with it. Eleven native tests were added across this PR. Device fault injection and restore of a captured backup envelope remain unrun; the original Android mainnet delay is not reproduced by this coverage.
Fresh follow-up validation on
0387da32: 16 focused native tests passed, including backup scheduling, paired SDK export/version, identity fencing and populated native restore fixtures. Complete format and translation reports still match the verified baseline. A normal unfunded simulator wallet uploaded and exported all six categories; a subsequent settings change also uploaded and exported correctly. The wallet envelope was empty of Paykit/payment state, so this is an ordinary backup smoke check, not completion of the controlled manual cases above.All required checks now pass on current head
0387da32, including unit, integration, translation, E2E build, the E2E device job (116723967976) ande2e-status(116742893487). This does not complete the controlled manual fault-injection or populated device-restore cases above.