Repository navigation
fix: bind profile removal to its original owner - #1143
ben-kaufman wants to merge 11 commits into
Conversation
|
Placed in the 3.1.0 milestone: its Android twin synonymdev/bitkit-android#1693 and its parent in the stack, #1125, are both in 3.1.0. This PR closes no issue, so it carries the milestone itself. |
talosmachina
left a comment
There was a problem hiding this comment.
No findings in the paths I reviewed. Binds profile removal, Disconnect and wallet wipe to a reserved admission scope with a durable profile-removal-v1.json obligation, makes credential, keychain and preference cleanup acknowledged rather than best effort, and gates wallet startup on an unresolved wipe. Reviewed 60b1485b, oversized tier (76 files, +7,577/-2,360, stacked on #1125 at 6c0a1607).
What I checked, and what I did not
Read in full: ProfileOperationAdmission.swift, ProfileRemovalStore.swift, ProfileRemovalRecoveryView.swift, AppReset.swift
Read as diff plus callers: Keychain.swift, SharedPubkyKeychain.swift, PrivatePaykitAddressReservationStore.swift, BackupService.swift, PrivatePaykitService+Backup.swift, PrivatePaykitService+State.swift, the removal flow in PubkyProfileManager.swift (prepareRemoval, confirmRemoval, finishPendingRemoval, runRemoval, clearLocalState), the gating in AppScene.swift and MainNavView.swift, and the ContactsManager removal/import paths
Call sites traced: every AppReset.wipe( caller, every admission.capture() / scheduleAfterTransition / reserveSessionTransition site, contactPublicKeyForAttribution
CI: SwiftFormat green on this head; Run Tests pending at review time; e2e jobs skipped
Ruled out
- Wipe marker deleted by its own wipe: the marker lives at
<network>/wallet0/profile-removal-v1.json, whilewipeStorageremoves onlywallet0/ldkandwipeAllDatabaseswipes the core DB tables in place, so the obligation survives untiladmission.completeremoves it. - Wallet wipe downgraded to profile recovery:
ProfileRemovalStore.beginrefuses a.profilerecord over a pending.walletWipeone. - Keychain wipe deletes other apps' or test-run credentials: enumeration is scoped to
Env.keychainGroupwith the unit-test prefix guard kept, and shared-group deletion is filtered to theownSourceApp:prefix. - Unreadable reservation ledger silently replaced:
clearContactAssignmentsForRemovalthrows when the stored ledger was unreadable or differs from memory, so local completion fails closed instead of overwriting it. - Wallet starts while a wipe is pending:
setupTask,handleWalletExistsChangeandstartWalletall return early onblocksWalletStartup, andmainContentswaps in the recovery view.
Not reviewed: the remaining payment, invoice, contacts and SDK changes in PubkyService.swift, PrivatePaykitService+Payments.swift, PrivatePaykitService+Contacts.swift, ContactPaymentsService.swift, the view changes outside recovery, the tests and the translations. At this size a line-by-line review of the whole diff is not reliable.
Not built: Swift does not build on this reviewer's Linux host, so this review is static and relies on CI for compilation and tests.
Merge confidence: 3/5, oversized tier, so the remainder above was not read.
Reviewer model: claude-opus-5-5
Reasoning effort: Not exposed (the review run's metadata does not report it)
There was a problem hiding this comment.
Verdict:
Findings:
- MEDIUM — Preserve incoming payment attribution during cleanup (
Bitkit/Services/PrivatePaykitService+Payments.swift:35): a private Lightning invoice paid while removal is reserved or recovery is pending is saved without its contact because the local lookup returns nil. Local Disconnect clears the invoice mapping before a later sync can recover it. The same admission gate blocks reserved-address attribution for on-chain receipts, and completion clears the assignment history.
QA for 60b1485:
Test 1: Controlled local recovery ✅ The isolated UI fixture retained its marker and watermark through failure and cancellation, then cleared them after explicit completion; XCTest assertions checked the fixture state.
recovery-retry-2x.2x.mp4
Test 2: J1 profile removal recovery ⏭️ The complete authenticated journey was not driven; scoped lost-acknowledgement and replacement controls were unavailable.
Test 3: J2 profile cleanup failure ⏭️ The complete authenticated journey was not driven; the required scoped withdrawal, contact and profile fault controls were unavailable.
Test 4: Manual 1 teardown ownership ⏭️ The full installed replacement and wipe interleavings were not driven; the PR reports scheduler-component coverage.
Test 5: Manual 2 durable cleanup failures ⏭️ The full credential and storage failure and restart boundaries were not driven; Test 1 uses controlled fixture state.
Check 1: Hosted tests ⚠️ The current-head hosted run failed two unit tests; both passed in a focused local repeat.
Hosted test run failed ContactPaymentsServiceTests.testContactPaymentsEnableWinsOverAnOlderDisableStillRemoving and ProfileDestinationViewTests.testCreateWaitsForTheOriginalTransitionReleaseWithoutAnotherBusyEvent. The repeat does not establish why CI failed.
The review covered the full PR range 6c0a160..60b1485, with targeted tracing of ownership, cleanup and receipt attribution; the complete diff was not reviewed line by line. No device case drove payment attribution or the authenticated fault journeys. The earlier source review covers additional cleanup paths.
Pair PR synonymdev/bitkit-android#1693 implements the same ownership and recovery goal; Android additionally offers Review, while iOS offers local Disconnect only.
Reviewer model: GPT-6
Reasoning effort: Not exposed (run metadata did not expose it).
Reviewed by gpt-6.1-terra-high via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest
jvsena42
left a comment
There was a problem hiding this comment.
Code scan: one MEDIUM, inline at PubkyProfileManager.swift:1560. It is the iOS form of the MEDIUM I raised on synonymdev/bitkit-android#1693: if Disconnect fails partway, it can't be retried, and the only way out removes the profile on this device alone.
Checked and clean:
- A profile switch can't make removal apply to the wrong identity.
- A restart doesn't replay the removal automatically.
- A re-run of the recovery wipe tolerates the database and keychain state it finds.
- PIN and startup gating hold.
- The DEBUG fault selector doesn't reach release builds.
Not raised as a finding: an unreadable reservation ledger blocks a local Disconnect until a wallet reset. That fail-closed behaviour is intended and tested here, and only external corruption of the ledger can reach it.
Device gate: waits for the MEDIUM.
| try validateRemovalOwner(plan) | ||
| } | ||
| try validateLocalIntent?() | ||
| try admission.persist(scope) |
There was a problem hiding this comment.
MEDIUM: a dropped connection during Disconnect makes remote cleanup unretryable.
runRemoval persists the removal marker here before withdrawPrivate()/withdrawPublic()/revokeSession(). If a withdrawal throws, the session and credentials stay, but prepareRemoval → admission.snapshot() → requireNoPending() now throws .pending, and reserve(kind: .removal) requires pending == nil (ProfileOperationAdmission.swift:481). The only action left is the recovery view's local Disconnect (finishPendingRemoval → .localRecovery). It clears the credentials and the publishing and cleanup-pending flags without revoking the homeserver session or withdrawing the published public/private endpoints, and nothing reconciles them afterwards. Reset wallet in this state is localOnly too (:505), so AppReset.wipeLocal skips the endpoint removal. The base branch kept this retryable: a failed signOut kept the identity and marked reconciliation pending. Same root as synonymdev/bitkit-android#1693 (ProfileRemovalCoordinator.kt:66).
Narrow fix: for .disconnect with withdrawEndpoints, call admission.persist(scope) (plus failedRemovalID/refreshRemovalRecovery()) just before revokeSession() instead of before the withdrawals. A withdrawal failure then leaves no marker and the user can Disconnect again. An uncertain revoke still keeps the obligation. Regression test: testTransientWithdrawalFailureKeepsDisconnectRetryableForLiveSession in ProfileRemovalCoordinatorTests.swift (below). It fails on this head with refused: pending and passes with the move.
Reproduction
diff --git a/BitkitTests/ProfileRemovalCoordinatorTests.swift b/BitkitTests/ProfileRemovalCoordinatorTests.swift
index 8b8a0ff4..ebbf231f 100644
--- a/BitkitTests/ProfileRemovalCoordinatorTests.swift
+++ b/BitkitTests/ProfileRemovalCoordinatorTests.swift
@@ -546,6 +546,26 @@ final class ProfileRemovalCoordinatorTests: XCTestCase {
XCTAssertNil(try store.pending())
}
+ func testTransientWithdrawalFailureKeepsDisconnectRetryableForLiveSession() async throws {
+ failStep = "private"
+ let manager = makeManager()
+ let first = try await manager.prepareRemoval(action: .disconnect)
+ do { _ = try await manager.confirmRemoval(first); XCTFail("Withdrawal failed") }
+ catch TestError.failure("private") {}
+ XCTAssertEqual(manager.publicKey, owner)
+ XCTAssertEqual(watermark, 42)
+ failStep = nil
+ let retry: PubkyProfileManager.RemovalConfirmation
+ do { retry = try await manager.prepareRemoval(action: .disconnect) }
+ catch {
+ XCTFail("Disconnect retry for the still-signed-in owner was refused: \(error)")
+ return
+ }
+ _ = try await manager.confirmRemoval(retry)
+ XCTAssertEqual(steps, ["private", "private", "public", "revoke", "credentials"])
+ XCTAssertNil(try store.pending())
+ }
+
func testRevocationOwnerChangeCannotClearReplacementCredentials() async throws {
let manager = makeManager()
duringRevocation = { manager.publicKey = "replacement" }xcodebuild test-without-building -project Bitkit.xcodeproj -scheme Bitkit -destination 'platform=iOS Simulator,name=iPhone 17' -only-testing:BitkitTests/ProfileRemovalCoordinatorTests
On head:
ProfileRemovalCoordinatorTests.swift:561: error: -[BitkitTests.ProfileRemovalCoordinatorTests testTransientWithdrawalFailureKeepsDisconnectRetryableForLiveSession] : failed - Disconnect retry for the still-signed-in owner was refused: pending
Executed 38 tests, with 1 failure (0 unexpected)
With the narrow fix applied, all 38 tests in ProfileRemovalCoordinatorTests and ProfileRemovalReservationTests pass.
There was a problem hiding this comment.
Confirmed: after an uncertain withdrawal, this implementation intentionally offers separately confirmed local completion rather than replaying remote cleanup. I am keeping this changes request visible; I am not treating the suggested marker move as a safe fix.
The durable UUID is required before the first potential remote mutation. In the production path, PrivatePaykitService.removePublishedEndpoints enters authenticated removal admission, which requires that matching persisted marker. It can also clear some payment lists before reporting a later delivery failure. Moving persist(scope) below withdrawal therefore both breaks the real admission boundary and leaves partially completed withdrawal without a durable restart obligation. The coordinator's injected closure test does not exercise those boundaries.
Local Disconnect acknowledges only local credential/state cleanup. It does not assert that endpoints were withdrawn or the homeserver session was revoked. A safe remote retry needs read-only reconciliation of the original owner/attempt plus a new confirmation; the pinned native contact reads do not provide that guarantee. The PR will retain the limitation explicitly. The separate receipt-attribution correction is proceeding without changing this persistence/retry contract. Please assess the limitation with these production boundaries in mind; no review dismissal or automatic remote replay is being performed.
There was a problem hiding this comment.
At f1fa5ee the marker still precedes endpoint withdrawal and recovery offers only local Disconnect after an uncertain withdrawal. I understand the admission and partial-mutation constraints described in the reply; the remote cleanup limitation remains, so I am leaving this thread open.
There was a problem hiding this comment.
You're right about the narrow fix I suggested. withdrawPrivate → removePublishedEndpoints runs under withOperation(.authenticated), and validate (:191-192) requires the persisted marker for a removal scope, so deferring the marker would make the first withdrawal throw .pending. My test only passed because the injected closures bypass that.
The gap is still there at 1f1bead: once a withdrawal or revoke fails, the user can only finish with a local Disconnect, which leaves the homeserver session and the published endpoints in place. A different shape would close it without moving the marker: let a Disconnect retry under the same pending removal id, the way .localRecovery already does at :553-556. Gate it on the same owner and on credentials still being present, and re-run the remote steps instead of jumping to local-only. The recovery view would then offer "Retry Disconnect" next to the local option while the session is live.
I'm keeping the REQUEST_CHANGES on this until there is a remote retry or we agree to track it as a follow-up.
There was a problem hiding this comment.
Agreed that Disconnect-only recovery is narrower. Disconnect does not delete contacts, so contact-read/CAS limitations alone do not rule it out. The pinned SDK already has revision-checked public endpoint deletion, runtime-local reuse of unchanged private-list work, and confirmed-inactive handling in grant revocation. Those are useful building blocks.
I checked whether the proposed retry can satisfy the required read-only assessment before a fresh confirmation using the current Paykit 01de5dd API. Two specific pieces are missing:
- Original grant status without reactivation.
identityStatusaccepts cached/live access. On a cache miss, the FFI provider restores the session. The exportedimportSessionexplicitly replaces the bearer. Neither exposes a read-only active/inactive/unknown result for the original grant. Retained credentials are therefore not proof that the grant remains live after an uncertain revoke. - The original outgoing withdrawal outcome.
privateMessageSchedulingSnapshotsupplies peers and pending counterparties, not the original outgoing message and its remote outcome.currentPrivatePaymentListsreads incoming counterparty lists. The sent-list reuse guarantee is runtime-local, and restart can republish. It cannot by itself classify a partially delivered withdrawal after lost acknowledgement or restart.
The same UUID can retain the local obligation, but the current record contains only version/ID/scope and the confirmation plan is consumed before execution. Simply admitting the same owner and credentials and rerunning private/public/revoke cannot supply those missing observations. In particular, the SDK's confirmed-revocation helper is itself a mutation operation, not a safe pre-confirmation probe.
The smallest coherent follow-up is a Disconnect-specific observation result tied to the original grant/attempt and outgoing work, then a fresh confirmation for verified remaining steps. Unknown states must stay unknown and retain local completion. Acceptance should cover partial withdrawal, lost acknowledgement, cold restart, changed remote state, inactive/rotated grants and cancellation without mutation. This does not require contact deletion or a full-wallet redesign.
Would you accept that focused SDK-backed follow-up with the current local-only limitation explicit in this PR? I have not added a guard-only retry or treated a deferral as agreed. This thread and your changes request remain open.
There was a problem hiding this comment.
At 86fb0c5 the pending marker still precedes remote withdrawal/revocation, and recovery still offers local Disconnect only. I checked that this contract survived the merge; the new receipt/restore fixes do not add remote retry. The focused Paykit observation follow-up proposed above has not been accepted in this thread, so I am leaving the discussion open.
There was a problem hiding this comment.
Verdict:
Findings: No new findings in the delta. The earlier receipt-attribution finding is unchanged. The separate Disconnect retry finding is also open.
QA for 91379a5:
Test 1: Controlled local recovery ✅ The previously recorded fixture retained its marker and watermark through failure and cancellation, then cleared them after explicit completion.
The earlier test report covers this unchanged fixture and includes the recording.
Test 2: J1 profile removal recovery ⏭️ The complete journey remains unrun.
A linked disposable peer and a paid regtest request established consumed version 0. A post-commit 502 led to recovery; the version survived a credential-erasure fault, and the final marker survived a later completion fault. The app returned to profile setup after explicit completion. State and proxy logs were checked directly. Remote edits and reconnect were not driven, and a post-commit 502 is not a dropped connection.
Test 3: J2 profile cleanup failure ⏭️ The complete journey remains unrun.
A pre-storage profile DELETE returned 404, recovery appeared, and a fresh GET still returned 200; proxy logs showed no additional DELETE after cancellation and restart. The withdrawal and partial contact-removal faults were not driven.
Test 4: Manual 1 teardown ownership ⏭️ One same-key replacement completion was exercised; the remaining teardown boundaries were not driven.
A local test hook held the old completion after teardown and opened profile creation. After a new session for the same key was active, releasing the completion returned false from its ownership check and left the new profile open. The hook's result and the final profile screen were checked separately; the recording ends during replacement setup and has visible stutters.
owner2-2x.2x.mp4
Test 5: Manual 2 durable cleanup failures ⏭️ The final-marker boundary was exercised; the full storage-failure matrix remains unrun.
The same recovery record survived the injected final-marker failure and a restart after credential cleanup, then disappeared after explicit completion. The record was compared on disk before and after the fault; the recording ends at the final confirmation, and the subsequent screen and marker removal were checked separately.
manual2-2x.2x.mp4
Check 1: Hosted tests ✅ The exact-head native test run passed.
Native CI passed at this head. These tests do not establish the unrun authenticated fault matrix.
This review covered the delta 60b1485..91379a5 and the earlier open attribution finding. The delta changes only test synchronization; no device case drove payment attribution. The earlier source review covers additional cleanup paths.
Twin synonymdev/bitkit-android#1693 shares the ownership and recovery goal; Android additionally offers Review, while iOS offers local Disconnect only.
Reviewer model: gpt-6.1-terra
Reasoning effort: high
Reviewed by gpt-6.1-terra-high via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest
There was a problem hiding this comment.
Verdict: ⛔️ Request changes — an old receipt can write after wallet wipe.
Findings:
- HIGH —
Bitkit/Utilities/ProfileOperationAdmission.swift:74: a receipt rejected from wipe admission still runs untracked, so its old-wallet activity can be written after database cleanup. The inline comment includes the controlled reproduction. - The earlier Disconnect retry finding remains open; the author has explained the remote reconciliation constraint.
QA for f1fa5ee:
Test 1: Controlled local recovery ✅ The marker and watermark survived failure and cancellation until explicit completion.
The earlier report records this unchanged fixture.
Test 2: Receipt attribution during cleanup ✅ A regtest reserved-address receipt retained its amount and contact key through local completion and restart.
The current-head report describes the stored-row and restart checks. It does not establish immediate physical persistence or power-loss atomicity.
Test 3: Receipt starting during wallet wipe ❌ An old receipt wrote an activity after wipe drained and cleared the test database.
A focused test reserved wipe, held an incoming invoice receipt in its resolver, drained and persisted wipe, cleared the isolated Core activity database, then released the receipt. The assertion that the database remained empty failed. The assertion result was checked in the test output; the recording shows the test host.
wipe-2x.2x.mp4
Test 4: J1 profile removal recovery ⏭️ The complete authenticated journey remains unrun.
The previous partial report exercised a post-commit error and credential failure, but not a dropped acknowledgement, remote edits and reconnect.
Test 5: J2 profile cleanup failure ⏭️ The complete authenticated journey remains unrun.
The previous partial report exercised a profile DELETE 404 and restart; withdrawal and partial contact-removal faults remain untested.
Test 6: Teardown ownership boundaries ⏭️ The full boundary matrix remains unrun.
The previous partial report exercised one held completion across same-key replacement, but not every SDK, publication, wipe and local-completion boundary.
Test 7: Durable cleanup failures ⏭️ The full storage-failure matrix remains unrun.
A preferences-flush fault retained the same marker across restart; after removing the fault, explicit Disconnect cleared it. The marker was compared on disk. The recording has visible stutters. The previous partial report exercised credential and final-marker failures; the complete storage and consumed-watermark matrix remains incomplete.
manual2-2x.2x.mp4
Check 1: Hosted tests ✅ The exact-head native test run passed.
Native CI includes receipt and admission regression coverage; it does not establish the unrun authenticated journeys or the new wipe interleaving.
The review covered 91379a5..f1fa5ee and the two earlier open findings. The authenticated fault journeys and the full teardown and storage matrices were not driven to completion.
Twin synonymdev/bitkit-android#1693 shares the ownership and recovery goal; Android additionally offers Review, while iOS offers local Disconnect only.
Reviewed by gpt-6.1-terra-high via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest
piotr-iohk
left a comment
There was a problem hiding this comment.
QA review
Scope: Full review of the complete PR diff against merge base 6c0a160, at f1fa5ee. No saved assessment baseline was available, so nothing was inherited.
2 actionable findings — resolve or provide an evidence-backed rebuttal.
Captured receipt leases keep invoice and reserved-address contacts through pending profile cleanup and check them again at the Core write. The earlier lost-contact thread is addressed at this head. A receipt that starts after a wallet wipe is reserved is not part of that lease: it still writes the activity after drain. A pending backup restore can also rewrite wallet data while wipe recovery is unresolved, because only some startup paths gained the new gate. The open Disconnect retry thread still matches the code and this PR's stated local-only contract; moving the marker would pass the SDK admission boundary the author described, so this review does not file a second copy.
Native CI completed successfully on this head. I confirmed that run's SHA and result only; I did not re-run the suite or read its logs. Device testing: not performed in this review. Compared with bitkit-android#1693 at 7e2fd14: Android still offers Review after an uncertain removal, and this PR deliberately offers local Disconnect only. Android's live-versus-receipt contact split matches the profile-cleanup behavior traced here; Android activity writes across wallet wipe were not followed. Parent bitkit-ios#1125 at 6c0a160 was comparison context, not a full review.
Suggested additional test cases
- iOS, warm wallet with
walletExistsalready true. Reserve or persist a wallet wipe, hold an inbound payment sync on the Core write until activities are deleted, then release it. Expected: the old activity is not inserted, including into a replacement wallet that reuses the default wallet id. - iOS, with a pending
paykitPendingBackupRestorekeychain item, a durable wallet-wipe marker, andwalletExists == true. Foreground the app or reconnect the network. Expected: no VSS or legacy restore writes until the wipe obligation is cleared.
Findings
- [HIGH] Stop untracked receipt writes during wallet wipe — inline at
Bitkit/Utilities/ProfileOperationAdmission.swift:74. - [MEDIUM] Block backup restore while wallet-wipe recovery is pending — Trigger: the running app already has
walletExists == true, a wallet-wipe recovery record, and a pending restore gate (paykitPendingBackupRestore, or a keychain read thathasPendingWalletRestore()treats as pending). The scene becomes active or the network reconnects.
Mechanism: handleScenePhaseChange and handleNetworkChange call retryPendingWalletRestoreIfNeeded() without blocksWalletStartup. restoreWalletBackupAndStart applies the VSS or legacy backup before startWallet, which is the call that returns on the new gate.
Consequence: this path would rewrite wallet backup data while wipe recovery is unresolved, including overlapping a confirmed wipe that then clears the marker.
Expected: restore retries should use the same gate as wallet startup and wait until the wipe obligation is gone.
Evidence basis: source analysis at f1fa5ee. A focused issue search for this restore-during-wipe mechanism found no existing ticket. I did not execute the path.
Source. Summary placement: The restore retries in handleScenePhaseChange and handleNetworkChange are outside the PR diff. The new blocksWalletStartup returns were added on setupTask, handleWalletExistsChange, and startWallet only.
Reviewer model: grok-4.7
Reasoning effort: high
There was a problem hiding this comment.
Verdict:
Findings: No new findings. The receipt/wipe and restore-retry fixes in 1f1bead0 hold at 86fb0c5 in source and the targeted native regressions. The existing Disconnect retry discussion remains open: recovery is still local-only, and the proposed Paykit follow-up has not been agreed.
QA for 86fb0c5:
Test 1: receipt writes versus wallet wipe ✅ Rejected admission stops the write, and wipe drains admitted work.
Checked the native XCTest assertions against a temporary bitkit-core database: reserved, persisted and unreadable wipe state; held payment enumeration; cancellation; and replacement-wallet operations. Nine selected receipt/activity tests executed successfully. The video shows the test host; the storage and ordering results come from XCTest assertions, not the splash screen. This was controlled component execution.
receipt-wipe-2x.2x.mp4
Native receipt/activity regression run, 2×.
Test 2: restore admission versus wallet wipe ✅ Pending retries reject before backup reads, and actual restore tails drain.
Both selected native restore regressions passed. Assertions cover reserved/persisted/unreadable wipe, pending-state read failure, live and cancelled held restore writes, invalidated completion and a fresh replacement restore. The video shows the test host; assertions establish these results. This does not establish the separate application network-reconnect action.
restore-wipe-2x.2x.mp4
Native restore regression run, 2×.
Test 3: controlled local recovery ✅ Failure and cancellation preserve recovery until explicit completion.
The actual confirmation/recovery UI test passed twice using the isolated DEBUG fixture. XCTest checked the same pending UUID, retained credentials and watermark 42, cancellation, zero remote cleanup calls, and explicit local completion. This fixture does not establish authenticated remote behavior. The recording retry still has visible stutters.
local-recovery-2x.2x.mp4
Controlled Profile recovery UI, 2×.
Test 4: J1 profile removal recovery ⏭️ Native fault/recovery steps ran; literal network reconnect remains unverified.
A genuine native peer payment established consumed-list version 0. The profile DELETE committed upstream with HTTP 204, then the scoped proxy dropped its acknowledgement. The app retained recovery, offered a separate local confirmation, and cancellation did no cleanup. An independent fixture client subsequently restored/edited the remote profile; the request journal showed no automatic second DELETE during the ensuing recovery/cold-launch sequence.
The same UUID survived the storage faults in Test 7. Profile recovery and ordinary Receive remained available after credentials were absent. The held old completion against a same-key replacement is covered in Test 6. Request journals, exact credential queries and stored-state comparisons establish the details that the screen recording cannot show.
Background/foreground was exercised. A device-scoped transport restart did not establish the application's network-change callback; reboot restored input but does not prove network reconnect. J1 action 10 and therefore the complete journey remain unrun. A separate decoded-QR/nonreuse check was not performed. The recording has visible stutters.
j1-2x.2x.mp4
Native lost-acknowledgement and recovery sequence, 2×.
Test 5: J2 profile cleanup failure ✅ Reused the authenticated report for unchanged cleanup paths.
I relied on the author's authenticated J2 report, attributed there to the installed candidate whose recovery fixes are preserved in 0eef45d1. It covers private-withdrawal refusal, unknown/partial contact cleanup, a profile DELETE returning HTTP 404 before storage, cancellation and cold restart without replay.
The reached profile, contact, credential, removal and recovery-view files are byte-identical from 0eef45d through 86fb0c5; the Paykit and bitkit-core pins are unchanged. The dependency delta updates LDK to 0.7.0-rc.78. This is inherited evidence, with no new J2 device execution or video claimed.
Test 6: Manual 1 teardown ownership boundaries ⏭️ One installed same-key completion case passed; the full boundary matrix remains unrun.
A temporary DEBUG-only gate held the result after actual profile deletion, grant revocation and local cleanup. Normal native profile creation then obtained a new grant/session for the same public key and published Replacement. Releasing the old result made the unchanged production ownership guard return false; Profile stayed on Replacement. The gate receipt, native request journal and fresh profile GET corroborated the UI.
This closes the installed completion case that earlier attempts did not reach. It does not cover every admitted Paykit SDK operation, publication and local-cleanup stage crossed with identity replacement or wallet wipe. The earlier reported scheduler tests remain component evidence; the complete Manual 1 matrix is unrun. The recording has visible stutters.
owner-2x.2x.mp4
Real removal, same-key native replacement and released old completion, 2×.
Test 7: Manual 2 durable cleanup failures ✅ The exact recovery obligation survived each exercised failure and cold launch.
The same pending UUID survived session deletion, root deletion, shared-keychain enumeration/deletion and unreadable reservation storage. Exact in-process Keychain queries checked credential presence/absence; cold-launch state samples checked the marker and genuine consumed-list version 0. That watermark survived the credential and reservation refusals.
Preference-flush and final-marker-completion faults also retained the same recovery UUID across cold launch after credentials were absent. At those later stages the preference writes had already cleared the watermark and identity; the result does not establish preference rollback or physical-disk/power-loss atomicity. After removing the final fault and separately confirming local Disconnect, the marker was absent, mappings/history and watermark were cleared, and reserved receive indexes remained. The edited remote profile was retained, with no automatic cleanup replay.
The video shows the confirmations and cold launches. Scoped fault receipts, credential queries and stored-state comparisons establish the invisible boundaries. The long recording has visible stutters.
manual2-2x.2x.mp4
Sequential native storage-fault/cold-recovery checks, 2×.
Test 8: Manual 3 receipt attribution through profile cleanup ✅ A real receipt retained its historical contact through local cleanup and cold resync.
With session erasure faulted and profile cleanup pending, a fresh 5,000-sat regtest payment arrived at the native peer's reserved address. The UI showed the receipt. After explicit local Disconnect and cold restart/resync, the bitkit-core activity retained the exact same ID, amount and historical contact key, verified by matching SQLite rows. Profile assignments/history were empty, the recovery marker was absent, and the reserved index remained. The contact-key claim comes from stored-row comparison; a deleted contact name is not displayed. The recording has visible stutters.
manual3-2x.2x.mp4
Native reserved-address receipt, local cleanup and cold resync, 2×.
Check 1: build and CI. Built 86fb0c5 for the iOS simulator and executed the selected tests. Xcode 27 promoted an existing implicit-strong-capture diagnostic to an error; the local build required an OTHER_SWIFT_FLAGS= override. Production source and dependency pins were unchanged for Tests 1–4, 7 and 8; Test 6 used the disclosed temporary completion gate. The exact-head native CI run reports success; I verified its head and conclusion, without auditing its complete test log here.
Coverage: Delta f1fa5eea..86fb0c5, the prior open receipt/restore/Disconnect feedback, and reached parent-merge interactions. Inherited Blocktank, notification, payment and translation changes were not separately audited or fully device-tested in this pass.
Twin: bitkit-android#1693 fix: bind profile removal to its original owner matches the ownership/recovery goal. Android also offers remote Review; this iOS implementation deliberately offers local-only recovery.
Reviewed by gpt-6.1-sol-high
piotr-iohk
left a comment
There was a problem hiding this comment.
QA review
Scope: Full reassessment of bitkit-ios#1143 at 08f29903, against merge base ef1286d8. The comparison base moved from 6c0a160 to ef1286d and no coverage baseline was supplied, so the whole pull request was reviewed again with no inherited coverage.
No new actionable code findings.
Removal, unlink, and Disconnect stay bound to the confirmation that started them. The recovery record is written before destructive work and removed only after credential, preference, and reservation acknowledgement. Active subscriptions block delete and unlink, including a recheck inside the confirmed profile deletion. Pending profile cleanup keeps ordinary receive attribution from the pre-cleanup snapshot, and wallet wipe waits for admitted receipt and restore work before clearing storage. Wallet backup capture fails closed while a removal record is pending.
The same removal, subscription, and local-completion behavior matches bitkit-android#1693 at e1cc3b17. iOS recovery offers local Disconnect only, as stated in journeys/profile/profile-removal-recovery.xml. This change is stacked on bitkit-ios#1125 at ef1286d; the assessment covers the delta from that merge base.
Hosted unit/UI and SwiftFormat succeeded on this revision. This review did not re-run them. Device testing: not performed. Authenticated J1 and J2 still need a disposable wallet and the fault-capable Pubky/Homegate setup in the PR; standard staging does not provide those faults. BITKIT-001 and BITKIT-002 did not apply.
Earlier findings reconciled at this revision: 1 still present.
Existing concerns
- [MEDIUM] Disconnect cannot retry remote cleanup after a failed withdrawal or revoke — still unresolved; already tracked.
Reviewer model: grok-4.7
Reasoning effort: high
There was a problem hiding this comment.
Verdict:
Findings: No new findings in this delta. The existing remote Disconnect retry discussion remains open. Recovery still offers local completion; the proposed paykit-rs#244 Expose original grant and private-list acknowledgement evidence is not adopted here.
QA for 08f2990:
Subscription guards before profile cleanup ✅ The added SDK guards and coordinator regressions passed.
Native XCTest assertions cover payer/payee records from other apps, absent contacts, expiry variants, unavailable reads, cancellation, confirmation rechecks and original-owner completion. The revised Create Profile admission test also passed. These are controlled component tests; the recording shows the test host.
subscription-regressions.2x.mp4
Subscription and coordinator regression execution, 2×.
Subscription refusal and cleanup failure ✅ Both active roles refused deletion; a fresh attempt succeeded after cancellation and completed peer setup.
J2's new subscription checks ran with genuine native peers and an unrelated saved contact. The payer subscription was canceled before testing the payee role independently. Each active record had zero payment proofs. Delete Profile showed “End active subscriptions before removing this profile,” with no generic Retry dialog, navigation or pending marker.
The scoped request journal establishes zero authorized PUT/DELETE for the tested identity during each refusal interval. Its rolling buffers have exact overlapping sequences; this claim comes from the journal, not the video. It excludes GET/LOCK/auth traffic and is bounded to those two attempts.
After both subscriptions were canceled, the first cleanup attempt retained recovery because the unrelated peer's link was incomplete. I completed local recovery, finished that peer setup and created a fresh session for the same key. A new explicit deletion then returned to onboarding; the recovery marker was absent and an independent profile GET returned 404. This was a fresh operation, not remote retry of the uncertain attempt. The recordings have visible stutters.
| Active subscriptions | Fresh cleanup after setup repair |
|---|---|
j2-refusal.2x.mp4 |
j2-cleanup-retry.2x.mp4 |
Authenticated refusal and cleanup sequences, 2×.
For J2's earlier withdrawal, unknown/partial-contact and pre-storage 404 faults, I retained the author's bounded report and prior review evidence. Those results remain attributed to their original candidates. The new subscription eligibility checks were exercised separately above; no new complete fault-matrix execution is claimed.
Reservation acknowledgement retains cold recovery ✅ The new fault retained the same obligation after credential erasure and restart.
Manual 2's new reservations:acknowledge selector fired at the actual local validation boundary. Exact Keychain queries confirmed root, session and own shared credential were present before cleanup and absent afterward, including after cold launch. The same recovery UUID survived with no cached identity, while Profile still offered Disconnect.
After removing the fault, a separately confirmed local Disconnect cleared that marker. Stored-state comparisons showed empty assignments/history and retained reserved indexes. The remote profile stayed present, as expected for local completion. Keychain queries and cold stored-state samples establish the invisible details. The recording has some stutters.
manual2-reservation.2x.mp4
Reservation refusal, cold recovery and explicit completion, 2×.
This boundary follows private-cache and watermark clearing; it does not establish watermark rollback or physical-disk atomicity. The prior successful credential/preference failure checks remain applicable to the unchanged earlier stages.
Receipt attribution through profile cleanup ✅ The prior successful receipt and cold-resync evidence remains applicable.
Manual 3 relies on the 86fb0c5 report: the same regtest activity retained its amount and historical contact key through local completion and restart. The reached receipt/accounting code and package pins are unchanged. The new opt-in reservation fault does not change normal assignment clearing. No new receipt or payment is claimed here.
Profile recovery after uncertain removal ⏭️ J1's literal device network-reconnect action remains unverified.
The prior report supplies the preserved lost-acknowledgement, local recovery and installed same-key completion evidence. A fresh pending-cleanup setup remained reachable through foregrounding, but Settings supplied no Wi-Fi control and restarting the simulator's network-configuration service produced no application network-change event. Those actions do not prove the reconnect criterion. No additional current-head video proof is claimed for this incomplete action.
Teardown ownership across replacement and wipe ⏭️ The complete installed boundary matrix remains unrun.
Manual 1 retains the prior installed same-key completion case and scheduler evidence. The reached ownership guards remain intact, and the selected coordinator regressions passed. These do not establish every SDK, publication and local-completion boundary crossed with replacement or wipe. Test access became unavailable before a new held-write/wipe scenario could run; no new matrix or actual-wipe pass is claimed.
Check: build and CI. Built the head's source and ran the selected regressions on iOS 26.5 with Xcode 27. The local build used OTHER_SWIFT_FLAGS=; production source and package pins were unchanged. The exact-head native CI run reports success. I checked its SHA and conclusion, without auditing its complete artifact here.
Coverage: the complete source delta 86fb0c5..08f2990, reached subscription/ownership/recovery paths and the existing open feedback. No device case inspected every translation, every teardown interleaving or a physical phone.
Twin: bitkit-android#1693 fix: bind profile removal to its original owner shares the ownership, recovery and subscription-guard goal. Parity is partial: its current description also offers fresh local Disconnect and remote Review; iOS retains local-only recovery after an interrupted attempt.
Reviewed by gpt-6.1-sol-high
Related issue: synonymdev/bitkit-android#1687 (remains open for native conditional contact cleanup and authenticated fault journeys).
Twin: synonymdev/bitkit-android#1693
Dependency: #1125
This PR binds profile deletion, Disconnect and their late callbacks to the sign-in and confirmation that started them. Interrupted cleanup stays visible after restart, including after credentials have already been removed, and requires explicit local completion.
Description
Out of Scope
Design
N/A — no design available. Existing Profile controls and recovery components are reused.
Preview
The native hosted-view test records the three recovery screens: interrupted profile cleanup with Disconnect, interrupted wallet wipe with its explicit confirmation path, and an unreadable local record with Retry. Screenshots are retained in the final XCResult evidence. These are controlled installed view states, not authenticated journey recordings.
QA Notes
Setup
Use the committed package pins: Bitkit Core 0.8.0-rc11, Paykit 0.1.1-rc2, VSS 0.5.26-rc1 and LDK 0.7.0-rc.78. Select an available concrete simulator and the repository unit selection in
.github/workflows/unit-tests.yml. The controlled UI checks can be run withxcodebuild test -project Bitkit.xcodeproj -scheme Bitkit -destination 'platform=iOS Simulator,id=<UDID>' -only-testing:BitkitUITests/ProfileRecoveryUITests -only-testing:BitkitUITests/ContactImportUITests; their isolated DEBUG fixtures need no funded wallet.Authenticated J1/J2 require a disposable wallet, regtest funds, compatible Pubky 0.16.2 native homeserver/Homegate services, app-approved companion grants, a peer, and principal/method/path-scoped fault controls that can reject before storage or drop an acknowledgement after committing. The recorded local environment used
E2E_BUILD=1,E2E_BACKEND=local,E2E_NETWORK=regtest,E2E_LOCAL_HOST=127.0.0.2, Homegatehttp://127.0.0.2:18889, and the launch argument-E2E_PROFILE_REMOVAL_FAULTS. The loopback alias was provisioned explicitly. Standard staging does not provide these faults. The working fixture/operators and frozen receipts are preserved in the validation workspace; a portable committed fixture recipe is not available. A reviewer needs equivalent owned services before reproducing the authenticated fault cases. Preserve existing wallets and pending operations; do not use live funds or reset a personal wallet.The DEBUG simulator fault selector is an app-scoped sticky file,
profile-removal-test-fault, in Application Support. Values arepaykit_session:delete,pubky_secret_key:delete,shared-own:enumerate,shared-own:delete,preferences:flush,reservations:acknowledge,marker:persist, ormarker:complete. It additionally requires E2E/regtest and is disabled for unit tests. Clear it before another profile activation/adoption, unrelated backup or reset. Faults invoke real error paths; they are not bound to a particular profile. Controlled mock UI states remain separate evidence.Journeys
J2's authored payer/payee subscription-refusal steps, unrelated saved contact, and cleanup after cancellation were exercised on October 29 on the owned unfunded simulator. Both roles displayed the active-subscription instruction without a Retry dialog; before/after reads preserved the profile revision, contact records, endpoint and reservation records, and outbound history. The earlier missing-contact refusal remains separately recorded. After both unpaid subscriptions were canceled, a new explicit confirmation completed cleanup and returned to profile onboarding. J2 remains unchecked: these earlier point-in-time observations do not establish zero attempted server mutations. The later bounded payer/unlink trace below supplies a scoped assertion for one new attempt; the earlier fault checkpoints retain their stated limits. The separate genuine Ring-owned unlink journey passed later that day, as recorded below.
profile-removal-recovery.xml— original confirmation, uncertain cleanup without replay, explicit local-only Disconnect, credential/storage failure, restart without an active identity and ordinary receive independence.profile-cleanup-failure.xml— incomplete cleanup stays a failure; local Disconnect remains separately confirmed.Manual Tests
1. Hold an admitted SDK/publication/local completion at each teardown boundary, then replace the identity or begin a controlled wipe → the old action drains without publishing or completing UI for the replacement. Exact scheduler barriers require the native test seams.
2. Fail keychain enumeration/deletion and preference/reservation acknowledgement, restart at each credential/local-cleanup boundary → consumed-list watermarks and the exact recovery obligation survive until required cleanup is acknowledged. These deterministic durable-store failures need controlled resource injection.
3. On the owned regtest simulator, fail session deletion before local mappings clear, receive one payment at the genuine peer-reserved address, then explicitly finish local cleanup and cold-restart/resync → the same activity retains its amount and historical contact key while profile mappings and the recovery marker are removed. The sticky teardown fault and stored-row inspection require capabilities outside the journey runner.
Current authored QA status (bounded to the recorded evidence):
profile-removal-recovery.xmlprofile-cleanup-failure.xmlunlink-ring-pubky.xml, actions 1–10The finite follow-up mapped historical J2 fault actions to current actions 4–11 and checked the relevant cleanup methods and package pins. Those results remain attributed to their original runtime candidate; the new global-subscription gates and LDK change are recorded explicitly. Manual 2 distinguishes credential acknowledgement from final local completion: consumed-list watermarks survive required credential-erasure failures, while the exact recovery marker remains until all required local acknowledgements succeed. Independent credential cold cycles do not all contain the original genuine watermark, and that historical installed selector has no reservation-acknowledgement fault. Manual 2 remains unchecked on those specific limits.
On the owned simulator, one additional active payer unlink refusal now has a continuous scoped mutation journal. The same fixture and Ring identity were preserved; after the fixture owner resumed the retained VSS/auth services, the wallet started and Bitkit published its private endpoint. A genuine future 1,000-sat subscription from another app was ActiveRecurring on both native peers, with zero proofs, alongside an unrelated saved contact. Tapping Unlink pubky displayed “End active subscriptions before removing this profile,” with no Retry dialog or navigation away. From 06:17:28.137858 through 06:17:38.913338 UTC on October 29 (10.775481 seconds), the current-owner journal recorded zero authorized PUT/DELETE attempts. Its prefix/hash was unchanged, setup mutations confirmed the observer was active, the scope/control remained unchanged, and no receipt-loss warning was found. The local marker was absent at both checkpoints.
This assertion covers that payer/unlink attempt at the authorized mutation boundary, excluding GET/LOCK/auth and rejected authorization. It does not retroactively instrument earlier payer/Delete or payee attempts. Broad pre/post reads retained the profile, both exact contacts and public endpoint, while reservation/outbound hashes differed outside the exact interval; full-state equality is not claimed. The one subscription was then Canceled on both peers with zero proofs, and the app and its scoped automation services were stopped. Initial wallet-not-ready and incomplete-link setup attempts are preserved. No product, dependency, compiled fixture or commit changed. The finite report and hashed receipts are retained under
finite-j2-manual2-20261029.The open remote-Disconnect follow-up is tracked by Paykit #244, which proposes original-grant observation and positive private-list acknowledgement evidence. It is unmerged and no new SDK release is adopted here. App integration, exact original-credential execution binding, cold restart and uncertain-revocation handling remain unfinished; the existing review discussion remains open.
Automated Checks
ProfileOperationAdmissionTests.swift,ProfileSessionTransitionTests.swiftandProfileWipeAdmissionTests.swift— original-owner admission, cancellation, queue/wipe draining and retained local completion.ProfileRemovalStoreTests.swift,ProfileRemovalCoordinatorTests.swiftandProfileRemovalReservationTests.swift— exact durable obligations, one-use confirmations, original profile CAS, missing-source cleanup and strict local acknowledgements.ProfileDestinationViewTests.swift— renders and checks all three recovery states without an active identity; original-owner readiness excludes replacements.ProfileRecoveryUITests.swiftand its isolated DEBUG fixture — drives actual confirmation, failed local cleanup, retained UUID/watermark, canceled confirmation and explicit completion; preserves the read-only restoration flow.ContactImportUITestscases — imports cannot complete into a screen the user left or bypass an outstanding save.Evidence
The downloaded
test-resultsartifact (12879768977) was subsequently audited: its SHA-256 matches GitHub and all 6,133 ZIP entries pass CRC. CI checked out mergedbfd6cea, whose tree exactly matches head08f29903. All 2,993 individual execution summaries reconcile without identifier/status mismatches: 2,988 passed and five skipped. Modern XCResult counts 2,959 distinct passing tests plus five skips; the difference is the launch test's 30 successful dynamic runs, not a failed-to-passed retry. Root, action/build logs, all per-test summaries, exported diagnostics, and modern result trees were inspected. The 286 distinct build warnings are mirrored in root/build containers and match the already-saved logs; action runtime issue containers and per-test diagnostic fields are empty. Raw app warnings remain visible, including six UI USDT refresh failures and one missing graph-cache warning; this is not a warning-free or complete-USDT acceptance claim. No original artifact entry changed; Apple's reader added only a deriveddatabase.sqlite3cache to the extracted copy. Separate evidence:work/validation/ios1143-subscription-composition-20261029/hosted-08f29903-xcresult/. Manual 2 remains incomplete.Hosted CI for
08f29903has now completed: unit/UI run 40832068350 passed 2,948 unit tests and 40 UI tests, with zero failures and five explicit fixture-dependent skips. Lint run 40832068393 reported 0/831 files requiring formatting (95 excluded). The full test log's 571 duplicate-symbol warning lines (SQLite, Rust/secp256k1 and BLAKE3) match the prior successfulacc028f4run after removing only CI prefixes/timestamps; all ten exposed warning annotations and all five skips also match. No new diagnostic was found in that comparison. Migration was skipped. This hosted result does not complete the reservation fault/cold-restart acceptance or the remaining Manual 2 watermark cases. Full logs, annotations and comparisons are preserved inwork/validation/ios1143-subscription-composition-20261029/hosted-08f29903/.Current head
08f29903adds the opt-inreservations:acknowledgefault at the real reservation-state validation boundary. It retains the existing DEBUG, simulator, E2E/regtest, explicit launch-flag, sticky-file, and non-unit guards. A refusal occurs after required credential erasure and private-cache cleanup, before final marker completion; this does not assert that watermarks survive that later boundary. A fresh independent review found no actionable issue.Final local validation on 2026-10-29 used the frozen two-file candidate that became
08f29903: SwiftFormat 0.64.0 reported 0/831 files requiring formatting (95 excluded); the E2E simulator build passed; the full unit selection passed 2,948 tests with zero failures and one explicit owned-USDT-fixture skip. Complete structured diagnostics contain no compiler errors, warnings or test failures; the raw log retains the existing Apple AppIntents metadata-extraction notices. All tracked inputs matched before/after validation. All 11 commits in the PR range were cryptographically verified locally against baseef1286d8, and GitHub reportedverification.verified=truewithreason=validfor every commit after push.The new reservation refusal/cold-restart/retry acceptance was not executed. Creating a disposable iOS26.2 simulator stalled; an unused-looking shutdown simulator also did not boot and its ownership was not established, so no app was installed there or further retry made. The already-booted, task-owned unit destination contains a production wallet seed and was preserved. Only this task's stalled create/boot subprocesses were terminated; no shared-service restart, wallet reset, ledger/cache injection, mining or payment was performed. Manual 2 remains unchecked. Source review confirms a genuine consumed-list watermark needs the incoming-request payment path; the earlier ordinary contact payment retained a nil watermark. Existing chain transactions and the retained Ring wallet were untouched. Evidence:
work/validation/ios1143-subscription-composition-20261029/runtime-acceptance/manual2-feasibility-20261029/.Earlier candidate
acc028f4added a test-only correction to the composed subscription safeguard. The Create Profile admission test now waits for the screen's actual original-owner capture before releasing its controlled transition or replacing the owner; a keyboard notification is no longer used as a readiness signal. The original two-second resumption deadline and replacement-owner assertions remain. The hosted test view disables user interaction; no claim is made that this alone proves programmatic keyboard suppression. An independent affected-delta review found no actionable issue. Production code and dependency pins are unchanged from95195079.On exact tree
3959dd8d, the required full unit gate passed 2,948/2,948 tests, with zero failures, skips or retries. All 1,660 input hashes remained unchanged; the corrected Create Profile case passed in 0.325 seconds, and all 12 affected profile-screen tests also passed in the focused run. Full SwiftFormat 0.64.0 reports 0/831 files requiring formatting, 95 configured skips; diff checks pass. Translation inputs are unchanged, retaining the separately verified zero errors and 15 exact-parent warnings. The complete diagnostic audit reconciled all nested results and 27 attachments; three native QoS warnings and two AppIntents notices remain with unresolved causal attribution, with no introduced actionable diagnostic blocker verified. Prior 40 UI passes and three Trezor skips remain separately attributed to95195079; all production, UI-test, package and configuration inputs are unchanged by this test-only follow-up.All ten PR commits against base
ef1286d8are cryptographically verified locally and on GitHub (verified=true,reason=valid), with the published head matching the tested tree. Current-head native CI passed on attempt 1: 2,948 unit passes, 40 UI passes (including 30 planned launch configurations), five explicit fixture skips, zero failed tests, and zero test reruns. Complete structured results and text diagnostics were audited, and the uploaded artifact and evidence hashes were verified. All 12 ProfileDestinationViewTests passed; the revised Create admission test took 0.429 seconds. The hosted merge67b19194has the exact tested tree3959dd8d.The audit retained 286 linker warnings, whose titles and duplicate symbols exactly match actual parent
ef1286d8. IntestFailedDisconnectRetainsSafetyStateUntilExplicitLocalCompletion, XCTest automatically retried the fixture Restore tap twice after AX scroll-to-visible failures; the third action succeeded and the test executed once. These action retries are separate from test reruns, with no product-cause or harmlessness claim. This successful run does not establish the cause of the earlier failed run. The five skips require the recovery-protocol fixture, local Arbitrum fork, and three disabled AI-only Trezor cases; skipped and excluded integration cases receive no pass credit. This general CI run does not establish authenticated remote recovery or the remaining manual journey cases.Formatting CI passed: its complete 193-line log and annotation were inspected, with 0/831 files requiring formatting, 95 configured skips and one runner-image notice. All nine jobs in the migration workflow were skipped, with no migration test credit. The PR remains ready for review under Ben's explicit instruction with incomplete testing. João's Disconnect-retry discussion remains open; recovery still offers local completion only. The remaining authenticated J1/J2 gaps are retained below.
The initial bounded subscription attempt used the audited local E2E build from
95195079on an unfunded iPhone 16 / iOS 26.2 simulator. The normal server settings savedtcp://127.0.0.1:18682; the local app was reinstalled preserving the wallet. Profile creation, app-approved companion authorization, peer linking, and proposal delivery succeeded, but acceptance initially failed and that proposal expired. The October 29 continuation found a fixture helper mismatch: it advertisedoutgoing_payments=false, while the native SDK requires that capability to claim and accept a request. The old generic error did not retain its exact native cause. The fixture-only correction retained an unsupported payment adapter and passed formatting, Clippy with warnings denied, and build checks. A capability preflight succeeded before one new proposal was sent on the same peer; the expired intent and evidence were preserved.The corrected native companion and peer both confirmed ActiveRecurring for a future 1,000-sat subscription (November 5–6, 2026), with zero payment proofs and no payment execution. At 04:20:19 UTC, Delete Profile displayed “End active subscriptions before removing this profile.” After native
remove_contactremoved only the ContactRecord, the same refusal appeared at 04:21:01 UTC. Native post-reads confirmed the global active request survived with the contact absent. The source-defined localprofile-removal-v1.jsonwas absent at the before/after checkpoints; this is point-in-time evidence, not a continuous filesystem trace. The remote fixture journal still targets the old principal, so no zero-server-mutation assertion is claimed for this identity. All 105 installed app files matched the audited bundle;acc028f4changes only one unit-test file from the built revision. No app source, network configuration, wallet funding, reset, or payment execution changed. Five startup/RGS diagnostics are retained, so runtime is not described as warning-free. The active orphan and expired-proposal evidence were frozen before the intentional transitions below.A further bounded continuation exercised Delete Profile with an unrelated linked contact in each subscription role. The payer refusal at 04:48:02 UTC and payee refusal at 04:48:39 UTC left the selected native before/after snapshots identical, including the unrelated contact and public profile revision. The reverse proposal was genuinely accepted as ActiveRecurring by the same owned peer, with a future November 5–6 schedule and zero payment proofs. Fixture helpers retained unsupported payment adapters; no payment was executed. The original payer subscription and new payee subscription were canceled through the native SDK, and the peer confirmed both terminal states. At 04:49:49 UTC, a fresh “Yes, Delete” confirmation completed and displayed profile onboarding. Independent peer GETs found both Paykit and legacy profiles absent. The GET-only encrypted-state observer found zero contacts, the managed Bitkit endpoint Removed, private payments disabled, all 11 outbound messages Sent, and no recovery markers. The local removal journal was absent and the wallet directory remained. This establishes the recorded cleanup outcome, without a server mutation-attempt count or complete keychain/reservation audit. At that checkpoint Ring was not installed; the later genuine Ring unlink result is recorded separately below. All 105 installed files still match the audited app; helper formatting, Clippy with warnings denied, and builds passed after one retained helper compile error was fixed. Five startup/RGS errors remain in the captured runtime log. The app and its scoped Argent services are stopped; the completed fixture and historical evidence are preserved. No app source, commit, funding, wallet reset or hosted-CI audit changed.
The separate
unlink-ring-pubky.xmljourney passed all 10 authored actions on October 29 using genuine Pubky Ring frompubky/pubky-ring@218922f7, built in Release for the owned simulator with only local-fixture startup/default-homeserver configuration. Ring generated and signed up a fresh disposable identity through its real UI/deep-link flow on the owned native backend. Bitkit discovered it through the shared keychain, adopted it through the Ring row and saved a profile. The UI displayed UNLINK, the link icon, “Unlink pubky” and the exact authored confirmation. “Yes, Unlink” at 05:21:03 UTC returned to profile onboarding with the same Ring identity still offered. Reopened Ring listed it, and its detail screen showed the identical full public key. Independent native peer GETs retained the exact Ring-owned Paykit profile revision; legacy profile was absent both before and after. This is the intended unlink behavior (testUnlinkDoesNotDeleteRingOwnedProfile): Bitkit disconnected its credentials while Ring's identity and public profile remained. The app logged session deletion and sign-out, the local recovery journal was absent afterward, and the wallet directory remained. This does not claim deletion of Ring-owned public data, a complete keychain/reservation audit, or a server mutation-attempt count. All 105 installed Bitkit files and 165 installed Ring files matched their recorded builds. Ring's build succeeded with zero errors and 474 structured warnings retained; its fixture edits passed type checking, lint and formatting, while eight lint warnings and five formatting files were verified byte-identical to its source baseline. The captured Bitkit runtime retains five startup/RGS errors plus a contact-load “changed” error and shared-contact-refresh warning during adoption; no warning-free runtime claim is made. Both test apps and only the owned simulator's Argent services were stopped, preserving the fixture and identity. No Bitkit source, commit, funds, wallet reset or hosted-CI result changed.Prior signed head
95195079composes the subscription safeguard from parent #1125 atef1286d8into the original owner/attempt coordinator. Raw payer/payee subscriptions from every app, including records without saved contacts, are checked during preparation, confirmation and the admitted cleanup. The strict contact batch and revision-bound final profile deletion recheck the same policy. A late refusal preserves the original recovery UUID and retained safety state; it adds no remote retry. Two independent affected-delta reviews found no introduced actionable issue.Local validation on the frozen composed tree
824c2cc5, using Xcode 26.1 (17B55) and the owned iPhone 16 / iOS 26.2 simulator, passed 2,948 unit tests and 40 UI invocations, with zero failures and three explicitly disabled Trezor skips. The focused 76-test gate also passed. All 1,660 source inputs remained unchanged. Full SwiftFormat 0.64.0 reports 0/831 files requiring formatting, 95 configured skips. Translation validation reports zero errors and 15 warnings verified individually against the exact parent. The current changes add three inherited SDK test functions and extend existing coordinator coverage to unlink, without a new child test framework.The sole complete diagnostic audit reconciled all 2,991 invocation records, all referenced results and 64 attachments. Five QoS warnings and 24 CoreGraphics NaN notices remain with unresolved cause; no introduced actionable diagnostic blocker was verified. All six dependency pins match; their status/diff checks are clean using read-only object mirrors, with prior incomplete object-store errors retained. This is not a claim that every dependency archive is available.
All nine PR commits against live base
ef1286d8have valid cryptographic signatures locally and on GitHub (verified=true,reason=valid). The published merge has exactly the tested tree824c2cc5. Native CI failed on attempt 1: the added Create Profile admission test timed out awaiting resumption after its original transition release. The following key-load assertion did not fail, and execution continued into the replacement-owner iteration. Complete session logs show a keyboard-queue timeout and an invalid keyboard-session message during the 9.734-second test; neither observation establishes the sole cause of the assertion timeout. The hosted result is preserved separately from the passing local result. Formatting CI passed with its complete log and annotations inspected; 0/831 files require formatting, 95 skipped, and one runner-image notice remains. All nine jobs in the migration workflow were skipped. No runtime pass is claimed for the new J2 expectations. The PR remains ready under Ben's explicit testing exception.Prior composed head
86fb0c5cincorporates published parent #1125 at3f887088, including LDK 0.7.0-rc.78. The backup cache API integration preserves profile admission and generation validation; the recovery fixture retains the actual local cleanup path alongside the parent's diagnostic trace. Two independent affected-delta source reviews found no actionable blocker, with unchanged prior source coverage retained by exact file/region identity.The final composed-tree run on Xcode 26.1 (17B55), iPhone 16 / iOS 26.2 (23C54), passed 2,945 selected unit tests and 40 UI invocations, zero failures, with three explicit disabled Trezor skips. Both ProfileRecovery UI cases and all four wipe/restore regression tests passed. All 1,660 source inputs and six package pins were verified. The complete 31,071-line build/test log, all 2,988 nested invocation summaries, eight diagnostic logs and 64 attachments were inspected. Five QoS warning records represent six native SDK/LDK occurrences; causal attribution remains unresolved. Two AppIntents notices remain. SwiftFormat 0.64.0 reports 0/831 files requiring formatting, 95 skipped. Translation validation reports zero errors and 15 warnings, byte-identical to the exact parent, with all warning-source files verified against it. Diff checks pass.
All eight PR commits against live base
3f887088are cryptographically verified locally and by GitHub (verified=true,reason=valid) for the same commit set. The signed additive merge has the exact locally tested treece248fd2. At86fb0c5c, formatting CI passed on attempt 1 with 0/831 files requiring formatting, 95 skipped. Its complete log and all annotations were inspected; one runner-label migration notice remains. Native CI passed on attempt 1: 2,945 unit and 40 UI invocations passed, five fixture skips, zero failures. All four wipe/restore regressions and both ProfileRecovery UI cases passed. The artifact digest, source checkout tree, complete 5,888-line job log, all 2,990 nested invocation summaries, eight diagnostic logs and 64 attachments were verified. The complete 575-object structured issue report is byte-identical to the prior1f1bead0result, covering the same 286 duplicate symbols. Ten duplicate-symbol warning annotations, five fixture-skip notices and one macOS capacity notice remain. No introduced actionable diagnostic finding was verified. Hosted QoS records are empty; the five local warning records and their unresolved attribution remain. The five fixture skips are the native-USDT case, three Trezor cases and local-Arbitrum payment case. The migration workflow was skipped by its release-branch condition.The installed receipt acceptance below remains tied to
f1fa5eeaand its earlier dependency pin; it is not new composed-binary acceptance. The four configured live/destructive suite exclusions and the native-USDT/local-Arbitrum fixture exclusions are not passes. Both literal J1 runtime gaps and João's open SDK-backed Disconnect-retry discussion remain unchanged. The PR remains ready under Ben's explicit testing exception.At prior head
1f1bead0, the repair closes the receipt and restore wipe-admission gaps reported onf1fa5eea. An isolated Core test reproduced the rejected receipt writing after cleanup. A rejected wallet lease now stops the entire operation. Public payment enumeration, queued processing and transaction-detail writes register before starting; wipe waits for their actual awaited tails. Pending VSS, legacy and USDT restore entries use the same wallet lifetime, and foreground/reconnect retry paths stop while startup is blocked. A restore invalidated by wipe cannot report successful completion. Profile-only cleanup retains the earlier receipt-attribution behavior.Four regression tests were added and one admission test updated. They cover reserved, persisted and unreadable wipe state; pending-restore keychain read failure; suspended enumeration; cancelled and uncancelled restore tails; and fresh replacement operations after cleanup. Both independent source-review perspectives are clean on the exact frozen candidate. This protects the public processing entries and their awaited work; it does not establish a global epoch guard for native event tasks before those calls.
At
1f1bead0, final local validation on Xcode 26.1 (17B55), iPhone 16 / iOS 26.2 (23C54) passed 3,105 selected unit tests and 40 UI invocations, zero failures, with three explicit disabled Trezor skips. The 31 focused final-entry tests also passed. All 1,700 inputs were unchanged. The complete 25,885-line build/test log, all 3,148 nested invocation summaries and eight diagnostic logs were audited. Five QoS warning records represent six logged occurrences in native SDK/LDK calls; causal attribution remains unresolved, so they are not described as pre-existing or warning-free. Two AppIntents notices and expected platform/failure-path diagnostics remain. SwiftFormat 0.64.0 reports 0/862 files requiring formatting, 94 skipped, with no formatter warning. Diff checks pass. The previously documented live-service, native-USDT and local-Arbitrum exclusions remain exclusions, not passes.At
1f1bead0, all seven PR commits against freshly fetched base6c0a1607are cryptographically verified locally and by GitHub (verified=true,reason=valid) for the identical commit set. Synthetic mergef6a1e065has exactly the locally tested treec4c8025f.At
1f1bead0, formatting CI passed with 0/862 files requiring formatting, 94 skipped. Its complete log and runner-image migration notice were inspected. Native CI passed on attempt 1: 3,105 unit and 40 UI invocations passed, five explicit fixture skips, zero failures. All four added tests passed. The artifact digest, exact checkout, complete 6,089-line job log, all 3,150 nested invocation summaries and all eight diagnostic logs were verified. The complete 575-object structured issue report is byte-identical to the priorf1fa5eearesult: 572 duplicate-symbol message representations covering the same 286 symbols. Ten duplicate-symbol annotations, five skip notices and one macOS runner-capacity notice are retained. No introduced actionable diagnostic issue was verified. Hosted logs report no QoS warnings; the five local warning records and unresolved attribution remain unchanged. The five fixture skips are the native-USDT case, three Trezor cases and local-Arbitrum payment case; the six configured live-service exclusions are also not passes. The migration workflow was skipped by its release-branch condition.The prior simulator receipt acceptance remains evidence for
f1fa5eea, not a new-head device acceptance run. The incomplete J1 reconnect/replacement cases, broader fault matrices and João's marker-order discussion remain recorded below. The earlier review and test passes did not expose the wipe-admission gaps now corrected.João's revised same-UUID Disconnect retry remains under discussion. The bounded source assessment distinguishes existing SDK retry safeguards from the missing read-only original-grant and outgoing-withdrawal observations needed before fresh confirmation. A focused SDK-backed follow-up was proposed, not agreed. The thread and changes request remain open. This assessment changed no source, fixture or acceptance result.
Prior head
f1fa5eeafixes contact attribution for receipts first indexed during pending profile removal. Controlled before-tests reproduced the lost invoice/address associations; a review-found mixed-snapshot case also failed before its correction. The final candidate keeps live profile admission blocked, permits bounded historical receipt lookup, validates again inside each Core write and drains receipt work before wallet destruction. Both independent final source review perspectives are clean on the exact frozen candidate.Final local validation on Xcode 26.1 (17B55), iPhone 16 / iOS 26.2 (23C54) passed 3,101 selected unit tests and 40 UI invocations, zero failures, with three explicit Trezor skips. All 3,144 invocation summaries were exported for diagnostic inspection; source hashes were unchanged through the run. The 35 focused receipt/admission tests also passed. SwiftFormat 0.64.0 reports 0/862 files requiring formatting, 94 skipped; its cache-write warning is retained. Diff checks pass. The same live-service/native-USDT/local-Arbitrum exclusions listed below remain excluded, not passed. The complete 30,890-line raw log and every nested invocation were audited: no failure summaries or compiler errors. Seven QoS warnings remain (three unit traces through unchanged native SDK constructors and four UI traces without symbols). Their prior occurrence or timing cause is not established; they are not claimed pre-existing. Expected failure-path runtime diagnostics also remain.
Manual 3 passed on the
f1fa5eeaDebug/E2E candidate built at 08:23–08:26 UTC on 2026-10-28 and installed without clearing the preserved wallet. A real native peer received the app’sbitkitlist version 7 for its native-SegWit reservation at index 8. Normal Delete Profile hit the existingpaykit_session:deletefault, retaining the durable profile UUID and mappings. One fresh 5,000-sat regtest incoming transaction was indexed with the exact peer contact key while pending. Explicit local Disconnect, cold restart and normal resync preserved the same activity ID, amount and stored contact key; after restart the marker, current assignments and assignment history were absent, with reserved indexes preserved. The immediate external plist sample still contained old mappings after marker removal; the app acknowledges cleanup through UserDefaults synchronization/readback, and this observation does not prove immediate physical persistence or power-loss atomicity. The UI showed the received 5,000 sats and no active profile. The historical key was verified in Core SQLite; the deleted live contact name is not displayed. The transaction is unconfirmed; no blocks were mined and no prior wallet operation was retried. Helper export was fixture-only, exact-identity/list/network restricted, mode 0600, and passed its selection/network controls, format and strict lint.All six PR commits are cryptographically verified locally and GitHub reports
verified=true,reason=validfor the identical commit set against base6c0a1607. Synthetic merge8ebc0ac2has exactly the published head’s tree.At
f1fa5eea, native CI passed on attempt 1: 3,101 unit and 40 UI invocations passed, five explicit fixture-dependent skips, zero failures. All 3,146 invocation summaries, the complete structured issue report, the job log and all eight exported diagnostic logs were audited. The 575-object issue report is byte-identical to the prior91379a54report: 572 duplicate-symbol message representations plus three containers, covering the same 286 symbols. No introduced actionable diagnostic issue was verified. The setup probe’s broken-pipe exception and simulator/network diagnostics remain recorded; this is not a claim that all logs are clean. No hosted QoS warnings were reported, without changing the local warning limitations above. The five skips are the native-USDT fixture case, three Trezor UI cases and the local-Arbitrum UI case; configured live-service exclusions are also not passes.At
f1fa5eea, formatting CI passed with 0/862 files requiring formatting, 94 skipped. Its complete log and runner-image migration notice were inspected. The migration workflow was skipped by its release-branch condition. Prior hosted results below remain attributed to91379a54.João’s proposed delayed marker is intentionally not applied: the durable obligation precedes the first possible remote mutation, and uncertain attempts remain local-only recovery. See the contract disposition. The review thread and changes-requested state remain visible.
Prior head
91379a54adds only two test synchronization fixes to60b1485b: the sharing test waits for actual ownership after admission, and the profile test lets keyboard setup finish before its unchanged two-second admission assertion. Missing keyboard presentation is a bounded, non-failing setup outcome; the admission and replacement-owner assertions still execute. No production behavior or test cases were added or removed. A delayed-entry control failed the old sharing barrier and passed the repair; an unavailable-notification control timed out twice and still passed the original profile assertions. One independent combined review and its bounded correction recheck are clean.The final local gate on this exact patch passed 3,093 selected unit tests, zero failures and zero skips; all 1,699 source inputs were unchanged. Both keyboard setup waits completed. The full 23,225-line log and all 3,093 nested results were inspected: two AppIntents notices, three QoS warnings and runtime/service diagnostics remain, with no verified introduced actionable finding. SwiftFormat 0.64.0 reports 0/861 requiring changes, 94 skipped; diff checks pass. The workflow's six live-service exclusions and the separately owned native-USDT fixture case were excluded, not passed. A preceding run passed the other 3,093 tests but failed that opt-in case after discovering an active fixture. Its failure is preserved: it changed fixture controls/tip; the owner's read-only comparison found stored ciphertext and the original signed operation unchanged, with no new backup acknowledgement or broadcast event. This case remains unvalidated here.
All five PR commits against
6c0a1607are locally signature-verified and GitHub reportsverified=true,reason=validfor the identical commit set. Synthetic merge66261d34has exactly the locally tested tree2a1cd5f2. New-head native CI passed on attempt 1: 3,093 unit successes and 40 UI invocation successes, with one unit and four UI skips, zero failures (3,138 total invocations). The complete job log, artifact, every nested test result, structured issues and all annotations were inspected. Its 572 duplicate-symbol issue representations exactly match the preceding run; they are not new diagnostics. Formatting CI passed 0/861 files, with 94 skipped; the complete log and annotation were inspected. The migration workflow is skipped by its release-branch condition.The original ready-triggered native run, attempt 1 on
60b1485b, failed two unit tests: sharing ownership ordering and profile admission timing. It passed 3,091 other unit tests and 40 UI invocations, with five explicit fixture/Trezor skips. Complete job logs, annotations, artifact digest and every nested test result were audited. A keyboard-queue timeout overlapped the profile waiter; that observation does not establish historical causation. The two-test repair above preserves all original behavior deadlines and ownership assertions. The old run was not retried unchanged.Prior published head
60b1485bcomposes parent6c0a1607; its treebf36b7d2is exactly the frozen candidate tested below. One combined final run on Xcode 26.1 (17B55), iPhone 16/iOS 26.2 (23C54), Debug, two jobs and nonparallel testing passed 3,093 unit tests plus 40 UI invocations (11 logical UI cases), with zero failures and three explicit Trezor-emulator skips. The 30 launch invocations account for the repeated UI count. All 1,699 captured source inputs matched afterward. Both Profile Recovery and all four Contact Import cases passed; all 46 mapped Manual 1 identifiers passed in the same run. The workflow’s six live-service unit exclusions, opt-in native-USDT recovery class and local-Arbitrum payment UI case were excluded, not counted as passes.The designated auditor inspected the complete 27,329-line raw build/test log, all 3,136 nested invocation summaries, action diagnostics and eight exported diagnostic logs. No actionable introduced diagnostic finding was verified. Two AppIntents notices, five nested QoS warnings (six raw occurrences), and offline/rate-limit/simulator diagnostics remain; passing assertions do not establish external-service availability, and exact new/old timing attribution is unproven. Full SwiftFormat 0.64.0 reports 0/861 requiring changes (94 skipped; a cache-write warning was retained). Complete translation diagnostics report zero errors and 15 warnings exactly equal to parent
6c0a1607; diff checks and both profile XML journeys pass. The independent source review and affected-parent recheck are clean. At that revision, all four commits in the parent-relative PR range were cryptographically signed, locally verified and confirmed by GitHub asverified=true,reason=valid; GitHub’s PR commit set exactly matches the locally verified range.Before readiness, hosted SwiftFormat passed 0/861 (94 skipped). Its complete log and annotation were inspected; the sole annotation is GitHub’s Ubuntu runner-image migration notice. The checked synthetic merge
ab5479d7has exactly treebf36b7d2, matching this head and local validation. The pre-readiness hosted unit/E2E jobs were skipped while the PR was draft, and are not passes. The ready-triggered result is recorded above.Authenticated simulator evidence was collected on the Debug/E2E build made 2026-10-27 17:35:48–17:40:25 UTC: then-HEAD
2ca19ed3, pending parent8592e58b, and the reviewed local recovery fixes now preserved by signed commit0eef45d1. All 1,692 captured build inputs matched before/after. The final parent composition changes 33 paths, plus the later J2 description clarification; this installed evidence remains attributed to that candidate. Core removal/coordinator, credential, contact and package inputs are unchanged. The source recheck preserves that bounded component evidence. Previous foreground/reconnect and later WALLET-backup resumption observations remain historical for this composition because AppScene and BackupService changed; final Home/USDT presentation also needs its own validation.J1 observed a real consumed-list version 1 established by an incoming-request payment, profile DELETE committed with a lost acknowledgement, no automatic deletion replay, separately confirmed local completion, cancellation, genuine remote profile/contact edits after uncertainty, and recovery across cold launches after credential removal without an active identity. Ordinary Receive remained available. Separate recreated profile cycles exercised session/root deletion, shared enumeration/deletion, preferences and final-marker failures on the preserved wallet/principal. These separate cycles do not all contain the original consumed-list watermark or a nonempty override.
J2 observed private withdrawal pending, uncertain native contact removal with a retained nonempty override, partial removal retaining the exact actively leased peer, pre-storage profile refusal, and DELETE returning HTTP 404 while fresh GET still returned the unchanged profile. The exact reviewed fault configuration and rejection journal prove the 404 refusal before storage. Credentials and the pending UUID survived Cancel/cold; explicit local completion cleared local tracked credentials/marker/identity while retaining the seed and remote profile. The native SDK sanitizes the error to HTTP 404; no direct client response/error-text capture established a literal
404 Not Foundmessage across FFI. The J2 description now states the actual HTTP status boundary; its action IDs are unchanged.Ordinary authenticated Disconnect was tested separately with a valid uniquely identified grant and genuinely absent remote profile: its own grant/session was revoked, tracked local credentials cleared, unrelated grants and the linked contact retained. Local Disconnect after uncertainty instead completed the durable local obligation; it does not claim fresh remote deletion or authenticated revocation.
Manual 1 has 46 passing tests mapped across 14 native scheduler boundary groups on the prior 3,071-test candidate. Production orchestration holds admission/SDK/publication/rollback/local-install/UI-completion races with injected continuations or noPointer SDK subclasses. Three other tests exercise real Rust-to-Swift provider callbacks for typed failure/retry. Those layers do not demonstrate an installed native callback held during wipe/replacement, or every concrete coordinator-stage-by-successor permutation. All 46 exact mapped test identifiers also executed and passed in the final composed-candidate run; this remains scheduler/component evidence, not the unexecuted installed J1 action 12.
Manual 2 has actual fault/cold-recovery observations. A fresh shared-own deletion follow-up independently retained the same pending UUID through cold launch, then cleared credentials/marker only after explicit local completion; identity was absent after 30.0662 seconds and a second cold launch, with seed/reserved indexes retained and own PUT/DELETE counters quiet. Its immediate preference sample still contained the old identity. The audit rejected an accidentally selected old log excerpt and corroborated the new run's timestamped fault directly. Earlier samples retain their historical limitations.
The current recovery delta received one independent combined source review and a separate risk review of the credential/persistence fault controls. The final-parent interaction/conflict delta was reviewed separately without repeating unchanged coverage. Final reports retain exact input hashes, fixture/configuration identities, warnings, failed attempts and limits.
Earlier published-candidate evidence (retained at its original revision)
Head
2ca19ed3only renames the unchanged changelog fragment to this PR number. Both actual-base commits are cryptographically signed, locally verified and GitHub verified=true/reason=valid. Product commit8b6cd5de(base3a7210b0) passed 3,004 selected native tests with zero failures or skips; all 1,681 frozen input hashes matched afterward. The complete 20,974-line build/test log and nested XCResult issues were inspected. Two AppIntents build notices, an Xcode physical-device enumeration assertion and three native SDK/DHT QoS observations remain; their timing cause is unproven. The simulator run completed. The opt-inUsdtNativeRecoveryIntegrationTestsrequiring its separate localhost protocol fixture was excluded in addition to the workflow’s six integration exclusions; none of those exclusions counts as a pass. Full SwiftFormat 0.64.0 reports 0/856 files requiring formatting. Translation validation reports zero errors and 15 existing warnings, individually compared with actual base3a7210b0; none was introduced.The local runs used Xcode 26.1 (17B54) and the owned iOS 26.5 simulator.
On the final UI fixture, both Profile Recovery cases and all four Contact Import cases passed with zero failures or skips. Complete logs and nested XCResults were inspected. The local failure case retained the same opaque recovery UUID, credentials and consumed-list watermark, rejected fixture restoration while pending, preserved cancellation, and completed only after explicit local acknowledgement. The fixture counts no remote cleanup calls and isolates terminal navigation from live Ring discovery. These controls do not establish authenticated network behavior. Three actual recovery screens were also rendered, exported and visually inspected in the native hosted-view suite.
Controlled failures cover stale publication admission, cancellation preference retention, credential/local-cleanup acknowledgement, initial readiness, required-local-error fallback, native signup after revocation and original callback ownership. Earlier failing/interrupted candidate runs remain in the evidence; they are not historical incident attribution. Two consecutive independent final source reviews (7 and 8) are clean on the same frozen candidate. Both reviewed correctness, callers, failure paths and repository fit; their validation and authenticated-journey limits remain explicit.
Current-head hosted formatting passed 0/856. Its complete 193-line log was inspected. Native/E2E jobs were skipped because this PR is a draft and are not passes.
The earlier Setup used Paykit 0.1.0 and VSS 0.5.23. The following was the gap status at that time, superseded only by the bounded observations above:
J1/J2 and complete authenticated fault journeys remain unrun. The shared Docker API failed two bounded probes; it was not reset, and independent native/source work continued. Temporary-file/keychain/native-mock controls and installed view assertions do not establish deployed homeserver behavior, a physical-phone journey or a real wallet reset. Native contact CAS and safe remote observation remain explicit limitations. This PR remains a draft while its acceptance gaps are open.
Gaps
Receipt attribution intentionally fails closed for request-only mappings with an empty reservation ledger during profile removal, and never reconstructs erased mappings. The actual receipt acceptance covers on-chain regtest only; Lightning receipt behavior is covered by Core-backed component tests. It does not establish wallet-wipe or replacement-identity callback acceptance.
J1 action 10 asks for test-device network reconnect. Owned endpoint outage/restart plus foregrounding establishes service recovery, but does not prove the distinct application network-change trigger. The available command interfaces expose no documented isolated connectivity control; a bounded inspection on the owned iOS26.2 simulator found no Wi-Fi/Airplane controls at the top of Settings and no search results for either term. No toggle was attempted or claimed to fail, and no network settings were changed. A verified isolated connectivity operation with an observed app reconnect event is still needed. J1 action 12 has a reviewed fixture-only gate in a separate source archive and disposable simulator built from head
60b1485b. The first attempt reached the old completion after real unlink, but iOS rejected the unregisteredpubkyauthURL scheme (-10814), and a subsequent platform safety flag stopped that run. On 2026-10-28, the installed executable, debug library and Info.plist still matched the reviewed build. The old hold deadline had passed. A device-scoped transport restart restored touch input, and a fresh normal-UI adoption/unlink cycle reached a new held completion with admission available, no identity or pending removal, and the task still active. The supported Scan/Paste route opened, but two simulator clipboard copies reported success while clipboard readback was empty; the app displayed Clipboard Empty. No signup approval or replacement B was reached. The new hold was explicitly released to end the incomplete cycle; same-token before/after-guard receipts show no authenticated identity, so this is not evidence of replacement protection. Production source and installed instrumentation were unchanged. Scheduler-seam coverage for Manual 1 remains separate.Only the original sequential J1 fault sequence contains consumed version 1; fresh local checkpoints do not all include that watermark or a nonempty contact override. Preference-flush refusal occurs after preference writes and does not prove asynchronous disk writes were prevented. Samples are non-atomic, indexed credential counts are not a complete keychain audit, and navigation is not proved to await physical preference persistence. Receive checks do not include a separate decoded-QR/nonreuse proof. Own counters cover authorized PUT/DELETE, not all GET/LOCK/auth traffic.
A portable shared authenticated fixture recipe, real wallet wipe, physical-phone behavior, native contact CAS and safe remote observation/replay are not established. Parent/old-candidate runs are not counted as final-head runtime passes. The two literal J1 app-acceptance cases remain incomplete; component checks do not substitute for them. Ben explicitly authorized marking this PR ready for review on 2026-10-28 even if these checks cannot be completed. The remaining verification is retained for reviewers: action 10 needs an authorized disposable device or verified simulator-local control that causes an actual reconnect event while preserving the app and unrelated fixtures; action 12 needs working input delivery, genuine activation of B after admission permits it, and delivery of a newly held old callback with B's screen unchanged. The isolated fixture and evidence are preserved, with no callback left intentionally held from the follow-up. Native contact CAS and full-WALLET redesign remain out of scope.
The pinned native Pubky
revalidateAPI alone is insufficient for the open Disconnect follow-up: an uncoordinated GET cannot distinguish a stale bearer from revocation of the original grant, while coordinated validation may refresh the bearer. Cold grant import also creates a fresh bearer. The original-grant and outgoing-withdrawal read-only outcome contract remains unimplemented.Models used
gpt-6-astra(reasoning:xhigh)gpt-6-astra(reasoning:xhigh)gpt-6-astra(reasoning:xhigh),gpt-6.1-sol(reasoning:high)