Skip to content

fix: bind profile removal to its original owner - #1143

Open
ben-kaufman wants to merge 34 commits into
fix/paykit-sdk-wipe-queuefrom
fix/profile-removal-parity
Open

ben-kaufman wants to merge 34 commits into
fix/paykit-sdk-wipe-queuefrom
fix/profile-removal-parity

Conversation

@ben-kaufman

@ben-kaufman ben-kaufman commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

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

  • Capture the original owner and one-use confirmation before cleanup, with the profile revision and contact snapshot. Drain admitted SDK, publication, private-payment and caller-local work before teardown. Stale callbacks cannot mutate or navigate a replacement identity, including a new session for the same key.
  • Persist recovery before authenticated reads or destructive work. Fresh confirmation can resume unfinished public cleanup for ordinary Disconnect after complete private acknowledgement, or retry unknown logout, under the original live grant, runtime, owner, client and signer. Acknowledged cleanup never replays. Same-grant bearer refresh is allowed; proof negotiation starts after confirmation. Logout acknowledgement permits local completion; restart or ownership loss leaves confirmed local Disconnect.
  • Acknowledge credential erasure before clearing consumed-payment-list watermarks and local metadata. Verify required preference, keychain and reservation cleanup, retain recovery on failure, and clear its exact marker last. Ring-owned credentials and reservation indexes/restore ceilings are preserved.
  • Keep interrupted profile cleanup and wallet-wipe recovery reachable without authenticating retained credentials. Public SDK reads and ordinary receive accounting remain independent. Complete private WALLET capture fails closed while identity recovery is unresolved; it never uploads a partial or stale replacement.
  • Preserve existing invoice and reserved-address contact associations for ordinary incoming receipts while profile removal is pending. Hold an immutable attribution snapshot through the actual Core write; cancellation, unavailable recovery and wallet wipe invalidate it, and wipe drains outstanding receipt writes. Ambiguous or erased mappings never produce a new association.
  • Refuse new receipt indexing and backup restore work during wallet wipe or unreadable recovery. Track payment enumeration, queued/detail writes and VSS/legacy/USDT restore tails through completion so wipe cannot clear storage before admitted writes finish. Foreground and reconnect restore retries honor the same recovery gate.
  • Refuse profile deletion and unlink while any active payer or payee subscription remains, including records from another app or without a saved contact. Recheck before cleanup, strict contact removal and the confirmed profile deletion; keep the original attempt and local recovery when a late check refuses cleanup. Offer a separate fresh confirmation for local Disconnect after a subscription refusal, without changing server profiles, subscriptions or access.
  • Require a positive acknowledgement for each intended private-list withdrawal's exact counterparty and outbound message ID before later cleanup. Missing reports, empty failure arrays, drained queues and stored Sent rows are insufficient. Partial and reservation failures retain the affected local state and stop removal.
  • Cancel retained publication and retry-idle work when its original profile is revoked, and revalidate SDK contention retries before native re-entry. Deferred startup callbacks retain their original owner after session transitions.
  • Keep sharing, contact imports, profile editing and creation/adoption UI effects under their original owner through completion and rollback. Cancelled sharing retains cleanup obligations without replaying remote work.

Out of Scope

  • Native contact batch compare-and-swap and cross-device edit/re-add/unblock/ABA protection remain under Bind profile deletion cleanup to the original sign-in bitkit-android#1687. The captured profile revision uses the existing native profile CAS; key-only contact deletion has a different, unresolved guarantee.
  • Remote retry after uncertain contact or profile cleanup remains unavailable. Native contact CAS is still required; an old bearer or persistent recovery record never authorizes new remote cleanup.
  • Full-WALLET format splitting, server migration enforcement and release/deployment changes are separate work. This PR does not claim to resolve historical contact/HTTP incident causation.

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 pins: Bitkit Core 0.8.0-rc11, Paykit 0.1.1-rc4, VSS 0.5.26-rc1 and LDK 0.7.0-rc.78. RC4 resolves to d56e051b4b1479a8da30c0a05f3fade868ff6477; released XCFramework SHA-256: 317add9f1b537a01345ecfc706d4d3f4d622af806e3283c172e48ab70b8e451d. Select an available concrete simulator and the repository unit selection in .github/workflows/unit-tests.yml. The controlled UI checks can be run with xcodebuild 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, Homegate http://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 are paykit_session:delete, pubky_secret_key:delete, shared-own:enumerate, shared-own:delete, preferences:flush, reservations:acknowledge, marker:persist, or marker: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.

  • J1: profile-removal-recovery.xml — original confirmation, uncertain cleanup/local Disconnect, restart and ordinary receive. Added actions 14–21 require original-grant proof retry, same-grant refresh, replacement/ABA rejection and actual tail draining; historical installed results do not establish these new expectations. Actions 22–24 cover endpoint/app failure continuation; the native zero-target registry case passed, while nonempty-target coverage remains open.
  • J2: updated profile-cleanup-failure.xml — incomplete cleanup stays a failure; local Disconnect remains separately confirmed. Action 6 now also requires an exact positive private-withdrawal acknowledgement; earlier installed results do not establish this added acknowledgement expectation. Actions 12–13 cover first-attempt withdrawal while sharing is disabled. Added actions 14–16 cover deleting a contact before profile cleanup: contact deletion clears blocked-peer state; later profile removal sends it no withdrawal, while remaining peers still need exact acknowledgements. Earlier runs do not establish these added actions.
  • J3: new profile-fixed-term-disconnect.xml — refuse profile removal during an active finite subscription, cancel the separate local Disconnect confirmation, then explicitly finish local cleanup while preserving server state. Component coverage and source review are recorded below; this candidate's authenticated simulator journey remains unrun.

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.

  • 4. On a fresh disposable wallet, inject malformed or unsupported reservation data. Profile-only cleanup must preserve it and refuse completion; full wipe must discard it after credential acknowledgement. Fail session and shared credential erasure separately: reservation bytes and consumed-list state must remain. Requires controlled storage injection; never alter a retained wallet.

Current authored QA status (bounded to the recorded evidence):

QA ID Status Evidence and limits
J1 — profile-removal-recovery.xml Incomplete Literal actions 10 and 12 and the exact evidence limitations remain in Gaps.
J2 — profile-cleanup-failure.xml Incomplete Payer/payee refusal and post-cancellation cleanup observed; one additional payer/unlink refusal has zero authorized mutation attempts in its scoped interval. Other runtime and fault-evidence limits remain.
Manual 1 Incomplete Scheduler/component coverage does not establish installed replacement/wipe callbacks.
Manual 2 Incomplete Recorded credential/final-marker boundaries retained; independent real-watermark cold cycles and an installed reservation-acknowledgement fault remain limited.
Manual 4 External storage-fault pass at 065a QA: malformed/version-2 ledgers and separate credential faults checked across restart. Watermarks injected; no consumed-payment or power-loss proof.
Manual 3 Passed within its recorded scope Existing checked on-chain regtest receipt case; its persistence, confirmation and candidate limits remain below.
unlink-ring-pubky.xml, actions 1–10 Passed Genuine Ring adoption/unlink; Ring identity and public profile retained.

The 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.

RC4 includes the native original-grant proof execution and disabled-capability empty-withdrawal fixes from #248 and #249. This app captures that live handle before cleanup and retains its full native/local tail. It never substitutes ordinary sign-out or reconstructs retry authority after restart. Disabled sharing still requires an acknowledged empty withdrawal without registering or re-enabling sharing.

Automated Checks

  • Updated ProfileRemovalCoordinatorTests.swift and PubkyProfileManagerTests.swift with five tests for signed-out Save feedback, startup restoration, identity replacement, and contact actions and confirmations during wallet wipe.
  • Admission/session/wipe suites cover original-owner capture, cancellation, actual native/local tail draining, persisted and unreadable recovery, queued payment writes and restore failures.
  • Removal/store/reservation suites cover exact durable obligations, one-use confirmations, profile CAS, missing-source cleanup, subscription refusal for both roles and other apps, finite/unknown expiry, and required local acknowledgements.
  • Original-access/revocation/lease suites cover live original-grant capture, confirmation before proof, logout-only retry, same-grant refresh, credential/source ABA retirement, replacement protection, acknowledged local completion and idle/active lease release.
  • updated PaykitContactLifecycleTests.swift, PubkyProfileManagerTests.swift — blocked-peer local cleanup, strict remaining-peer ACKs and wallet-only ledger discard after credential acknowledgement. Existing SDK/keychain/contact/sharing/backup coverage retains late callbacks, import generations, cold reads and disabled-sharing withdrawal; auth cancellation uses isolated gates.
  • Core-backed receipt tests cover current/historical invoice and reserved-address attribution during cleanup, immutable mixed-output snapshots, cancellation, wipe draining, same-seed replacement and mixed request/reservation mappings.
  • Public address/sharing tests cover preparation and publication tails during removal/wipe, retained revealed indexes, same-owner cancellation repair and rejection of successor-state mutation.
  • Hosted Profile views cover three recovery states and original-owner readiness. Controlled ProfileRecoveryUITests cover failed local cleanup, retained UUID/watermark, canceled confirmation and explicit completion; four ContactImportUITests cover outstanding saves and navigation after leaving.
  • Full native selection, SwiftFormat, translation diagnostics and revision-specific results are recorded below. Controlled fixtures do not establish authenticated journeys.
  • updated PrivatePaykitServiceTests.swift — covers a saved-contact lookup that completes after profile removal reserves admission. The lookup must fail before it updates the contact cache.

Evidence

Current head 0f2b654. Add Contact, Edit Contact, and Contact Detail can use the session restored for their original wallet and profile. A signed-out Save shows the sign-in warning. Identity changes and wallet wipe invalidate pending actions. Ovi reproduced the silent Save on 43c72a5. All five new session tests passed locally and in CI. Repeating the public-link Save taps on this head is still open. Contact fix and review.

The iPhone 16 simulator passed 3,376 distinct tests (3,405 executions), with five skips and zero failures or retries. Seven runtime advisories also appeared on the parent or previous head. SwiftFormat reported zero violations across 867 files locally and in CI. Translation validation had zero errors and the same 17 warnings as the parent. GitHub verified all 34 PR commits.

First hosted native run failed. Of 3,410 executions, 3,404 passed, five skipped and one failed. In testExternalLnurlHandoffDoesNotTransferOwnershipToOpenSend, the open Send sheet had an unexpected generation after an external scan reset. The replacement then lost its LNURL confirmation route and amount. Those three assertions failed. The same test passed locally on this tree. Its cause remains open. All nine migration jobs skipped. The failed-withdrawal findings and manual J1/J2 gaps below remain open on RC4. Native CI · SwiftFormat · Migration.

Previous head 43c72a5 composes #1125 at 814a837 and keeps Paykit 0.1.1-rc4. A saved-contact lookup rechecks profile admission before caching its result. Profile removal logs the failed stage and error category without logging wallet or peer data. The SDK queue distinguishes completion, deferral and failure.

The full local Debug suite on iPhone 16 / iOS 26.2 (Xcode 27.2) passed 3,371 distinct tests, with five fixture skips and zero failures or retries. Counting all 30 declared launch configurations, 3,400 executions passed. The first build exposed an RC2 test fixture missing RC4’s required acknowledgement field. The corrected fixture, late contact lookup, Jade test and all launch configurations passed on the published tree. SwiftFormat passed 0/866. Translation validation found zero errors and the same 17 warnings as the parent. The local audit retained five QoS findings, one SwiftUI state finding and two AppIntents advisories.

The failed-withdrawal reproduction remains open on released RC4. J2 action 15 hit the native policy error on two identities. J2 action 16 and J1 actions 10 and 12 still lack end-to-end evidence. The SDK change is awaiting release and app integration. Native reproduction · Paykit #267.

GitHub verified all 33 commits against the parent. On the first hosted run, 3,400 executions passed, five skipped and zero failed. XCTest retried the Restore tap twice inside one passing test. It reran zero test cases. The 286 duplicate-symbol warnings matched the parent, and a system-gesture timeout was recorded during a passing USDT receive test. Hosted SwiftFormat passed 0/866 with 120 exclusions. All nine migration jobs skipped. Native CI · Formatting · Migration.

Runtime f9890dc, docs 065a4c6 (Nov 5): f989: 261/261 focused; local/CI 3,363 pass, 5 skip; 0 failures. Local: 0 retries, 8 advisories; CI: 2 tap retries. Format 0/866 (118 excluded). All 31 commits signed/verified. Docs XML/diff pass; runtime unchanged. CI on 065a4c6: 3,359 pass, 4 fail, 5 skip (inherited Jade test race; 3 launch timeouts); audit complete; launch cause unresolved. Lint HTTP504, then pass after one owner retry without the required grant. External QA confirms native policy_error after failed withdrawal (2 identities); no-fault controls reached onboarding. João retracts “any contact.” Shared/UncertainClear unknown; J2 16 unrun.

At fc4b5b7 (Nov 4), #1125 6306930 added wipe/startup recovery: expired cleanup checks ownership; native tails retain admission. Focused 5/5. Local Xcode27.2/iOS26.2: 3,361 pass/5 skip, 1 tap retry, 7 advisories. CI: same counts, 2 tap retries; no test reruns. Format 0/866 (118 excluded); 17 verified parent translation warnings, 286 prior symbols/extractor limit, migration skipped. Reviews clean; 29 signatures locally/GitHub valid. Compile errors fixed; Xcode26.1 bootstrap timeout unexplained. No new native acceptance; prior gaps remain.

At 21d5ee2 (parent 5f5fcf5), local and hosted gates each passed 3,190 tests with five skips; hosted had two Restore tap retries, no test reruns. All 27 commits verified. The earlier 40be059 full-local clipboard failure (missing NSItemProvider staging file) remains unexplained despite focused/CI passes; the parent QR-copy failure is separate. Native author evidence stays at 630be75. Prior reports: ios1143-parent5f5f-20261103, hosted-ios1143-21d5-20261103, and earlier revision-specific evidence.

2c6cbdbf (base 53b67b07) adopted Paykit 0.1.1-rc3 with exact private-withdrawal acknowledgements. Released ZIP/all seven cached XCFramework files match; other pins unchanged. Fifteen receipt scenarios/all three removal actions reject missing, conflicting or partial acknowledgements; an empty queue is not success.

At 6e5dccb6, the local Xcode 26.1 / iPhone 16 / iOS 26.2 unit selection passed 3,012 tests with zero failures/skips and 1,685 unchanged inputs. All 22,286 log lines and nested results were inspected; three native DHT/Pubky QoS observations, two AppIntents notices and simulator appearance/haptic output are retained without assigning an introduction/timing cause. Earlier compile failures and the 173-pass/one-failure attempt remain preserved; the finite-expiry assertion then passed focused and full reruns.

Hosted run 41004930635 at 6e5dccb6 failed: 3,011 unit executions passed, one test failed two assertions, and one skipped; UI had 40 passes/four skips. All 6,018 log lines, 3,057 nested results and 19 annotations were inspected; the artifact digest and 7,383 original entries/CRC match. The 571 raw linker diagnostics/286 symbols match audited 08f29903. Five skips require owned USDT/Trezor fixtures. Modern counts collapse 30 launch executions to one; the formatted log omits 31 individual pass lines confirmed by XCResult.

The failed auth-cancellation test shared one admission gate across independent cases; a cancelled publication can retain a revoked scope after its caller returns and reject the next case. Independent review supports isolating each case's gate, with no product, timeout or assertion change. Original and repaired tests each passed 50 local repetitions; the exact CI scheduling interleaving is inferred. The repaired full local selection passed 3,012/0 failures/0 skips with 1,685 unchanged inputs after one preserved simulator Busy launch failure. SwiftFormat: 0/834 (102 excluded). Unchanged translations retain zero errors/15 warnings verified against 53b67b07; XML/diff checks pass. All 17 PR commits verify locally and on GitHub. Unit/UI run 41010521651 passed 3,012 unit and 40 UI executions, with five fixture skips. Complete logs/results and artifact digest/CRC audited; hosted lint passed. Evidence: work/validation/ios1143-rc3-integration-20261030/ and ios1143-cancellation-isolation-20261030/.

Local gates exclude the workflow's four integration classes and the separately owned USDT fixture; no standalone UI or authenticated journey ran on the wallet-bearing unit simulator. J1/J2/J3, Manual 1/2 and the SDK execution limitation remain bounded below. Migration was skipped.

At earlier head 08f29903, artifact 12879768977 matched GitHub's SHA-256 and all 6,133 entries passed CRC; merge dbfd6cea had the head's tree. Complete root/build/per-test/diagnostic inspection reconciled 2,993 executions: 2,988 passed/five skipped. Modern results collapse 30 successful launch executions to one. The 286 distinct build warnings matched saved logs; runtime/per-test issue containers were empty, while raw logs retained six UI USDT refresh failures and a missing graph-cache warning. Original files stayed unchanged; the reader added only database.sqlite3. This was not complete USDT acceptance; Manual 2 remained incomplete. Evidence: work/validation/ios1143-subscription-composition-20261029/hosted-08f29903-xcresult/.

Hosted CI for 08f29903 has 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 successful acc028f4 run 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 in work/validation/ios1143-subscription-composition-20261029/hosted-08f29903/.

Earlier head 08f29903 adds the opt-in reservations:acknowledge fault 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.

The frozen candidate that became 08f29903 passed local validation on 2026-10-29: SwiftFormat 0.64.0 0/831 (95 excluded), E2E simulator build, and 2,948 unit tests with zero failures and one owned-USDT-fixture skip. Structured diagnostics had no compiler errors, warnings or test failures; raw Apple AppIntents notices remain. Tracked inputs matched before/after. All 11 PR commits against ef1286d8 verified locally and on GitHub (verification.verified=true, reason=valid).

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 acc028f4 added 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 from 95195079.

On tree 3959dd8d, all 2,948 selected unit tests passed with zero skips or retries. The 12 focused profile-screen tests passed. SwiftFormat reported 0/831 and translation validation matched the parent’s 15 warnings.

All ten commits against ef1286d8 verified locally and on GitHub. Hosted merge 67b19194 matched the tested tree. CI passed 2,948 unit tests and 40 UI executions with five fixture skips. Native run.

This run retained 286 linker warnings matching parent ef1286d8. XCTest retried the fixture Restore tap twice and the test executed once. The five skips required a recovery fixture, local Arbitrum or disabled Trezor cases.

SwiftFormat passed 0/831 with one runner-image notice. All nine migration jobs skipped. João’s SDK-backed Disconnect retry remains open. Formatting · Migration · Discussion.

The initial bounded subscription attempt used the audited local E2E build from 95195079 on an unfunded iPhone 16 / iOS 26.2 simulator. The normal server settings saved tcp://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 advertised outgoing_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_contact removed 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 local profile-removal-v1.json was 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; acc028f4 changes 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.xml journey passed all 10 authored actions on October 29 using genuine Pubky Ring from pubky/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 95195079 composes the subscription safeguard from parent #1125 at ef1286d8 into 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.

At 95195079, all nine commits against ef1286d8 verified locally/GitHub (verified=true, reason=valid); published tree 824c2cc5 equals the tested tree. Native CI 40808362510, attempt 1, failed the Create Profile admission resumption timeout; the later key-load assertion passed and replacement-owner execution continued. A keyboard-queue timeout and invalid session message overlap the 9.734-second test without proving causation. Lint 40808362568 passed 0/831, 95 skipped, with a runner-image notice; complete logs/annotations were inspected. All nine migration jobs skipped. No runtime pass for added J2 expectations; readiness uses Ben's exception.

Prior composed head 86fb0c5c incorporates published parent #1125 at 3f887088, 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.

Local tree ce248fd2 passed 2,945 unit tests and 40 UI executions with three Trezor skips. The diagnostic audit retained five native QoS warnings of unresolved cause and two AppIntents notices. SwiftFormat passed 0/831. Translation validation matched all 15 parent warnings.

At 86fb0c5c, all eight commits against 3f887088 verified locally and on GitHub. CI passed 2,945 unit tests and 40 UI executions with five fixture skips. Its 286 duplicate symbols matched 1f1bead0. Lint passed 0/831 and migration skipped. Native run · Formatting · Migration.

The installed receipt acceptance below remains tied to f1fa5eea and 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 on f1fa5eea. 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, the local gate passed 3,105 unit tests and 40 UI executions, with three Trezor skips. All 31 focused tests passed. The diagnostic audit retained five native QoS warnings of unresolved cause and two AppIntents notices. SwiftFormat passed 0/862.

All seven commits against 6c0a1607 verified locally and on GitHub. The hosted merge and local validation used tree c4c8025f.

On attempt one, CI passed 3,105 unit tests and 40 UI executions, including all four added tests, with five fixture skips. Its 286 duplicate symbols matched f1fa5eea. Lint passed 0/862 and migration skipped. Native run · Formatting · Migration.

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 f1fa5eea fixes 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 f1fa5eea Debug/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’s bitkit list version 7 for its native-SegWit reservation at index 8. Normal Delete Profile hit the existing paykit_session:delete fault, 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=valid for the identical commit set against base 6c0a1607. Synthetic merge 8ebc0ac2 has 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 prior 91379a54 report: 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 to 91379a54.

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 91379a54 adds only two test synchronization fixes to 60b1485b: 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 6c0a1607 are locally signature-verified and GitHub reports verified=true, reason=valid for the identical commit set. Synthetic merge 66261d34 has exactly the locally tested tree 2a1cd5f2. 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 60b1485b composes parent 6c0a1607; its tree bf36b7d2 is 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.

At 60b1485b, the audit covered all 27,329 raw lines, 3,136 nested invocations, action diagnostics and eight exported logs. No actionable introduced finding was verified. Two AppIntents notices, five nested QoS warnings (six raw), and offline/rate-limit/simulator diagnostics remain; external availability and timing attribution are unproven. SwiftFormat 0.64.0: 0/861, 94 skipped, retained cache-write warning. Translations: zero errors/15 warnings equal to parent 6c0a1607; diff and both profile XML journeys pass. Independent source and affected-parent reviews are clean. All four parent-relative commits verify locally/GitHub (verified=true, reason=valid), with exact commit-set equality.

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 ab5479d7 has exactly tree bf36b7d2, 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 parent 8592e58b, and the reviewed local recovery fixes now preserved by signed commit 0eef45d1. 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 Found message 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.

Independent source and credential/persistence risk reviews covered the recovery change; affected-parent rechecks reused unchanged coverage. Reports retain input/configuration hashes, fixture identities, warnings, failures and limits.

Earlier published-candidate evidence (retained at its original revision)

At 2ca19ed3 (changelog rename only; product 8b6cd5de, base 3a7210b0), both PR commits were locally and GitHub signature-verified. Xcode 26.1 (17B54), iOS 26.5: 3,004 selected native tests passed, zero failures/skips; 1,681 inputs unchanged. Complete 20,974-line log and nested XCResults were inspected. Two AppIntents notices, one physical-device enumeration assertion and three native SDK/DHT QoS observations remained without established timing cause. The separate USDT fixture and six integration exclusions were not passes. SwiftFormat: 0/856; translations: 0 errors, 15 warnings verified against that base.

Both Profile Recovery and four Contact Import fixture tests passed; logs, nested results and three rendered recovery views were inspected. Controls retained recovery UUID, credentials and consumed watermark, rejected restoration while pending, preserved cancellation, required explicit local acknowledgement, and counted no remote cleanup. They did not prove authenticated network behavior. Independent reviews 7 and 8 were clean; prior failed/interrupted attempts remained preserved.

Formatting run 40457650414 passed 0/856 after full 193-line inspection. Native/E2E jobs were skipped while draft. Setup then used Paykit 0.1.0 and VSS 0.5.23. J1/J2 and authenticated faults remained unrun after two Docker API failures; no Docker reset occurred. Mock/hosted-view checks did not prove deployed homeserver behavior, a physical-phone journey or wallet reset. Native contact CAS and safe remote observation remained limitations. These historical results do not validate later revisions.

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 unregistered pubkyauth URL 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 and native contact CAS 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 bounded native original-grant cases above passed on RC4. Same-grant bearer refresh, the full credential replacement/ABA and linked-peer sharing-disabled matrices, and the remaining J1 cases are still incomplete. External reviewer observations remain separately attributed; this continuation does not claim those executions. Cold recovery stays local-only.

Models used

  • Planning/scoping: gpt-6-astra (reasoning: xhigh)
  • Implementation: gpt-6-astra (reasoning: xhigh)
  • Review: gpt-6-astra (reasoning: xhigh, medium), gpt-6.1-sol (reasoning: high), Unknown (reasoning: Unknown). Piotr self-reports claude-opus-5-5/high; independent runtime metadata was unavailable.

@ben-kaufman
ben-kaufman marked this pull request as ready for review October 28, 2026 04:34
@ovitrif ovitrif added this to the 3.1.0 milestone Oct 28, 2026
@ovitrif

ovitrif commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator

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 talosmachina left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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, while wipeStorage removes only wallet0/ldk and wipeAllDatabases wipes the core DB tables in place, so the obligation survives until admission.complete removes it.
  • Wallet wipe downgraded to profile recovery: ProfileRemovalStore.begin refuses a .profile record over a pending .walletWipe one.
  • Keychain wipe deletes other apps' or test-run credentials: enumeration is scoped to Env.keychainGroup with the unit-test prefix guard kept, and shared-group deletion is filtered to the ownSourceApp: prefix.
  • Unreadable reservation ledger silently replaced: clearContactAssignmentsForRemoval throws 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, handleWalletExistsChange and startWallet all return early on blocksWalletStartup, and mainContent swaps 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)

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: ⚠️ Not verified — incoming attribution regression and incomplete authenticated journeys.

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 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: ⚠️ Not verified — the open attribution issue and incomplete fault journeys remain.

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

@ben-kaufman
ben-kaufman requested a review from jvsena42 October 28, 2026 09:57

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 walletExists already 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 paykitPendingBackupRestore keychain item, a durable wallet-wipe marker, and walletExists == 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 that hasPendingWalletRestore() 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

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: ⚠️ Not verified — literal network reconnect and the complete teardown boundary matrix remain unrun.

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 piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Reviewer model: grok-4.7
Reasoning effort: high

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: ⚠️ Not verified. No findings in this delta. The same three matrices as at the previous heads are still unrun.

Findings: None. This push merges the parent stack at 9fbc9e0, which brings 4e6c06ed and the clipboard trace.

  • runScheduledBackup now removes the job and publishes running: false in one backup-queue block. A foreground retry can no longer see the category as idle while its job is still registered.
  • The cleanup block cannot be skipped on cancellation, and it adds no new lock ordering: scheduleBackup already updates status from inside that queue.
  • No code in this PR reads backupJobs or the backup running flag. The fix does not reach the wipe-admission or profile-recovery gates.
  • The one conflict, at Bitkit/Utilities/Testing/ProfileRecoveryUITestFixture.swift:46, keeps local.isPending. That is the right side: isDisconnectPending no longer exists at this head, and ProfileRecoveryUITests waits for "Disconnecting" from this flag.

QA for 40be059:

Debug build at the head, iPhone 17 Pro simulator, iOS 26.5. The build used OTHER_SWIFT_FLAGS= for the known Xcode 27 ImplicitStrongCapture diagnostics in PubkyProfileManager.swift.

Scheduled backup finishing keeps foreground retry available ✅ 149 backup, wipe-admission and profile-removal tests passed, 0 failures.
  • The new testScheduledIdlePublicationWaitsForForegroundAdmissionCleanup passed: idle was not published while the backup queue was held, and the foreground retry then uploaded once.
  • Also passed: BackupForegroundRecoveryTests, BackupServiceTests, BackupFailureNotificationGateTests, BlocktankBackupTests, ProfileWipeAdmissionTests, ProfileOperationAdmissionTests and the four ProfileRemoval* suites.

Pass results are from the xcodebuild output.

unit.2x.mp4
Profile recovery screens with the clipboard trace ✅ All 3 recovery and 4 contact-import UI tests passed with the merged fixture.
  • Failed Disconnect kept the safety state until explicit local completion.
  • Profile controls stayed read-only until the session and profile recovered.
  • The restored-profile Disconnect reached the original owner's preparation.
  • The copy checks read the key back through the Paste button, with the new trace in place.

Pass results are from the xcodebuild output.

ui.2x.mp4

Not driven (still open from the previous review; this merge does not touch these paths):

  • ⏭️ Pending profile cleanup across a network reconnect: the simulator shares the host's network and has no isolated network-path toggle.
  • ⏭️ Wrong-peer acknowledgement and open-ended cancellation matrix: the native queue builds and validates its own acknowledgements, so the app has no control that injects a mismatched one.
  • ⏭️ Every admitted native callback across replacement and wipe: this needs native callback test seams the repository does not ship.

Relied on:

Coverage:

  • Reviewed the 2bc43e9…40be059 delta, the merge's conflict resolution, and how the backup change meets this PR's wipe and recovery code.
  • No authenticated device journey ran, because the delta changes no user-facing profile behaviour.

Twin: synonymdev/bitkit-android#1693 is at 3b3b226. This delta's backup fix comes from the iOS parent stack, so the twin has nothing to match on it.

  • The twin's new commit keeps Retry visible beside Show Seed Phrase on Android's unreadable wallet-reset recovery screen.
  • iOS has no counterpart screen: an unreadable marker keeps admission closed until explicit local completion (ProfileOperationAdmission.swift:444).
  • The twin was not rerun.

Reviewed by claude-opus-5-5-low

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: ⚠️ Not verified. No findings in this delta. The same three matrices as at the previous heads are still unrun.

Findings: None. This push merges the parent stack at 5f5fcf5, which brings 11fb65d7 from #1123, approved there at that commit.

  • VssError.AuthError that wraps a request-send failure is now treated as temporary. vss-client-ng renders sign-in refusals as authorization was refused: … and bad data as invalid data: …, so neither is retried.
  • The new retryable errors take the same scheduled-retry path as ConnectionError. runScheduledBackup re-checks shouldSkipBackup (wipe and restore) before each attempt (BackupService.swift:1095), so the wipe-admission and profile-removal gates are unchanged.
  • User-visible effect: a brief sign-in outage now leaves the category pending and retrying, instead of raising the backup-failed alert.
  • The BackupService.swift hunk matches the parent's change apart from line offsets. The one conflict, in BitkitTests/BackupServiceTests.swift, keeps every test from both sides and duplicates none.

QA for 21d5ee2:

Debug build at the head, iPhone 17 Pro simulator, iOS 26.5. The build used OTHER_SWIFT_FLAGS= for the known Xcode 27 ImplicitStrongCapture diagnostics.

Backup retries when the backup sign-in request cannot reach the server ✅ All 3 new backup tests passed.
  • A wrapped sign-in send failure retried once after 10 s, and the settings backup completed.
  • A permission denied refusal stayed pending after one attempt, with no automatic retry.
  • Malformed hosts and ports, and messages wrapped twice, were not treated as temporary.

Pass results are from the xcodebuild output. The video shows the test host.

backup-and-admission-tests.2x.mp4
Wallet wipe and profile removal still block backups after the merge ✅ 160 backup, wipe-admission and profile-removal tests passed, 0 failures.
  • Suites: BackupServiceTests (including the restore-versus-wipe regressions), BackupForegroundRecoveryTests, BackupFailureNotificationGateTests, BlocktankBackupTests, UsdtRecoveryBackupTests, ProfileWipeAdmissionTests, ProfileOperationAdmissionTests and the four ProfileRemoval* suites.
  • They passed in the build run and again in the recorded run.

Pass results are from the xcodebuild output.

backup-and-admission-tests.2x.mp4

Not driven (still open from the previous review; not re-attempted at this head because this merge does not touch these paths and the blockers below are unchanged):

  • ⏭️ Pending profile cleanup across a network reconnect: the simulator shares the host's network and has no isolated network-path toggle.
  • ⏭️ Wrong-peer acknowledgement and open-ended cancellation matrix: the native queue builds and validates its own acknowledgements, so the app has no control that injects a mismatched one.
  • ⏭️ Every admitted native callback across replacement and wipe: this needs native callback test seams the repository does not ship.

Relied on:

  • The recovery-screen and profile passes at 40be059 and earlier, and João's approval at 2bc43e9. This merge leaves those paths untouched.
  • The #1123 device run at 11fb65d, which drove the identical hunk: a scheduled settings backup retried after the sign-in server was briefly unreachable, and an invalid sign-in challenge was not retried.
  • Current-head Run Tests and SwiftFormat passed.

Coverage: Reviewed the 40be059…21d5ee2 delta, the merge's conflict resolution, and how the backup retry meets this PR's wipe and removal gates. No device journey ran at this head; the sign-in outage journey for this hunk was driven on #1123 at 11fb65d.

Twin: synonymdev/bitkit-android#1693 is still at 3b3b226. This delta's backup fix comes from the iOS parent stack, so the twin has nothing to match on it.


Reviewed by claude-opus-5-5-medium

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: ⚠️ Not verified. No findings; three reconnect/native fault matrices remain unrun.

Findings: None.

QA for fc4b5b7:

The Debug app built with the repository's signing and warning-as-error settings on Xcode 27.0. Checks ran on iPhone 17 Pro simulators with iOS 26.5. App sources and package pins were unchanged. Current-head unit/UI CI and SwiftFormat passed.

Wallet wipe waits for profile work and keeps failed cleanup gated ✅ All 481 selected profile, wipe, SDK, VSS, backup, receipt and wallet-lifecycle checks passed.

Includes expired cleanup held at five SDK boundaries, profile drain deadlines, local-only wipe retry, marker/intent completion ordering, receipt writes and VSS client retirement. Results are from XCResult and the test log; the video shows the component-test host. These are bounded component checks.

focused2x.2x.mp4

Merged payment and hardware lifecycles ✅ All 538 selected send, request, allowance, amount, hardware-session and PIN checks passed.

Results are from XCResult and the test log. The same 1,019 component checks passed again in the combined run, with no failures or skips. The video shows the test host; no funded payment or physical hardware journey is claimed.

inherited2x.2x.mp4

Profile recovery and contact-import controls ✅ All three recovery and four contact-import UI tests passed, including the repeated run.

Controlled fixtures retained the pending UUID and safety state after failure and Cancel, required explicit local completion, allowed Disconnect after restoration, and prevented late imports from navigating away. These fixtures do not establish authenticated remote cleanup.

recovery-ui2x.2x.mp4

Interrupted wallet wipe survives restart and finishes after Retry ✅ Cancel retained recovery; a final-store refusal held the wipe gate across relaunch, and Retry completed after the fault was removed.

Used the unmodified installed app, an unfunded controlled local-store fixture and the committed marker:complete fault. Confirmation entered the durable wipe gate. The exact marker remained through failure and cold relaunch; after removing the fault, Retry returned to onboarding and the marker was absent. Marker identity/removal was checked in storage and completion in the app log. This case held no wallet credentials and drove no authenticated remote cleanup.

installed-wipe2x.2x.mp4

Reset a wallet while its native Lightning node is running ✅ Normal Reset stopped the real node, completed the wipe and retained onboarding after cold relaunch.

Used a fresh, unfunded regtest wallet in the ordinary Debug app. The app log recorded Node started, Node stopped, every wipe stage, successful Core database cleanup and Wipe completed, in that order. The recovery marker was absent afterward. No fault selector or code hook was used; no held native callback timing is claimed.

native-wipe2x.2x.mp4

Home header buttons and balance unit ✅ The normal app opened the drawer and Profile from the extended header hit areas, and toggled the balance unit twice.

The installed UI test passed on a newly created, unfunded wallet.

home-header2x.2x.mp4

Pending profile cleanup across a network reconnect ⏭️ No actual application network-change event was produced.

Recovery remained visible after background/foreground and a simulator configuration-service restart, with its exact marker retained. The service PID changed, but the app log contained no Network changed event. This attempt does not prove the required reconnect trigger.

network-attempt2x.2x.mp4

Native acknowledgement mismatches and open-ended cancellation ⏭️ The complete authenticated fault matrix remains unrun.

The installed controls cannot inject another peer's or message's signed native acknowledgement or stage every cancellation combination. The corresponding controlled acknowledgement and subscription guards passed in the component suites; they do not substitute for this native journey.

Every native callback across replacement and wipe ⏭️ The full installed callback-by-successor matrix remains unrun.

The available SDK, receipt, restore and VSS scheduling barriers passed, and installed durable wipe recovery passed. They do not establish every real native SDK, publication, adoption, receipt, indexing and restore callback across identity replacement and wallet wipe.

Earlier evidence retained where the delta leaves the profile paths unchanged:

  • 429a8c4 review: original-binding retirement, restored Profile ownership and public/app/logout continuation.
  • 630be75 review: durable profile-cleanup faults and fixed-term local Disconnect in owned payer/payee and Ring fixtures.
  • 3e49570 review: genuine acknowledgement, disabled-sharing and receipt cases.

Coverage: Reviewed the complete 21d5ee2…fc4b5b7 delta, both merge resolutions and reached callers. Presentation, translations and physical hardware behavior beyond the checks above received source review. The accepted unknown-private-withdrawal limitation remains unchanged.

Twin: bitkit-android#1693 fix: bind profile removal to its original owner at c228adb matches the original-owner and local-only recovery contracts. Reset and migration presentation differs by platform; Android was not rerun.


Reviewed by gpt-6.1-sol-high

@ben-kaufman
ben-kaufman requested a review from jvsena42 November 5, 2026 05:43

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

QA review

Scope: Full reassessment of the pinned PR at fc4b5b7 against 6306930, following the comparison-base change. No coverage was inherited.

2 actionable findings — resolve or provide an evidence-backed rebuttal.

Checked removal/recovery, original-owner admission, credential cleanup, subscription refusal, receipt attribution, backup/wipe interactions and affected UI completion. Comparison checks covered the corresponding contracts at Android c228adb, iOS dependency #1125 at the reviewed base, and Paykit d56e051; these were not full companion reviews.

Validation: local translation validation passed (0 errors, 17 warnings), as did the diff whitespace check. The matching CI unit/UI run passed with five skipped tests. Native tests were not run locally because this Linux environment lacks Xcode. Device testing: not performed in this review.

Authenticated J1–J3 and the authored scheduler/storage fault cases still require compatible owned services and fault injection; controlled UI/component tests do not establish those runtime outcomes.

Earlier findings reconciled at this revision: 3 still present, 5 verified fixed.

Findings

  • [MEDIUM] Deleting a contact makes later profile removal fail its private withdrawal — inline at Bitkit/Services/PrivatePaykitService+Contacts.swift:507. Still present from an earlier unpublished review.
  • [LOW] Wallet reset cannot finish when the reservation ledger is unreadable — inline at Bitkit/Utilities/AppReset.swift:176. Still present from an earlier unpublished review.

Existing concerns

Reviewer model: gpt-6.1-sol
Reasoning effort: high

Comment thread Bitkit/Utilities/AppReset.swift Resolved

@jvsena42 jvsena42 left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Delta review of f9890dc51 (blocked-peer cleanup, wipe reservations by scope), plus the five parent merges since my last review. No HIGH or MEDIUM; one LOW inline on the journey.

Checked at f9890dc51:

  • Scope plumbing. .walletWipe is passed only by AppReset.swift:176. resumeWipe requires pending.scope == .walletWipe (AppReset.swift:51), and begin refuses a .profile record over a pending wipe (ProfileRemovalStore.swift:79). Neither scope can take the other's path.
  • Wallet-wipe ordering. clearLocalAppState forgets the session and the shared credentials before PrivatePaykitAddressReservationStore.shared.clear(), so a throw leaves the ledger intact. clear() resets storedLedgerWasUnreadable, and the wipe validates ledger == .empty with defaults removed. The profile scope still fails closed on an unreadable ledger.
  • Blocked-peer exclusion. It matches state exactly: linked, linking and recovery-required peers still need an exact counterparty and outboundMessageId acknowledgement. When every peer is blocked there is no SDK call and no drain. The session, revision and admission guards are unchanged, so a replacement identity's peers can't classify the old identity's keys.
  • Merges.
    • 91eb82b and fc4b5b7: checkMutationAdmission() resolves to checkCurrent() + checkWipeOwnership(). Equivalent or stricter, since checkCurrent() already calls Task.checkCancellation().
    • The incoming abandoned-wipe test is kept, renamed testAbandonedWipeCleanupRetainsAdmissionAndCannotRecreateOldKeyState.
    • The other three merges change only fixtures or tests.
  • perf: not relevant (one set filter over an already-fetched list).
  • Android twin (synonymdev/bitkit-android#1693). Its profile-removal path still builds cleanup keys without excluding BLOCKED (PrivatePaykitRepo.kt:2257-2272). It also doesn't carry the three new journey actions. Both are parity notes for a follow-up, not findings here.

Device gate (partial): f9890dc51 — not run yet; the new profile-cleanup-failure.xml actions 14-16 (two linked simulators, withdrawal failure through the CONNECT proxy) are still to run. The device result will follow.

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

QA review

Scope: Follow-up. Re-reviewed the changes since fc4b5b7, including affected code paths, at f9890dc. Unchanged coverage is inherited from the baseline review. The previous head is an ancestor of this head, the merge base is unchanged, the delta is one commit, and the 112-file PR inventory matches the baseline with no package pin changes.

1 actionable finding — resolve or provide an evidence-backed rebuttal.

The delta has no production defect. The wallet-wipe branch discards an unreadable reservation ledger only after credential erasure, validates its absence, and stays idempotent across the wipe's retries. The profile branch keeps its strict refusal and exact-bytes validation. The blocked-peer filter is sound against Paykit d56e051: peers are unique per counterparty, a blocked peer has no Encrypted Link, the SDK parks unsent lists until a fresh handshake, and other peers still need exact acknowledgements. The one finding concerns the J2 journey premise, not product behaviour.

Parity: source comparison against Android c228adb matches for wipe order (reservations clear after the keychain wipe, with no ledger decode) and for ordinary contact deletion of a blocked peer. One difference is left for the Android review: Android's acknowledgement-required profile removal does not exclude blocked peers from its cleanup keys, so a blocked peer with retained published-list state would refuse removal there, while iOS now clears it locally.

Validation: no Swift toolchain was available, so this review compiled and ran nothing; conclusions are from source analysis and inspection of the new tests. The author reports 3,363 passed executions at this head, which this review did not reproduce. SwiftFormat passed; Run Tests was still in progress at the last check. Companion Appium coverage was inherited and not re-read.

Recommended before device testing: wait for the Run Tests result at this head, and correct J2 actions 14–15 as described in the finding.

Device testing: not performed in this review.

Earlier findings reconciled at this revision: 1 still present, 6 verified fixed.

Findings

  • [LOW] Fix J2 action 15: deletion now clears the state it expects profile removal to clear — inline at journeys/profile/profile-cleanup-failure.xml:42.

Existing concerns

Reviewer model: claude-opus-5-5
Reasoning effort: high

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

QA review

Scope: Follow-up review at 065a4c6f. I re-reviewed the changes since f9890dc5, including the affected code paths, and inherited unchanged coverage from the baseline review. Git confirms the previous head is an ancestor of this head and the base and merge base are unchanged. The PR still has the same 112 changed files.

No new actionable code findings.

No new actionable code findings in the follow-up. The only change is journeys/profile/profile-cleanup-failure.xml, which rewrites the J2 blocked-peer description and actions 14-15. Production code and native tests are identical to the baseline.

The new journey wording matches the code at this revision:

  • Contact deletion: PubkyService.removeContact logs a failed withdrawal, then still blocks the peer and removes the contact. removeSavedContacts then filters out the blocked peer, so no further withdrawal is attempted, and clears the published-list flag and pending cleanup key.
  • Later profile removal: the blocked peer stays excluded, while other linked peers still need exact acknowledgements.
  • Paykit d56e051b: block_peer parks unfinished outbound messages and clears the encrypted link. Outbound processing skips blocked peers, and remove_contact deletes only the contact record, so the blocked peer record survives for that filter.
  • Android c228adb7: a targeted check found the same blocked-peer filter in clearPrivatePaymentLists. This was not a full Android review.

Validation: the XML parses and git diff --check passes on the delta. Both were run locally. Native tests were inspected, not run, in this pass. At the last CI check on this head, SwiftFormat had passed and Run Tests was still in progress.

Recommended before device testing: wait for Run Tests to finish on this head.

Device testing: not performed in this review. The rewritten J2 actions were not executed, and they need the controlled fault fixtures named in the journey.

Earlier findings reconciled at this revision: 1 still present, 5 verified fixed.

Existing concerns

Reviewer model: claude-opus-5-5
Reasoning effort: high

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Device gate at f9890dc51. Head is now 065a4c6f2, a journey-only change (profile-cleanup-failure.xml). It rewords actions 14-15 to the behaviour my earlier LOW described, and that thread is resolved.

  • Action 14: PASS. A withdrawal failure was forced with the homeserver CONNECT proxy refusing the payee's host: Failed to withdraw private endpoints before contact deletion at 14:23:01, then removeContact finished at 14:23:06. The contact was removed, and later messages from that peer fail with policy_error, which shows it is blocked. Right after deletion privatePaykitCacheState.contacts = {} and there was no privatePaykitDeletedContactCleanupKeys, with no second delivery attempt. This also passes the 065a4c6 wording.
  • Action 15: FAIL, see the inline comment. The blocked peer got no new withdrawal (no clearPrivatePaymentLists, no tunnels to its host). Profile deletion then stopped after syncPaykitApp on every attempt that followed a contact deletion: two identities, five runs. A control on a fresh identity with no contacts deleted cleanly.
  • Not run: action 16 and the force-quit variant of 15. Action 16 depends on the same deletion finishing. I stopped when the head moved.
  • Perf: removeContact took 19.7-21.5 s. Profile deletion took about 30 s from confirm to the failure screen. No crash reports.

Evidence is in bitkit-ios/.ai/gate_ios_1143_1793887896/ (app logs, lldb throw traces, proxy log, screenshots).

Reviewer model: claude-opus-5-5 (coordinator), device gate by a claude-opus-5-5 subagent
Reasoning effort: Unknown (not exposed in session metadata)

try await validateRemoteRemovalOwner(live)
}
if !live.appAcknowledged {
try await removalServices.syncApp()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

MEDIUM: profile deletion can't finish once any contact has been deleted.

Contact deletion blocks the peer (removeContactsAndBlockPeers / blockPeer, PubkyService.swift:1281-1306), and profile removal never withdraws a blocked peer's list. This syncApp() then calls publishPaykitApp with privatePayments: false, and that call throws. Because completePublicRemoval now treats the throw as fatal, deletion stops at "Finish local cleanup", where only Disconnect is offered. The contacts and the remote profile both stay.

On the base branch (fix/paykit-sdk-wipe-queue) the same app sync went through removePublicPaykitEndpoints after the profile was already deleted. A failure there only marked cleanup pending, so this is a regression introduced here.

Repro at f9890dc (the head is now 065a4c6, which changes only the journey), on Debug simulators against staging:

  1. Use a fresh identity with no contacts and delete the profile. It finishes: syncPaykitApp 14:38:39.6 → removeContactsForProfile → deleteConfirmedProfile → keys deleted → Pubky choice screen.
  2. On the same identity, add a contact, wait for the link, then delete the contact with no fault ("Removed contact" 14:44:15).
  3. Delete the profile. It stops right after Finished Paykit operation syncPaykitApp (14:45:36), with no error logged, and shows the recovery screen with Disconnect only.
  4. Steps 2-3 failed the same way at 14:49, 14:54 and 14:59, and on a second identity at 14:26 and 14:31. An lldb swift_willThrow trace puts the throw in PaykitSdk.publishPaykitApp(displayName:capabilities:) under PaykitSdkService.syncPaykitApp.

I couldn't read the error text. From paykit-rs v0.1.1-rc4 source, the likely cause is require_app_capability_downgrade_safe (paykit-sdk/src/runtime/app_removal.rs:177-290). It refuses to drop private_payments while a counterparty still holds a shared private list or a PrivatePaymentList message is pending or failed. That fits a blocked peer whose list was never withdrawn.

Two other problems show up here:

  • "Finished Paykit operation syncPaykitApp" is logged even though the call threw.
  • The failure isn't logged anywhere, so from the logs alone the stop looks like a hang.

Possible fixes:

  • Withdraw the list before blocking at contact deletion, or clear blocked peers' shared lists in the SDK.
  • Keep the post-delete best-effort behaviour for the app downgrade.

Action 16 ("acknowledging it allows cleanup to finish") will hit the same wall.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The failure remains open. Released RC4 does reject capability downgrade for active private-list work and Shared/UncertainClear history, but the exact SDK cause in this run is still unconfirmed. I saw that the error text was unavailable. If your existing artifacts captured the app-owned private-list state at action 15, could you share only whether it was never attempted, attempted/pending, or possibly shared/uncertain clear? If that was not captured, please say unavailable. No keys, tokens, raw lists, or new rerun needed. We are preserving the withdrawal/unknown-outcome checks while tracing this.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reproduced at 065a4c6 on two staging identities after refusing only the peer’s connection during contact deletion. The native error is now captured: policy_error, private_payments cannot be disabled while app-owned work is active. Contact deletion cleared the local published flag and pending key; later profile deletion stopped at registry sync with Disconnect only. For the state question: withdrawal was attempted and failed; the exact retained native Shared/UncertainClear classification was not exported. A no-fault control completed on both identities, so the broader every-deleted-contact wording is not established. Recordings and storage checks are in this review; leaving the finding open.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The private-list state is unavailable. The gate did not export the native classification and the error text was already freed before lldb read it.

What the logs do show:

  • Action 15 identity (Payer1213): the withdrawal was attempted and failed. The proxy cut the tunnel to the peer's host, and Failed to withdraw private endpoints before contact deletion was logged at 14:23:01.
  • Third1143 case ("no fault"): removeContact finished at 14:44:02 with no withdrawal warning. The counterparty was Payer1213, which an hour earlier had been through a failed profile deletion, a Disconnect and a sign-in again. I did not confirm that its homeserver accepted the clear. So the case only shows that the app logged no failure, not that the counterparty acknowledged the clear.

ovi-reviewer's narrower scope stands: the failure is reproduced after a withdrawal fails during contact deletion. My "any contact has been deleted" wording is not established.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

43c72a5 now logs which profile-removal step failed and whether the queued operation completed, deferred or failed. Thanks for capturing the native policy_error on both identities after a failed withdrawal. The controls reached onboarding when withdrawal succeeded. The app still uses RC4, so the blocked-withdrawal case stays open. The next check is to repeat J2 actions 15–16 when the SDK fix is released and integrated, and verify that the original withdrawal is acknowledged before profile removal finishes. SDK change.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Agreed. The thread stays open until pubky/paykit-rs#267 is released and integrated. I'll repeat J2 actions 15–16 then, and check that the original withdrawal is acknowledged before profile removal finishes. Confirmed at 43c72a5: the new logging rethrows the same error and doesn't change the failure path.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@ben-kaufman Confirmed at 43c72a5: after a genuine failed private withdrawal during contact deletion, local published/pending state cleared, but profile deletion still stopped at syncPaykitApp and offered only Disconnect. The new failed-operation and policy-category logs correctly identify the failing step. The original recovery UUID remained; the native Shared/UncertainClear classification was not exported. RC4 is still pinned, so this finding remains open pending the Paykit fix being released and integrated, followed by the native completion check. Recordings are in this review.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@ben-kaufman Reproduced at 0f2b654 with the logging-only hook: a linked contact’s withdrawal failed under a connection refusal, contact deletion completed, then connectivity was restored. The native cleanup report omitted that blocked contact and matched the remaining linked peer’s clear/acknowledgement pair (message 3). The later capability update still failed with policy and retained identity/recovery. A fresh no-fault control completed deletion; a separate same-key profile recreation with a re-added Linking peer also hit policy. Paykit remains RC4 and pubky/paykit-rs#267 Retain explicit empty-list withdrawals when blocking peers is not integrated, so this finding remains open.

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: ⛔️ Request changes — profile deletion still fails after an unsuccessful contact withdrawal.

Reaudit: diff, 7 files, plus earlier findings.

Findings: No new finding. The existing MEDIUM at PubkyProfileManager.swift:1633 remains. I reproduced it on two staging identities and captured the native policy_error: disabling private_payments is refused while app-owned work is active. The no-fault control completed twice, so the failure is narrower than every deleted contact.

QA for 065a4c6: iPhone 17 Pro 2× simulators, iOS 26.5, Xcode 27.0; unfunded regtest wallets and staging profiles.

Contact deletion clears blocked-peer cleanup state ✅ Carried from f9890dc.

Checked by @jvsena42’s manual run: failed withdrawal still removed and blocked the contact, cleared its published flag and pending key, and made no second delivery attempt. The relevant production and test files are unchanged.

Ordinary profile deletion finishes after contact deletion ✅ Both no-fault controls reached profile onboarding.

Local identity and recovery state cleared; checked in the UI and stored state. Remote profile absence was not independently queried. The recording shows one of the two runs.

ordinary-delete-control2.2x.mp4

Profile deletion finishes after a failed contact withdrawal ❌ Registry synchronization stops deletion.

After the peer connection was refused, contact deletion cleared local cleanup state, but profile deletion showed “Finish local cleanup” with Disconnect only. The native policy error was checked in the debugger. The recording contains stutter.

failed-withdrawal-repeat.2x.mp4

Corrupt reservations stay strict for profiles and clear during wallet reset ✅ Manual 4 passed within the storage-fault scope.
  • Profile-only cleanup refused malformed and version-2 reservation records and retained their exact bytes and recovery UUID across restart.
  • In full Reset, separate session-acknowledgement and shared-credential deletion errors retained the ledger, injected consumed-version data and wipe UUID after restart. Removing each fault allowed Retry to finish; cold readback found no ledger, safety state or marker.
  • Ledger and safety values were checked in storage. Watermarks were injected, not consumed by a payment. An immediate preference sample lagged after one reset; no power-loss atomicity is claimed.
Malformed reservation data Unsupported reservation version
malformed-ledger.2x.mp4
unsupported-ledger.2x.mp4
A remaining peer acknowledgement allows profile deletion to finish ⏭️ Blocked by the reproduced app failure.

The combined J2 action 16 completion remains unrun: the blocked-peer registry downgrade still prevents its required final success. Component acknowledgement results do not prove that combined journey.

Pending cleanup survives a real network reconnect ⏭️ No actual application network-change event.

The simulator configuration-service PID changed and recovery stayed visible with the same UUID, but the app log recorded no new network-change event. The required real reconnect was not produced.

network-attempt.2x.mp4

Wrong native acknowledgements and open-ended cancellation retain safety ⏭️ Full authenticated matrix unrun.

The available native fixtures cannot emit another peer’s or message’s signed acknowledgement or stage every open-ended cancellation combination. Existing component checks and bounded native cases do not prove the full matrix.

Every delayed native callback rejects replacement and wipe ⏭️ Full native timing matrix unrun.

The available barriers do not hold every SDK, publication, adoption, receipt, indexing and restore tail across every replacement and wipe boundary.

Unchanged evidence retained:

  • 630be75 native review: credential faults, receipt safety, fixed-term local Disconnect and bounded native callback cases.
  • 429a8c4 review: original-binding retirement, restored Profile ownership and public/app/logout continuation.
  • fc4b5b7 review: component, recovery/import UI and running-node wipe coverage.

The current native CI run is failing; those failed tests were not rerun here. XML parsing and the delta whitespace check passed. Device coverage is bounded to the cases above.

Pair PR: bitkit-android#1693 fix: bind profile removal to its original owner, at c228adb, shares the original-owner/local-only recovery contract. Its strict cleanup still includes retained blocked-peer targets, so that behavior differs; Android was source-checked only.


Reviewed by gpt-6.1-sol-high

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Delta review at 43c72a58f (since 065a4c6f2). The delta adds no new findings. The MEDIUM at PubkyProfileManager.swift:1633 stays open until the SDK fix (pubky/paykit-rs#267) is integrated, so this review requests changes.

25ac19c48 (failure reporting): this commit only changes logging.

  • completePublicRemoval now logs the failure category for syncApp() and rethrows the same error. appAcknowledged is still set only after a successful sync.
  • The DEBUG-only withSdk log reports Failed, Deferred or Completed. Release behaviour is unchanged, and no path returns success when the SDK failed.
  • The log lines carry only the category and operation name. They contain no keys or error payloads.
  • The MEDIUM's failure path is unchanged. A policy_error from publishPaykitApp still stops deletion at "Finish local cleanup", which is what your reply says.

Merge 43c72a58f (saved-contact payment routing): every file matches the union of both sides, except PrivatePaykitService+Payments.swift and the tests.

  • The incoming saved-contact routing is intact:
    • private path check → beginContactPayment with the updated-list retry;
    • no public fallback for a saved contact with a private path.
  • This PR's profileAdmission.withOperation binding is intact. The one added line, profileAdmission.checkCurrent() in savedContact(_:), only tightens it: a removal reservation that lands between the SDK read and the cache write now throws instead of re-caching the contact.
  • The new testSavedContactLookupCannotCacheTheContactAfterProfileRemovalReservesAdmission reaches that path. The acknowledged: [] fixture fix is complete.

perf: not relevant (one in-memory admission check per saved-contact lookup).

⚠️ Device gate: not re-run for this delta. Actions 15–16 of profile-cleanup-failure.xml will run again once the SDK fix is integrated.

Reviewer model: claude-opus-5-5 (coordinator); delta review claude-fable-5-1
Reasoning effort: Not exposed (no setting in session metadata)

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

QA review

Scope: Full reassessment of 43c72a58 against 814a837b after the comparison base changed, with no inherited coverage. Included targeted parity checks against Android c228adb7 and composition with iOS #1125 at its supplied pin.

1 actionable finding — resolve or provide an evidence-backed rebuttal.

Validation: Native CI passed on merge commit 0cdaa5ab, combining the exact reviewed head and base; five tests were skipped. Locally, 37 adapted Linux tests passed for admission, leases, session transitions and recovery storage. Translation validation returned zero errors and 17 warnings.

Device testing: not performed in this review; no local iOS build or visual verification. Authenticated J1–J3 and the manual fault cases remain unverified here. They require owned Pubky/Homegate services, companion grants and transport/storage fault controls; a portable equivalent of the author’s service fixture is needed for reproduction. Native contact compare-and-swap remains outside this PR’s guarantee.

Earlier findings reconciled at this revision: 3 still present, 4 verified fixed.

Findings

  • [MEDIUM] Contact screens stay inert when opened before a session is available — inline at Bitkit/Views/Contacts/AddContactView.swift:258. Still present from an earlier unpublished review.

Existing concerns

Reviewer model: gpt-6.1-sol
Reasoning effort: high

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: ⛔️ Request changes — two existing app failures reproduced at 43c72a5.

The PR moved to 0f2b654 after this run; these results are for 43c72a5.

Reaudit: diff, 49 changed files plus open findings. No new findings.

Findings:

  • MEDIUM: Bitkit/Managers/PubkyProfileManager.swift:1634 — after a failed private withdrawal during contact deletion, profile removal still fails its Paykit app capability downgrade and offers only local Disconnect. The new failure/category logging works; the removal failure remains in the existing thread.
  • MEDIUM: Bitkit/Views/Contacts/AddContactView.swift:258 — following a public contact link after local Disconnect exposes an enabled Save button that silently returns. I reproduced the signed-out branch of the existing finding; delayed session restoration was not driven.

QA for 43c72a5:

Built the reviewed source with normal signing and warnings-as-errors settings. Tested on iOS 26.5 simulators using staging profiles and regtest funds. Eight cases pass, including five explicitly carried from linked reports; two fail and four remain unrun. Recordings are 2×; repeated recordings still contain stutter.

Saved-contact routing and profile admission ✅ Selected regression assertions pass.

Checked in XCTest results and logs: contact recaching after admission revocation, private routing, missing/wrong acknowledgement controls, removal coordination, backup metadata resumption, scanner and funding regressions all pass. The recording shows the test host; it is not the assertion source.

delta-components.2x.mp4
Saved contacts wait for a private link and open privately after mutual saving ✅ Both pending routes refuse public fallback; a reciprocal private link reaches Amount.

Contact Pay and Send → Contact showed private-connection guidance without opening Amount, even though the peer published public endpoints. A separate reciprocal-peer fixture then opened native private payment-list version 0 and Amount with a funded wallet; no contact payment was sent. The brief guidance was also checked in a full-resolution screenshot because the recording stutters.

Pending private connection:

private-link-pending.2x.mp4

Guidance captured in the still:

Reciprocal private link, aligned payer and peer:

private-link-ready.2x.mp4
Fixed on-chain requests keep their amount through confirmation and send ✅ The requested amount survives confirmation and payment.

A 10,000-sat BIP21 opened Confirm directly, retained its amount after touching the review amount, and sent successfully with a 143-sat fee. A 500-sat URI showed the 547-sat minimum; an amount-free URI opened Amount. Amount, fee and dust feedback were checked in full-resolution screenshots; successful payment and the resulting 10,143-sat debit were also checked in the UI.

fixed-onchain.2x.mp4
Profile deletion finishes after a failed contact withdrawal ❌ Registry downgrade still stops removal.

Refusing the linked peer's connection produced a genuine failed-withdrawal warning. Contact deletion cleared its local published flag and pending cleanup key, checked in persisted state. Subsequent profile deletion failed syncPaykitApp with the new policy-category log and showed “Finish local cleanup” with only Disconnect. The original recovery UUID and identity remained. The exact native Shared/UncertainClear classification was not exported. This confirms the narrower failed-withdrawal trigger in the existing thread.

failed-withdrawal.2x.mp4
Saving a public contact without a session gives feedback ❌ Save remains enabled and does nothing.

After completing explicit local Disconnect, I opened a public contact link and tapped Save twice. Add Contact stayed open without saving, navigating or showing an error. Persisted checks confirmed no identity or removal marker before or after. This adds installed-app evidence to the signed-out branch, without claiming a delayed-restoration reproduction.

contact-without-session.2x.mp4
A remaining peer acknowledgement permits profile deletion ⏭️ The combined native completion remains unproved.

Three identities and reciprocal links were prepared, and current exact-acknowledgement component controls pass. The failed registry downgrade blocks the required final success. The head moved before a separate combined withholding/acknowledgement fixture completed, so no native action-16 pass is claimed.

Pending cleanup survives a real network reconnect ⏭️ No application network-path transition was produced.

Restarting the simulator configuration service retained the recovery UUID, but produced no new application network-change event. This attempt does not establish reconnect behavior.

network-attempt.2x.mp4
Wrong native acknowledgements and open-ended cancellation retain safety ⏭️ The authenticated native matrix was not driven.

Missing/wrong acknowledgement component controls pass. Native operations and reciprocal peers ran, but available fixture routes provide ordinary operations rather than the required mismatched signed acknowledgement. The full authenticated mismatch and open-ended cancellation matrix remains unrun; further execution stopped when the head moved.

Delayed native callbacks reject replacement and wipe ⏭️ The complete native callback matrix was not driven.

Current admission and coordinator controls pass, but do not prove every native SDK, publication, adoption, receipt, indexing and restore callback against replacement and wipe. Prior bounded results retain their original limits; head movement ended further execution.

Ordinary profile deletion finishes after contact deletion ✅ Carried from 065a4c6.

The earlier installed-app report records two no-fault controls reaching onboarding and clearing local identity/recovery state. Remote absence was not independently queried. Not rerun here; the reached behavior and Paykit RC4 pin are unchanged.

Credential and acknowledgement failures preserve cold recovery ✅ Carried from 630be75.

The storage-fault report and recordings establish retained safety state and original recovery until explicit completion, within the report's owned-profile and Ring limits. Not rerun here; the reached credential, reservation and acknowledgement paths are unchanged.

Fixed-term subscriptions retain remote state during local Disconnect ✅ Carried from 630be75.

The subscription report and recordings cover owned payer, owned payee and Ring-payee refusal, Cancel, explicit local completion and cold launch. Not rerun here; those paths are unchanged.

Receipt attribution survives profile cleanup and cold resync ✅ Carried from 3e49570.

The receipt report and recordings establish that the same real regtest receipt retained its amount and historical contact key through cleanup and cold resync. Not rerun here; receipt attribution is unchanged.

Corrupt reservations stay strict for profiles and clear during wallet reset ✅ Carried from 065a4c6.

The reservation report and recordings cover malformed/version-2 ledgers and separate credential failures across cold starts. Watermarks were injected; power-loss atomicity was not established. Not rerun here; the reached wipe and strict profile-cleanup paths are unchanged.

Pair PR synonymdev/bitkit-android#1693 fix: bind profile removal to its original owner matches the owner/acknowledgement design. At its inspected c228adb head, Android also filters blocked peers; no filter parity difference is established. Android results do not prove iOS execution.

This delta includes merged payment and backup changes, rather than a full re-review of the parent stack. USDT-specific device paths, the complete native matrices above and delayed contact-session restoration were not driven. The accepted uncertain-private-withdrawal retry limitation remains deferred in its existing discussion.

The PR advanced to 0f2b654 during testing. These results apply to 43c72a5; the new head is unreviewed here.


Reviewed by gpt-6.1-sol, reasoning effort high.

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Delta review of 0f2b654 ("fix: restore contact actions for the original session"), compared with 43c72a5.

No new findings. The open MEDIUM on PubkyProfileManager.swift (Paykit RC4 policy_error after a failed withdrawal) still stands until pubky/paykit-rs#267 is released and integrated, so this stays as changes requested.

What I checked:

  • piotr-iohk's MEDIUM at AddContactView.swift:258 is closed on all three screens: Add Contact Save, Edit Contact Save and Delete, and Contact Detail Edit, tag edits and Delete all go through ContactScreenSession.current(onSignedOut:). A signed-out Save shows pubky_auth__no_identity.
  • Adopting a session cannot select a different identity:
    • Adoption requires owner.publicKey == nil, an unchanged sessionRevision and a non-nil currentSession.
    • When a read-only display key is set, the restored key must match it.
    • walletForPresentation() and wipe.isGateUp reject a screen opened before an identity change or a wallet wipe.
    • An outstanding Delete confirmation fails on currentSession == session, isGateUp or validate(generation) after a switch, a wipe request or a removal reservation.
  • In deleteContact, no await sits between the ownership check and the call inside withScope. Edits bind expectedIdentity and an isSessionCurrent closure that now also requires owns(session).
  • The five new tests run against a real PubkyProfileManager and ProfileOperationAdmission, not a copy of the view logic.
  • A screen opened while signed out still rejects silently after a later interactive sign-in, because the revision bump means the owner is not adopted. I reproduced that in a unit test, but no navigation path keeps a signed-out contact screen under a sign-in flow: Back pops it, and the drawer and the sign-in completions replace the path. So I'm not raising it.

CI: Run Tests fails in SendSheetLifecycleTests.testExternalLnurlHandoffDoesNotTransferOwnershipToOpenSend. The PR touches no Send, LNURL, scanner or sheet source, and that test is byte-identical at 43c72a5, where it passed. The log points to a timing flake on a loaded runner, so it needs a re-run.

Reviewer model: claude-fable-5-1 (delta review), claude-opus-5-5 (verification, coordination)
Reasoning effort: Not exposed (no effort setting in the agents' run metadata)

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: ⛔️ Request changes — profile deletion still fails the Paykit capability update after contact deletion.

Reaudit: 43c72a5…0f2b654, seven files, plus earlier open concerns.

Findings: No new changed-line findings. Existing MEDIUM at PubkyProfileManager.swift:1650: the registry-downgrade concern remains open. Paykit is still pinned to RC4; pubky/paykit-rs#267 Retain explicit empty-list withdrawals when blocking peers is not integrated.

QA for 0f2b654:

Driven on iOS 26.5 simulators with disposable staging identities. Videos are 2×; recordings still contain stutters after repeat captures.

Contact actions use only the original restored session ✅ The five new regressions and reached admission/contact suites passed in XCTest.

The five new regressions and reached admission/contact suites passed in XCTest. The recording shows the test host; assertions were checked in XCTest output. The previously hosted-failing Send-sheet test also passed locally, which does not explain or clear the hosted failure.

contact-sessions.2x.mp4
Saving a public contact without a session gives feedback ✅ On the pristine head build, two Save taps each showed “Pubky Identity Required” and kept Add Contact open.

On the pristine head build, two Save taps each showed “Pubky Identity Required” and kept Add Contact open.

signed-out-save.2x.mp4
A contact opened during session restoration can be saved ✅ The same Add Contact screen saved after genuine native restoration.

On 0f2b654 + a timing-only hook, I held a genuine native startup-restoration result before application, opened Add Contact through scanner entry, and released restoration. Save then completed on that same screen. A second contact repeated the result. Edit and Contact Detail restoration branches were checked in source and component tests, not driven as separate installed-app cases.

restored-contact.2x.mp4
Timing-only test hook

Not pushed. Holds only a genuine native restoration result until explicit release, cancellation or its deadline; it substitutes no result.

diff --git a/Bitkit/Managers/PubkyProfileManager.swift b/Bitkit/Managers/PubkyProfileManager.swift
index 1da08681..bf8d9416 100644
--- a/Bitkit/Managers/PubkyProfileManager.swift
+++ b/Bitkit/Managers/PubkyProfileManager.swift
@@ -614,6 +614,20 @@ class PubkyProfileManager: ObservableObject {
             let initialized = try await removalAdmission.withDetached {
                 try await initializeSession()
             }
+            #if DEBUG // test hook
+                if ProcessInfo.processInfo.arguments.contains("-REVIEW_HOLD_CONTACT_RESTORE"), case .restored = initialized { // test hook
+                    let directory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] // test hook
+                    try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) // test hook
+                    let release = directory.appendingPathComponent("review-contact-restore-release") // test hook
+                    try "held".write(to: directory.appendingPathComponent("review-contact-restore-held"), atomically: true, encoding: .utf8) // test hook
+                    Logger.info("review test hook: genuine native restoration held before result application", context: "ContactRestoreHook") // test hook
+                    let limit = ContinuousClock.now.advanced(by: .seconds(180)) // test hook
+                    while !FileManager.default.fileExists(atPath: release.path), ContinuousClock.now < limit, !Task.isCancelled { // test hook
+                        try await Task.sleep(for: .milliseconds(100)) // test hook
+                    } // test hook
+                    Logger.info("review test hook: native restoration result released", context: "ContactRestoreHook") // test hook
+                } // test hook
+            #endif // test hook
             try removalAdmission.checkCurrent()
             result = initialized
         } catch {

The hook log confirmed that genuine restoration was held, then released; the recording shows the same Add Contact screen reaching Contact Saved.

A remaining peer acknowledgement permits profile deletion ❌ The exact acknowledgement passed; final deletion still failed policy.

In the same failed-withdrawal attempt, the native report omitted the blocked contact and reported both clear and acknowledgement for only the remaining peer and message 3. That gate passed, but final profile deletion still failed the public capability downgrade. The full combined completion therefore fails. Both deletion results come from the same attempt.

failed-withdrawal.2x.mp4
Pending cleanup survives a real network reconnect ⏭️ Recovery UUID and guidance survived the simulator service restart, but no app network-path transition occurred.

Recovery UUID and guidance survived the simulator service restart, but no app network-path transition occurred. Restarting the simulator configuration service emitted no disconnected/connected event; the simulator CLI exposes no isolated connectivity toggle.

Wrong native acknowledgements and open-ended cancellation retain safety ⏭️ Native peers ran and ordinary links/operations were attempted; an authenticated wrong-peer or wrong-message acknowledgement was not produced.

Native peers ran and ordinary links/operations were attempted; an authenticated wrong-peer or wrong-message acknowledgement was not produced. Available native fixture controls cannot emit a mismatched signed acknowledgement. The broader open-ended cancellation matrix remains undriven.

Delayed native callbacks reject replacement and wipe ⏭️ The full installed callback matrix remains unproved.

The genuine delayed startup-restoration case passed separately. The earlier bounded native tails retain their original scope. Native fixture attempts did not produce delayed authenticated receipt/indexing and all other callback tails needed for the complete replacement/wipe matrix; component tests do not establish that installed-app result.

Profile deletion finishes after a failed contact withdrawal ❌ Native withdrawal failed, then profile deletion failed policy.

On 0f2b654 + logging-only instrumentation, refusing the linked contact’s connection produced a genuine failed-withdrawal warning. After contact deletion completed, I restored connectivity. Profile deletion later failed syncPaykitApp with policy and retained identity and recovery. The native Shared/UncertainClear classification was not exported.

failed-withdrawal.2x.mp4
Ordinary profile deletion finishes after contact deletion ✅ The pristine head build completed contact deletion and profile deletion.

The pristine head build completed contact deletion and profile deletion. Cold launch returned to profile onboarding; persisted checks confirmed no profile identity or recovery marker. Remote profile absence and a complete keychain audit were not independently checked. Native contact removal took at least 11.8 seconds with a spinner; this service path is unchanged in the delta.

ordinary-cleanup.2x.mp4
Credential and acknowledgement failures preserve cold recovery ✅ Carried from 630be75; not rerun.

Carried from 630be75, not rerun: Owned and Ring credential, acknowledgement, reservation and final-marker fault cycles retained cold recovery; original native watermark and nonempty-override checks apply. Original report. The reached behavior is unchanged by this delta.

Fixed-term subscriptions retain remote state during local Disconnect ✅ Carried from 630be75; not rerun.

Carried from 630be75, not rerun: Genuine app-approved finite owned payer, owned payee and Ring payee cases retained remote state during local Disconnect; broader open-ended cancellation remains unrun. Original report. The reached behavior is unchanged by this delta.

Receipt attribution survives profile cleanup and cold resync ✅ Carried from 3e49570; not rerun.

Carried from 3e49570, not rerun: Genuine regtest receipt attribution and cold resync passed within the original report scope. Original report. The reached behavior is unchanged by this delta.

Corrupt reservations stay strict for profiles and clear during wallet reset ✅ Carried from 065a4c6; not rerun.

Carried from 065a4c6, not rerun: Malformed/version-2 reservation wipe and credential faults passed; consumed watermark was injected and power-loss durability was not proved. Original report. The reached behavior is unchanged by this delta.

Saved contacts wait for a private link and open privately after mutual saving ✅ Carried from 43c72a5; not rerun.

Carried from 43c72a5, not rerun: Both saved-contact routes waited for the private link, then resolved privately after mutual saving. Original report. The reached behavior is unchanged by this delta.

Fixed on-chain requests keep their amount through confirmation and send ✅ Carried from 43c72a5; not rerun.

Carried from 43c72a5, not rerun: Fixed 10000-sat on-chain payment retained its amount through send; 500-sat dust and amount-free controls retained their reported behavior. Original report. The reached behavior is unchanged by this delta.

Profile deletion finishes after re-adding a contact ❌ Profile deletion failed policy after a re-added contact was deleted.

On 0f2b654 + logging-only instrumentation, I re-created a same-key profile and re-added its retained contact. Native state was linking when I deleted the contact. Subsequent profile deletion failed syncPaykitApp with the policy category and retained local recovery. No explicit network refusal was active. This is additional evidence for the existing concern under a different precondition; it is not a failed linked-peer-withdrawal reproduction.

readded-peer-cleanup.2x.mp4
Logging-only test hook

Not pushed. Logs actual native peer states and clear/acknowledgment pairs; it changes no decision or returned result.

diff --git a/Bitkit/Services/PubkyService.swift b/Bitkit/Services/PubkyService.swift
index 159c9a54..5c16cec4 100644
--- a/Bitkit/Services/PubkyService.swift
+++ b/Bitkit/Services/PubkyService.swift
@@ -1250,6 +1250,11 @@ actor PaykitSdkService {
     func removeContact(publicKey: String) async throws -> Paykit.ContactRecord? {
         try await withStateRevisionTracking { sdk in
             let peers = try await sdk.linkedPeers().filter { PubkyPublicKeyFormat.matches($0.counterparty, publicKey) }
+            #if DEBUG // test hook
+                if ProcessInfo.processInfo.arguments.contains("-REVIEW_TRACE_CLEANUP") { // test hook
+                    Logger.info("review contact peer states: \(peers.map { String(describing: $0.state) })", context: "CleanupTraceHook") // test hook
+                } // test hook
+            #endif // test hook
             let now = Date()
             let activeSubscriptions = try await sdk.paymentRequests().filter {
                 PubkyPublicKeyFormat.matches($0.counterparty, publicKey) &&
@@ -1405,6 +1410,11 @@ actor PaykitSdkService {
         guard !counterparties.isEmpty else { return nil }
         return try await withStateRevisionTracking { sdk in
             let peers = try await sdk.linkedPeers()
+            #if DEBUG // test hook
+                if ProcessInfo.processInfo.arguments.contains("-REVIEW_TRACE_CLEANUP") { // test hook
+                    Logger.info("review cleanup peers: \(peers.map { "\($0.counterparty):\($0.state)" })", context: "CleanupTraceHook") // test hook
+                } // test hook
+            #endif // test hook
             let blockedPeers = peers.filter { $0.state == .blocked }
             let updates = counterparties.filter { counterparty in
                 !blockedPeers.contains { PubkyPublicKeyFormat.matches($0.counterparty, counterparty) }
@@ -1434,10 +1444,17 @@ actor PaykitSdkService {
                 throw PubkyServiceError.sessionNotActive
             }
             try checkMutationAdmission()
-            return try await sdk.syncPrivatePaymentListsWithReservationsAndProcessOutbound(
+            let report = try await sdk.syncPrivatePaymentListsWithReservationsAndProcessOutbound( // test hook
                 updates: updates,
                 clearUnlistedLinkedPeers: false
             )
+            #if DEBUG // test hook
+                if ProcessInfo.processInfo.arguments.contains("-REVIEW_TRACE_CLEANUP") { // test hook
+                    Logger.info("review cleanup cleared: \(report.cleared.map { "\($0.counterparty):\(String(describing: $0.outboundMessageId))" })", context: "CleanupTraceHook") // test hook
+                    Logger.info("review cleanup acknowledged: \(report.acknowledged.map { "\($0.counterparty):\($0.outboundMessageId)" })", context: "CleanupTraceHook") // test hook
+                } // test hook
+            #endif // test hook
+            return report // test hook
         }
     }
 
@@ -1813,7 +1830,13 @@ actor PaykitSdkService {
         try await withSdk(priority: priority, beforeOperation: { try await self.checkRefreshAdmission(priority) }) { sdk in
             try await readRetryingSharedStateBusy {
                 try await self.checkRefreshAdmission(priority)
-                return try await sdk.linkedPeers()
+                let peers = try await sdk.linkedPeers() // test hook
+                #if DEBUG // test hook
+                    if ProcessInfo.processInfo.arguments.contains("-REVIEW_TRACE_CLEANUP") { // test hook
+                        Logger.info("review observed peers: \(peers.map { "\($0.counterparty):\($0.state)" })", context: "CleanupTraceHook") // test hook
+                    } // test hook
+                #endif // test hook
+                return peers // test hook
             }
         }
     }

The deciding native log recorded linking at contact removal, followed by Paykit app capability update failed during profile removal (policy). Recovery was checked in the UI snapshot and persisted marker.

Profile cleanup stops without the remaining peer acknowledgement ✅ Missing acknowledgement retained recovery and stopped cleanup.

On a fresh three-wallet fixture, contact deletion first produced the same genuine failed-withdrawal warning. I then refused the remaining linked peer. The native report cleared message 5 but acknowledged none; privateUnavailable stopped cleanup and retained identity and recovery. No later public-capability update or final profile-delete call was logged in that attempt. Cold launch and opening Profile retained the same identity, marker and pending private state, checked on disk and in the appended cold-launch clip. The recording combines the deletion attempt with a separate cold launch and Profile entry. This proves the missing-acknowledgement refusal, not the broader authenticated wrong-peer/wrong-message matrix.

missing-acknowledgement.2x.mp4
Check: hosted CI ⚠️ Run Tests remains failed.

The hosted Run Tests job fails SendSheetLifecycleTests.testExternalLnurlHandoffDoesNotTransferOwnershipToOpenSend. That test passed in the focused local run. No hosted rerun or full-suite success is claimed.

The delta review and installed-app cases cover signed-out Add Contact and genuine startup restoration. The complete Edit/Detail action matrix and broader native callback matrices remain unverified on a device.

Pair PR: synonymdev/bitkit-android#1693 fix: bind profile removal to its original owner matches the owner/attempt and acknowledgment contract; the new iOS contact-screen restoration behavior has no runtime parity claim.



Reviewed by gpt-6.1-sol-high

@ben-kaufman ben-kaufman mentioned this pull request Nov 6, 2026
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants