Repository navigation
fix: separate usdt recovery backups from paykit - #1681
ben-kaufman wants to merge 14 commits into
Conversation
|
Triage: 3.1.0. It fixes shipped USDT recovery backups, and 3.1.0 is due 28 Oct. |
There was a problem hiding this comment.
Verdict: ⛔️ Request changes — a delayed Retry failure leaks into a new send form.
The PR moved to f496f43 after this run; these results are for 2493dbb.
The PR moved to f496f43 during this run. These findings and results cover 2493dbb; the new head has not been reviewed here.
Findings:
- LOW — Discard retry feedback after closing the payment screen (
app/src/main/java/to/bitkit/ui/screens/wallets/usdt/UsdtViewModel.kt:275). Closing while Retry awaits VSS resets the same view model. The old failure then puts the preceding payment’s backup explanation into a fresh recipient form. A focused deferred-callback regression and the device reproduce this; the pending-transfer guard still prevents another payment.
QA for 2493dbb:
Tested on Pixel 8 / Android 16 with published Core 0.8.0-rc11 and VSS 0.5.26-rc1. The funded app run used a disposable Arbitrum fork and a test-only override for app backup endpoints; payment and recovery logic were unchanged. The encrypted backup protocol fixture does not verify LNURL-auth signatures.
Test 1: Rejected recovery uploads ✅ Instrumentation assertions verified the production callback preserves the original signed operation through restart and migration restore.
The extended protocol harness calls the production UsdtService backup adapter and BackupRepo, rather than the direct-store callback in the committed native test. Prepare, resume and restore each verified their own upload fault and no submission before acknowledgement. One unique operation and the complete-wallet sentinel were retained. These assertions exercise native signing and encrypted VSS, not deployed contracts or Paykit authorization.
T1.2x.mp4
Test 2: Lost recovery acknowledgements ✅ Instrumentation assertions verified committed encrypted records restore and retry the same signed operation.
The same production callback and migration path passed separate prepare, resume and restore processes with dropped acknowledgements. Each process required its own successful acknowledgement before submission; signed operation bytes and the complete-wallet sentinel were preserved.
T2.2x.mp4
Test 3: Funded app failure and recovery ✅ The app showed the backup error, then confirmed the original payment after restart and acknowledgement.
Rejecting VSS after approval showed Error, the same-payment explanation, Retry, Details and Close; Details showed Pending. The bundler count stayed unchanged while uploads failed, including after restart. After acknowledgement, the app showed Confirmed. SQLite comparisons preserved the application ID, operation hash and exact signed-plan bytes. The deployed-contract receipt contains one successful user operation and exactly one 1-USDT recipient transfer. A native readback then decrypted and decoded the complete WALLET envelope successfully.
T3-initial-evidence.2x.mp4
T3-relaunch.2x.mp4
T3-confirmation.2x.mp4
Test 4: Close during Retry ❌ The old retry failure enters a fresh recipient draft.
With a rejected upload in flight, I tapped Retry, Close and Send. The fresh Send USDT form first had no error, then displayed the preceding payment’s backup explanation as UsdtRecipientError. The focused deferred-callback regression fails on the same update. No duplicate payment was observed.
T4-evidence.2x.mp4
Test J1: Complete recovery journey ⏭️ The full authenticated, concurrent recovery journey remains incomplete.
The funded subset above covers ordinary backup failure and restart recovery. The running fork and VSS fixtures do not provide authenticated Paykit/Pubky fault controls. Genuine Paykit authorization/proof faults, interrupted full restore, concurrent production-app writers and the deadline-transition journey were not driven together. Protocol/component results do not complete J1.
Check 1: Build and static checks ✅ The unmodified head passed compilation, unit tests and lint.
compileDevDebugKotlin, testDevDebugUnitTest, assembleDevDebug, assembleDevDebugAndroidTest and just lint passed. Structured Detekt and Android Lint reports contain no findings. The added stale-retry regression was run separately and fails as described above.
Twin: bitkit-ios#1118 fix: separate usdt recovery backups from paykit matches the independent record format, restore order and same-operation Retry behavior at f82258e. Android additionally carries the explicit deadline journey action. I relied on the recorded earlier native cross-platform and screen-component results in the PR descriptions; those are not new full-J1 results.
Full review of 2b0c8a3..2493dbb, all 43 changed files and affected callers. Full-wallet retry scheduling, Paykit proof revalidation and concurrent-writer behavior retain component coverage; no complete device journey verified them here.
Reviewed by gh-pr-review-loop.
Reviewer model: gpt-6.1-sol
Reasoning effort: high
There was a problem hiding this comment.
Verdict:
Findings: No new actionable findings. The previous Retry finding is fixed in 5640fad; Test 4 verifies it at the reviewed head.
QA for 7c21884:
Test 1: Ordinary USDT payment while Pubky is unavailable ✅ Recovery acknowledgement preceded submission; the original confirmed during the Pubky outage.
Pubky LOCK requests returned 503 errors. An independent encrypted VSS acknowledgement preceded the observed bundler request by 0.125 seconds, and the original operation confirmed with exactly one recipient transfer. Native assertions decoded the complete WALLET and validated the recovery records while the fault remained active.
| Payment | Native readback |
|---|---|
J1a-2x.2x.mp4 |
J1a-readback-2x.2x.mp4 |
Test 2: Rejected backup, repeated retry and cold restart ✅ Rejection, repeated Retry and cold restart preserved one original until acknowledgement allowed confirmation.
The app showed the backup explanation with Retry, Details and Close. Two retries and a cold restart retained the original signed operation without submission while rejected. After acknowledgement the same bytes and hash confirmed, and the UI changed Pending to Confirmed.
The first clip shows the original run. The second is a separate rejected-retry replay; settlement and restart identity were checked through SQLite, app logs and the deployed-contract receipt.
| Original rejected upload | Separate retry replay |
|---|---|
J1b-prepare-2x.2x.mp4 |
J1b-clean-retries-2x.2x.mp4 |
Test 3: Authenticated lost acknowledgement and automatic recovery ✅ Committed writes with dropped responses retained the original; automatic recovery confirmed it after acknowledgement.
Real encrypted VSS writes committed with their responses dropped. The app retained the same Pending operation without broadcasting. After acknowledgements were enabled, automatic recovery confirmed the same operation and signed bytes without pressing Retry. SQLite, VSS/RPC ledgers and deployed-contract receipt assertions establish the ordering and exactly one recipient transfer.
J1c-2x.2x.mp4
Test 4: Close during Retry isolates the new recipient form ✅ Closing a delayed Retry kept its eventual failure out of the fresh recipient form.
A delayed failed retry was closed and Send reopened. The fresh recipient form stayed clear through the returned failure; raw SDK accessibility assertions and callback logs confirmed no old error. The retained original signed bytes and hash were unchanged, with no extra operation or bundler call.
T4-clean-action-2x.2x.mp4
Test 5: Paykit authorization and lock recovery ⏭️ Retention, authorization and settlement passed; the full held-lease retry/reconciliation assertion remains unverified.
Partial: a genuine USDT-only Paykit request required UI authorization, settled while scoped Pubky failures persisted, and the sender showed Proof submitted and the recipient showed Payment verified after release. Additional real native Pubky leases produced SharedStateBusy; the original signed operation and pending work survived restart and settled once after release.
Unverified: A clean end-to-end observation of bounded retry slowdown and remote-outcome reconciliation under the held lease was not completed; concurrent authentication failures prevented that assertion.
| Request and approval | Recipient proof | Real-lease authorization | Settlement |
|---|---|---|---|
J1e-aligned-2x.2x.mp4 |
J1e-verified-b-2x.2x.mp4 |
J1e-final-authorize-a-2x.2x.mp4 |
J1e-settlement-a-2x.2x.mp4 |
Test 6: Interrupted full restoration and migration ✅ Interrupted restoration resumed safely, selected metadata survived, and the restored app confirmed the retained valid original.
- I interrupted restoration after bitkit-core state import and again after payment associations were saved. Both native keychain gates persisted, and sending stayed blocked until Try Again completed.
- The selected older-format backup’s EUR metadata survived the USDT-only restart, checked through native encrypted-backup and settings readback.
- Full Android and iOS restores recovered byte-identical signed originals after rejection and lost acknowledgements. The restored Android app then submitted the retained valid original without another approval; its deployed-contract receipt contains exactly one 1 USDT recipient transfer.
- Full restore also recovered the original retained through the real Pubky lease case. SQLite, native keychain and encrypted-VSS assertions supplement the recordings.
| First interruption | Association restart | Selected metadata restart | Recovered original confirms |
|---|---|---|---|
J1f-resume-boundaries-2x.2x.mp4 |
J1f-completion-2x.2x.mp4 |
RN-restart-metadata-2x.2x.mp4 |
J1f-B-C-valid-restore-2x.2x.mp4 |
Test 7: Concurrent app writers and cross-platform restore ⏭️ Cross-platform recovery passed; the complete concurrent-association and proof-revalidation assertion remains unverified.
Partial: SQLite comparisons verified identical original IDs, hashes and signed bytes on Android and iOS. Subsequent SQLite status checks and the deployed-contract receipt verified confirmation on both platforms. Their native logs show different acknowledged encrypted recovery records. The restored Android keychain retained paymentStarted=true and had no pending proof. The selected older metadata branch also passed.
Unverified: An association change during an in-flight upload and subsequent stale-proof revalidation on both platforms were not demonstrated. A new genuine request reached the restored app, but its private endpoint preparation did not complete.
| Initial iOS restore | Android recovery | iOS restore and request |
|---|---|---|
J1g-ios-restore-final-2x.2x.mp4 |
J1f-B-C-valid-restore-2x.2x.mp4 |
J1f-B-ios-completion-2x.2x.mp4 |
Test 8: Saved payment-request deadline transition ✅ The saved request retained its expiration explanation through Pending → Failed without a new operation.
Initial send returned SubmissionDeadlineExpired after a local debug/E2E callback hold following genuine encrypted VSS acknowledgement. The expiration title and explanation remained visible when recovery changed the same operation from Pending to Failed. SQLite byte comparison and the RPC ledger showed no new operation or submission.
| Initial deadline | Retained Failed state |
|---|---|
J1h-held-payment-b-2x.2x.mp4 |
J1h-held-failed-b-2x.2x.mp4 |
Check 1: Exact-head build and static checks ✅ Compilation, the complete unit suite, Detekt and Android lint passed.
The normal reviewed head built successfully, including the app and instrumentation APKs. The published Paykit RC2 native library matched the library packaged in the APK. Device runs used the supported local E2E configuration; restoration pauses, the deadline callback hold and the older-backup URL used scoped diagnostic builds. The iOS input fixture populated the seed form through the existing QR-input handler; restore, authentication, encryption and settlement paths remained native.
Check 2: Delta, carried evidence and twin ✅ The earlier native protocol checks remain applicable.
I reviewed the eight-file delta 2493dbb..7c21884 plus the outstanding journey acceptance. The localhost VSS/auth configuration was exercised by the device cases, the RC2 dependency was exercised natively, and the Retry isolation change was driven in Test 4.
The earlier 2493dbb review proves the production-adapter encrypted rejection and lost-acknowledgement protocol cases T1/T2; their recovery implementation and bitkit-core/VSS pins are unchanged in this delta.
The twin synonymdev/bitkit-ios#1118 fix: separate usdt recovery backups from paykit matches the recovery architecture. I built its 2b387b2 head and used its full restore path for the cross-platform comparisons. Its current-head review also covers the scoped outage, rejection, restart and lost-acknowledgement subset; it does not close the remaining matrix here.
Reviewed with gpt-6.1-sol, reasoning effort high.
Reviewed by gpt-6.1-sol-high via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest
There was a problem hiding this comment.
Verdict:
The PR moved to 8a176f0 after this run; these results are for 416c1e3.
Findings: None in the reviewed delta. The previous Retry/Close finding remains fixed.
QA for 416c1e3:
The PR advanced to 8a176f0 during this run. These results cover 416c1e3; the newer head is outside this review.
Test 1: M1 — Receive layout, privacy and QR decoding ⏭️ No device case completed before the head changed.
The merged receive/share and compact QR cases remain unrun at this head in this review.
Test 2: M2 — Paykit amount and request layout ⏭️ No device case completed before the head changed.
The merged amount-entry and long request-row cases remain unrun at this head in this review.
Test 3: M3 — Contact resolution state ⏭️ No device case completed before the head changed.
Selected-contact loading and disabled competing selections retain unit coverage; their current-head device assertions were not executed here.
Test 4: J1e — Held-lock retry and reconciliation ⏭️ The complete Android held-lease retry/reconciliation case remains unverified.
The previous Android review proves retention, genuine authorization and eventual settlement, but explicitly leaves retry slowdown and remote-outcome reconciliation incomplete. The newer twin review proves that additional behavior on iOS. It does not establish Android retry scheduling. No new Android fault-injection phase began before this run ended for the head change.
Test 5: J1g — Concurrent associations and proof revalidation ⏭️ Cross-platform recovery is proved; full device proof revalidation remains unverified here.
The newer twin review built Android 416c1e3 and exercised overlapping Android/iOS uploads while a new request association was added. I independently compared its native exports: both platforms retain the same seven operation IDs, hashes and signed plans. Android retains all three started-payment guards and clears imported proof flags. Stale-proof rejection in that report is component coverage, rather than a new device rejection case. No additional device phase ran here.
Check 1: Exact-head build and unit suite ✅ Compilation, the full unit suite and both APK builds passed.
compileDevDebugKotlin, testDevDebugUnitTest, assembleDevDebug and assembleDevDebugAndroidTest completed successfully with the committed dependency pins. Complete JUnit reports contain no failures, errors or skips. Lint was not started before the head changed, and no device test completed in this run.
Check 2: Delta and carried evidence ✅ The contiguous delta contains no confirmed actionable defects.
I reviewed all 42 changed files in 7c21884..416c1e3, including reached activity synchronization, transaction lookup, contact opening, receive presentation and recovery callers. An independent reviewer also inspected the complete delta. git diff --check passed.
The previous Android review retains passing evidence for J1a/J1b/J1c, Retry/Close isolation, interrupted restoration/migration and the saved deadline transition: their recovery, signing, restore and deadline implementations are unchanged. The merged receive, activity and contact presentation changes were source/unit reviewed; their new device coverage is listed above.
Twin: synonymdev/bitkit-ios#1118 fix: separate usdt recovery backups from paykit at 4d1c0ba matches the independent record format, request bindings, started guards and imported-proof reset behavior. Its Android recovery evidence is cited above; platform-specific scheduling remains separately scoped.
Reviewer model: gpt-6.1-sol
Reasoning effort: high
Reviewed by gpt-6.1-sol-high via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest
[Critical impact] No actionable code issues were found in the reviewed changes. SummarySeparates required USDT recovery uploads from complete wallet backups and Paykit export.
No actionable issues were found. Tests were not run during this review. The PR description explicitly leaves the complete device journey unfinished; this review does not treat it as passed. DiagramsequenceDiagram
participant Core as USDT wallet
participant Backup as BackupRepo
participant VSS as Encrypted VSS
participant Network as Bundler
Core->>Backup: Persist signed recovery snapshot
Backup->>VSS: Write content-addressed record
alt Write acknowledged
VSS-->>Backup: Success
Backup-->>Core: Backup acknowledged
Core->>Network: Submit original signed operation
else Write fails or acknowledgement is lost
Backup-->>Core: Backup unavailable
Note over Core: Retain original operation for retry
end
Reviews (1) · Last reviewed commit: "Merge master into independent USDT recov..." · Reviewed by Greptile |
Regtest APKDownload bitkit-dev-debug universal APK (expires in 30 days). |
talosmachina
left a comment
There was a problem hiding this comment.
No findings. Moves USDT recovery data out of the full WALLET backup into immutable, content-addressed USDT_RECOVERY_V1_<sha256> VSS records, acknowledged before broadcast, with a keychain-gated restore that replays the legacy payload and then every record. Reviewed 8a176f0, oversized tier (47 files, +1268/-89).
What I checked, and 5 candidates I ruled out
Read in full: the production diff (UsdtRecoveryBackupStore.kt, BackupRepo.kt, UsdtService.kt, UsdtRepo.kt, PaykitUsdtPaymentRepo.kt, UsdtViewModel.kt, UsdtSendContent.kt, WalletViewModel.kt, Env.kt, BackupTestEndpoints.kt, Keychain.kt, build.gradle.kts, the version pins). Tests, journeys, docs and translations were not reviewed: the oversized tier.
Call sites traced: hasPendingWalletRestore, performFullRestoreFromLatestBackup, restoreUsdtRecoveryAfterLegacyMigration, every BackupCategory.entries consumer, bindingState().
Not built here: Gradle would run this branch's build scripts with a packages token in its environment, so this is a static review. CI is the gate: build, lint and detekt green, the e2e-tests-local shards pending when I posted.
Ruled out
- Replaying every record in key (hash) order could let an older snapshot overwrite a newer one: bitkit-core
0.8.0-rc11restore_backupmerges, keeps the local outcome when a transfer id already exists and only merges the plan (src/modules/usdt/backup.rs), so order does not matter. - Restore throws on an attempt whose binding differs between records: two records for one
(wallet, quoteId)come from the sameprepare, which refuses a changed binding (previousBinding != attemptthrowsOperationInProgress). USDT_RECOVERYreaching the genericputObject(category.name, ...)path or the remote-timestamp scan: both are routed away (uploadUsdtRecoveryBackup, and thegetLatestBackupTimefilter).- Wallet backups stall while the USDT restore gate is set: intended.
shouldSkipBackupholdsWALLETuntil the recovery restore clears the keychain flag, andhasPendingWalletRestoresurfaces it as a retryable restore. - Test endpoints reaching a release build: the BuildConfig fields are empty outside debug, and
BackupTestEndpointsaccepts onlylocalhostwith an explicit port.
Merge confidence: 3/5, the oversized-tier ceiling (tests and journeys not read), and the backup and restore paths are proven only by CI and the author's fixtures, not by a run here.
Reviewer model: claude-opus-5-5
Reasoning effort: Unknown (not exposed in this run's metadata)
There was a problem hiding this comment.
Verdict:
Findings: None.
Reaudit: diff. The merged notification changes were not driven on device.
Pair PR: synonymdev/bitkit-ios#1118 matches the recovery architecture at e5aa5776.
QA for 6fc2267:
Tested on Android 16 emulator. The zero and expired-rate checks used seeded test data and an unavailable test rate endpoint.
Test 1: Receive layout privacy and QR decoding ✅ The receive privacy and QR decoding instrumentation checks passed.
m1.2x.mp4
Test 2: Paykit amount and request layout ✅ The amount and request layout instrumentation checks passed.
m2b.2x.mp4
Test 3: Contact resolution state ✅ The contact selection instrumentation check passed.
m3.2x.mp4
Test 4: Held lease retry and reconciliation ⏭️ Not fully run: The held-lease proof retry, restart reconciliation and duplicate prevention were not exercised.
A held lease was acquired, but authentication failed before proof delivery; after reauthentication the payment completed.
j1e-3-2x.2x.mp4
j1e-4-2x.2x.mp4
Test 5: Concurrent recovery and proof revalidation ⏭️ Not fully run: Concurrent association writes and proof revalidation were not exercised.
A fresh restore recovered the transfer and its started payment guard, checked in the restored wallet data.
j1g-2x.2x.mp4
Test 6: Cached USDT balance offline and privacy ✅ After an offline restart the cached balance and updating indicator remained visible, and the privacy control hid and revealed the balance.
c1a-2x.2x.mp4
Test 7: Cached balance refresh ✅ After reconnecting and adding test funds the displayed balance rose from $23.17 to $25.17 and updating cleared.
c1a-2x.2x.mp4
c1b-2x.2x.mp4
Test 8: Cached balance wallet isolation ✅ A seeded cache entry for another wallet was ignored on offline startup; the balance remained unknown.
c1c-2x.2x.mp4
Test 9: Zero balance and expired payment rate ✅ A live zero replaced a seeded stale balance; an expired rate blocked selection of USDT for the payment.
zero-2x.2x.mp4
rate-2x.2x.mp4
Test 10: Restored paid request guard ⏭️ Not fully run: No stale pending request row was available to exercise the paid-request race.
The restored payment guard was present in wallet data.
Test 11: Profile creation and restart ✅ A newly created profile retained its full name after restart.
p2b.2x.mp4
Test 12: Allowance and subscription notifications ⏭️ Not fully run: The isolated wallets did not have the Lightning channels and receiving capacity needed for the scenario.
Notification behavior was not exercised.
Earlier reports:
- Android recovery test report covers the ordinary outage, backup retry, close, restore and deadline paths at 7c21884.
- Prior delta report records the cases left unrun.
- Pair PR recovery report covers cross-platform recovery.
Reviewed by gpt-6.1-terra-high via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest
Twin: synonymdev/bitkit-ios#1118
This PR separates acknowledged USDT recovery backups from the full wallet backup, so an ordinary transfer does not export Paykit state or acquire a Pubky lock before broadcast.
Description
0.1.1-rc2with the validated lease, suspension and acquired-lock cleanup fixes. Pubky remains0.16.2; unmerged fix: recover grant requests after confirmed 401 pubky/pubky-homeserver#741 is excluded.0.5.26-rc1. Existing backup APIs remain in use; the conditional API does not activate a split full-wallet format or exclude legacy writers.0.8.0-rc11, including the restore-order fix from fix: preserve USDT application identity across backup merges bitkit-core#179 and its regenerated bindings.Out of Scope
Native Pubky authentication recovery is tracked in pubky/pubky-homeserver#741. This PR does not release SDK artifacts, replay uncertain contact mutations or establish the historical contact-error cause.
Design
N/A — no new design supplied; Retry uses the existing send-screen controls.
Preview
The controlled Android emulator evidence includes backup failure/retry screens, genuine Paykit request authorization and proof delivery, interrupted and fresh restores, concurrent snapshot uploads and the retained deadline failure. No production funds were used.
QA Notes
Setup
Use the committed released pins: Core
0.8.0-rc11, VSS0.5.26-rc1, Paykit0.1.1-rc2and Pubky0.16.2. Follow the recovery contract and J1 prerequisites. Standard staging does not supply the controlled provider, authenticated encrypted VSS fault selector, owned native Pubky/Homegate services or write-lease control required by this journey. Report the missing fixture rather than substituting production funds or resetting another wallet.The October 27 acceptance used dedicated API 36 arm64 emulators, an owned synthetic wallet/recipient, non-fork chain 42161 with controlled EntryPoint/paymaster/token runtimes, real native signing and LNURL-authenticated encrypted VSS/PostgreSQL, and real SDK peer/request/proof records. For an equivalent provisioned local fixture, build:
Forward only the dedicated emulator's ports: 18705→host18727 (receipt read adapter), 18706→18726 (bundler), 18715→18715 (native VSS auth), 18717→18717 (selected VSS fault proxy), 6288→18789 (Homegate), 15411→15411 (PKARR), 15412→18712 (Pubky auth relay), 18786/18787→the same host ports, and 18682→18682 (Electrum). The final historical-wallet restore alone used 18717→host18728, a read projection that substitutes the saved WALLET ciphertext only after successful upstream authentication; independent records remain current. The receipt adapter preserves the transaction receipt and logs, adding only the original receipt-log index. These adapters and fault controls are prerequisites, not standard RPC services. Use a new empty owned restore instance; preserve existing fixture wallets, acknowledged records and operations. Do not reset a shared chain or re-sign an old operation. The historical fork recipe below records an earlier, now unavailable fork and is not the current fixture.
Journeys
usdt-independent-recovery.xml— Pubky outage with working VSS, actual backup failure before broadcast, lost acknowledgements, repeated/automatic retry, closing/restarting, interrupted restore/migration and concurrent writers; shares the recovery expectations across platforms; Android also has an explicit deadline-transition action.Manual Tests
N/A — the controlled device cases are specified in J1; backend fault injection is an explicit prerequisite.
Automated Checks
added
UsdtNativeRecoveryIntegrationTest.kt— real encrypted backup rejection/lost acknowledgements, separate-process restart, fresh-database restore and the original signed operation before and after retry.added
UsdtRecoveryBackupStoreTest.kt— independent uploads, immutable concurrent writers, lost acknowledgements, byte-order/hash compatibility and fail-closed restore.updated
BackupRepoTest.kt,PaykitUsdtPaymentRepoRecoveryTest.kt,UsdtRepoTest.kt,WalletViewModelTest.ktandUsdtViewModelTest.kt— restart/migration gates, retained payment progress, proof revalidation and same-transfer Retry/automatic recovery.updated
PaykitPaymentRequestRepoTest.kt— a real proof-only USDT restore preserves the started guard, clears stale proof evidence and keeps an already-listed request nonpayable before reconciliation.Evidence
Current head
6fc2267a8ca54a42ccf91f59ee66f23afb909946additively merges masterb392d6ad99211e7278e856a1348017f548305974; reviewed treec990228ff78dfd49752bb5f10040e49ed1091f0c. Finaljust format,just compile,just testandjust lintpassed with 4,647 tests in 256 suites, zero failures/errors/skips and zero Detekt/Android Lint findings. Complete logs, XML/SARIF/HTML reports and nested diagnostics were audited. Three JVM sharing warnings and four cursor warnings remain; the allocating provider/test sources are byte-identical to master. A copied Gradle deprecation report was identified as prior-run evidence. Standalone compile was up-to-date; the later E2E APK build executed Kotlin/Java compilation. Two independent final source reviews are clean. All 14 commits against the live base are locally signature-verified and GitHub reportsverified=true,reason=valid.J1 device evidence at
8a176f032a59c8888aab8dae87090103f033e8b0(APK SHA-256bb38326a4b0bea5f4c68386f20a782e293b2ff587d2e1ff0d94ff3e6c7e5d213) uses the released dependencies above:On the final composed tree, installed APK SHA-256
a70f1d85768f324c35b4f2e4e746ae38fe62eb106a5fe176f6690c720e30f9e2upgraded that preserved Android wallet. F displayed “Proof submitted” without Pay; G had no Pay and its original transfer remained Failed after a cold restart. Both request history entries remained non-actionable, all seven original native records and the complete association plaintext were unchanged, and the entire provider request file stayed identical. No new quote, approval, payment or funding was performed. The app was stopped with data preserved. An initial launcher command failed; launching its resolved main activity and the subsequent normal restart succeeded.Hosted CI and lint completed successfully on this head: 4,647 unit tests passed with zero failures/errors/skips, and all Detekt/Android Lint report formats contain zero findings. Actual hosted app checkout
561a98051500901c1f73bdfeb42f7f669300e302has parents base + PR head and the same reviewed treec990228ff78dfd49752bb5f10040e49ed1091f0c.The seven-shard E2E workflow completed successfully with 39 unique tests eventually passing and one explicit address-type skip. Four shards passed their first step; three used the workflow's built-in second step, with no third step. Four first-step failures remain recorded:
@lightning_1received an expired-invoice rejection from LND;@send_2timed out onQuickpayIntro-button;@lnurl_1timed out on activity text150;@transfer_2timed out on activity textTransfer. All four passed on the automatic second step with unchanged app/suite. Three prior-pass lock-marker skips on the second steps are distinguished from the one explicit source skip. This is not a first-pass-clean result, and the precise failure causes are not proven or classified as PR regressions.All seven jobs used E2E suite
4c0cbef019220efed01ed3080c1f0ccc1fb627a3and APK artifact12769200977(APK SHA-2567a60f6f585f9c1c7e44daa4d5fc61136fbb3fdfaf794661c95cf88c7de6fcd00). The sole hosted audit retained four artifact ZIPs and all 14 complete executed-job logs, inspecting each executed test step and reconciling unique coverage. Final check inventory: 15 successful, six skipped migration checks, none pending. Each workflow has one GitHub run attempt; no workflow rerun was requested.Hosted warnings remain disclosed: JVM sharing, cursor allocation and a manifest removal marker (the allocating/provider/test/manifest sources are base-identical), native stripping notices and Gradle deprecation summaries. The external E2E suite install's npm vulnerability/deprecation summaries are separate from app lint findings. Successful/recovered E2E jobs did not upload local screenshot/video/logcat artifacts under the existing final-failure-only condition; detailed Gradle problems and npm advisory reports were also not uploaded. Complete available logs are retained, and no additional execution was started to obtain missing artifacts.
Gaps
The full controlled campaign above retains its exact
8a176frevision. The final master composition reuses unchanged recovery/SDK/native inputs, adds the focused restore/request-cache regression and re-exercises genuine retained request history without spending. F/G were already terminal and past their deadlines, so that final UI check does not reproduce a fresh-payment cache race; the repository regression covers the stale pending-list interaction.The historical selected WALLET is older chronological data in schema version 1, not an older-schema or React Native migration. Metadata evidence proves the selected payload's provenance through successful application, not a field-by-field competing-metadata sentinel comparison. The two writer snapshots contain the same transfer set with different association progress; no disjoint-transfer writer campaign is claimed. Native cryptographic proof verification ran in a same-UID released-Core sidecar; application proof creation/queueing were observed separately. The conditional Confirmed/Replaced deadline override was not selected. These qualifications are retained rather than turning partial observations into broader passes.
This PR remains ready following the human ready-for-review action; required human review is still pending. Hosted checks are complete with the retry and artifact qualifications above. Generic hosted E2E coverage is distinct from controlled J1 acceptance; the migration workflow was skipped and provides no migration coverage. Earlier draft-skipped jobs are not passes. No app release, deployment, production-fund test or public-network settlement is claimed. Legacy complete-wallet compatibility and downgrade limits below still apply; the separate Pubky authentication-recovery artifact remains outside this PR.
Models used
gpt-6-astra(reasoning:xhigh)gpt-6-astra(reasoning:xhigh)gpt-6-astra(reasoning:xhigh),gpt-6.1-sol(reasoning:high)Historical validation and setup, retained at their tested revisions
These entries preserve prior results, failures and limitations. Their draft status and then-pending cases describe those earlier checkpoints; current evidence and gaps are above.
Current composed head — 2026-10-27
8a176f032a59c8888aab8dae87090103f033e8b0additively incorporates live master7ce91e86ef7123ffdc420d193d393b5b1d12dd97. The final combined tree passed 4,636 unit tests in 256 suites with zero failures/errors/skips, compilation, unchanged formatting, Detekt and Android Lint with zero findings. Complete logs and structured diagnostics were inspected; three JVM notices and four cursor-close warnings from unchanged provider sources are retained without a timing-cause claim. Two consecutive independent final reviews are clean. All 13 commits in the complete current-master range are locally and GitHub verified.The master cache remains display-only; native balance/payment, required restore, immutable acknowledgement and same-operation retry gates remain intact. This composition does not complete the remaining authenticated J1 cases, and this PR remains draft only for its own unfinished acceptance. Historical results below retain their original revisions; skipped draft jobs are not passes.
Setup
Use the committed app dependency pins for automated tests. The complete device journey requires an owned disposable test wallet and recipient, controlled USDT RPC/bundler, encrypted VSS endpoint and Pubky homeserver with fault injection. Standard staging does not provide this combined fixture. Do not use production funds or reset an existing wallet. Record operation IDs, signed bytes, acknowledgement and broadcast counts; see the committed USDT recovery backup document and journey prerequisites.
For the native protocol subset, follow the pinned loopback fixture recipe in synonymdev/bitkit-core#180. It uses a public test mnemonic without funds, real native signing and encrypted VSS, and separate prepare/resume/restore processes. The earlier combined run below used local Core
c393f8e0, Paykite79b7bbband Pubky65a331e0bindings; these overrides are not the committed release pins.The previously used fork block
517943110is no longer served by the upstream RPC. Preserve that fixture and its wallets; future runs require a separate healthy fork with account/code/nonce preflight. The commands below describe the recorded historical run, not a currently usable fixture.For the deployed-contract UI subset, use the Core fork provider at
c393f8e0with Anvil v1.8.6, chain42161, Prague hardfork and fork block517943110. Build withUSDT_RPC_URL=http://127.0.0.1:18545 USDT_BUNDLER_URL=http://127.0.0.1:18546; install on a new dedicated emulator and reverse only its ports18545/18546. Create a disposable wallet, use the provider'stest_fundWalleton its receive address, then send one synthetic USDT to0x1111111111111111111111111111111111111111. Cancel once, review again, confirm and inspect the receipt. Remove only that emulator's18545 reverse mapping before restarting to check saved activity offline. All funds and mutations remain in the local fork; normal VSS is used and no backup/Pubky faults are injected in this subset.Current combined-tree validation —
416c1e355Signed additive merge
416c1e355a964357d3020180b5a1c8ee3ae5ea23incorporates live masterea89ed18, including merged local-transaction/history and receive UI changes, while preserving the released Core/VSS/Paykit pins and independent recovery behavior. Final combined-tree checks passed 4,629 tests across 256 suites with zero failures/errors/skips, full compile, formatting, Detekt and Android Lint with zero findings. Complete logs, all test XML and SARIF/XML reports were inspected; existing JVM, Gradle and controlled provider diagnostics remain disclosed. All 1,763 final inputs match, except the independently checked README-only journey parity clarification after native validation.Two consecutive independent final reviews (rounds 3/4) are clean. All 12 commits against current live master verify locally and on GitHub (
true/valid). Hosted checks are newly triggered; draft-skipped native jobs are not passes. J1 remains unfinished, and earlier native/external authenticated results below retain their original revision scope. The author is preparing isolated full-app fixtures for the remaining failure/retry/restart/restoration cases. Full-wallet redesign and native contact CAS are not blanket prerequisites for this PR.Earlier released VSS validation
At signed
c1748c876, normal Gradle resolution adopts published VSS0.5.26-rc1. AAR SHA25633bc7f5ef9202fb183c95ebaf85c83b1afac06235d17d4d48ad20c53b810724ematches the cache and installed app native bytes; all four ABI build IDs match the published symbol archive. Final 4,604 fresh units across 254 suites passed with zero failures/errors/skips; compile, formatting, full Detekt and Android Lint passed with zero findings. Complete logs and structured reports were inspected; the JVM and Gradle notices match recorded base evidence. Both installed APK hashes were verified without clearing wallet data.Both platforms passed fresh rejected-backup and lost-acknowledgement prepare/resume/restore phases, plus encrypted restore in both directions: 14 native phases total. Every retry preserved the original signed operation, each process acknowledged its own recovery backup before a broadcast attempt, there was one unique operation, and the complete WALLET sentinel remained unchanged. iOS calls the production BackupService handler; Android calls the recovery store directly. These tests use native signing/encryption with a loopback protocol/RPC fixture, not deployed contracts or the full UI/Paykit J1 journey. Two consecutive final independent reviews were clean. All seven commits against the live base are locally signed and GitHub reports
verified=true,reason=valid. Hosted checks for this head are pending; draft-skipped jobs will not be counted as passes.Earlier revision evidence
At
cc14496ed, requiredjust compile,just testandjust lintpassed on macOS ARM64 with the committed library coordinates. Complete JUnit reports contain 4,602 passing tests, zero failures or skips. Detekt SARIF and Android Lint XML contain zero findings. The final test/lint run had unchanged source content.Validation caught and repaired a restore-gate early-return type error and a stale test expectation that trusted imported proof. The repaired tests preserve payment-started guards while requiring proof revalidation. Journey XML parses. Independent review and bounded repair rechecks found no remaining actionable issues.
At test commit
2d8f8747, requiredjust compile,just testandjust lintpassed again with committed dependency coordinates: 4,602 tests, zero failures/skips, Detekt SARIF zero findings and Android Lint XML zero findings. With the combined local SDK build, native rejection and lost-acknowledgement sequences passed prepare, resume and restore; bidirectional iOS/Android encrypted snapshot restore also passed. The instrumented native case calls the recovery store directly; BackupRepo orchestration is covered separately by unit tests.Each native failure phase verifies zero broadcast attempts before its own recovery acknowledgement, unchanged signed operation bytes on retry and one unique payment. Full-wallet sentinel bytes remain unchanged. This verifies the native protocol subset, not the complete J1 journey.
At adoption commit
258bf9e5b, normal Gradle resolution uses published Core RC11. The downloaded AAR SHA256 is9d5d54f5761b7773d2cfaaf0be77eeb914348d52e31ebf81684d7e761ce68b5b, matching Gradle's resolved artifact and the installed app's native library. All four ABI build IDs match the published debug-symbol archive. Required compile/lint checks passed with zero Detekt SARIF or Android Lint XML findings. The initial unit invocation reused cached output, so an explicit fresh execution was run: 4,602 passed, zero failures/errors/skips across 253 suites. Existing Gradle plugin deprecation and JVM CDS diagnostics are recorded separately.The normal published-dependency app and instrumentation APKs were installed on the owned Android emulator and their installed bytes verified. Native rejected-backup and lost-acknowledgement cases each passed prepare, resume and restore. Fresh iOS-to-Android and Android-to-iOS encrypted restores passed with iOS
646670df; all 16 released-dependency native phases preserved the original operation and acknowledgement-before-submission ordering. All four PR commits are signed and GitHub reportsverified=true,reason=valid. Current-head hosted Detekt and Android Lint also passed; their uploaded SARIF/XML reports contain zero findings. Draft-only build/E2E jobs were skipped and are not counted as executed tests.At acknowledgement fix
dc5a47d90, a successful refresh with no backup callback no longer clears recovery feedback. The UI reads live acknowledgements from the native backup adapter, including when a prior payment was already acknowledged. Failed persistence and no-op automatic/manual retry retain the error and Retry action. Regressions fail on unchanged258bf9e5band pass with the repair. Final requiredjust compile,just testandjust lintpassed: 4,604 tests across 254 suites, zero failures/errors/skips; complete Detekt SARIF and Android Lint XML/SARIF reports contain zero findings. Source inputs were unchanged through validation and independent re-review is clean. All five commits against the live base are signed and GitHub reportsverified=true,reason=valid. This follow-up has component/adapter coverage; the earlier native protocol evidence remains tied to the commits above.Hosted lint for
dc5a47d90passed in run40123893660. The downloaded artifact checksum matches GitHub metadata; complete Detekt SARIF and Android Lint XML/SARIF contain zero findings. Build and E2E jobs remain skipped by the draft guards and are not counted as passes.A fresh normal-dependency app and instrumentation build at
dc5a47d90also passed. Both installed APK hashes were verified on the owned Android emulator, without clearing wallet data. All tenUsdtPaymentContentTestdevice cases passed, including submitted/confirmed transitions, closing the payment screen and comma-region input. This is screen-component coverage; it does not execute a full payment against the backend.At
dc5a47d90, a separate full-app run on a newly created Pixel 8 / API 36 ARM emulator used published dependencies with only USDT RPC/bundler URLs pointed to the owned loopback Anvil fork. The exact installed APK SHA256 was02606962f5493af2e32c05219df6888e42f0c10e7ea9d2789aebafb490902cc7. After synthetic funding, canceling the first fee warning submitted no operation. Repeating review and explicitly confirming sent one USDT; the UI showed Confirmed. The local deployed-contract receipt contains one successful user operation and exactly one recipient token transfer, with zero native ETH in the sender. Restarting with the chain RPC unavailable preserved the saved activity. The full build log had no compiler diagnostics; the sole structured Gradle deprecation exactly matched the prior normal-dependency build. This exercises the real app and BackupRepo success path, but does not inject VSS/Pubky faults or complete J1. Android returns to Amount after canceling the fee warning; iOS retains Review. Both preserve the amount and submit nothing before approval.At signed
071fe7d7, the typed deadline-expiry explanation and title survive the same saved transfer’s Pending → Failed transition. Confirmation, replacement and later bridge states still supersede stale feedback. The pre-fix regression failed and all 28 focused cases pass after repair. Final compile, formatting, full Detekt/Android Lint and 4,604 unit tests across 254 suites passed with zero failures/errors/skips and zero structured lint findings. An actual installed Compose case passed the pending/failed/replaced/confirmed title and body transitions; both APK hashes were verified without clearing app data. Two consecutive independent final reviews are clean. All six current-base commits are locally verified and GitHub true/valid. iOS parity is in synonymdev/bitkit-ios#1115. This is component coverage, not a new complete J1 pass.Gaps
J1 has not run as a complete UI/Paykit/deployed-contract journey. The protocol fixture does not execute deployed contracts or exercise a real Pubky outage. Full wallet migration and concurrent state changes remain covered by component tests rather than this native fixture; Android's native store test does not exercise all BackupRepo orchestration.
This remains draft pending its own remaining J1 acceptance: final-composition production-app backup failures, restart/restore/migration, concurrent recovery records, and preserved Paykit authorization/proof behavior. The ordinary required-backup path uses released Core RC11, released VSS RC1 and local payment associations; it does not call Paykit export or a Pubky lease. An unreleased Paykit/Pubky upgrade and the separate future full-wallet split are not blanket prerequisites. A dependency will be recorded only if a required case demonstrates it. Core #179 and RC11 preparation #181 are merged; published Core RC11 is adopted and validated in both native recovery PRs. Local combination builds and native tests passed with Paykit lease-expiry safety from pubky/paykit-rs#211 and authentication recovery from pubky/pubky-homeserver#741; the Paykit portion is now released and adopted as described below, while the separate Pubky #741 artifact remains unavailable. No app release or deployment is part of this PR.
The later device-sleep lease repair at Paykit
461864a4passed 1,557 Rust tests, both native binding builds and full generated Android checks, and is included in the later published RC2 adoption below. The combined app/native evidence above used Paykite79b7bbb. Core RC11 does not contain the Pubky authentication or Paykit lease fixes.The legacy complete wallet format remains supported. Older apps cannot read journal-only operations; downgrade recovery is limited to the last complete wallet backup. Records are not pruned, and immutable writes do not make multi-page listing atomic or coordinate simultaneous spending by multiple devices.
Authenticated local backup fixture configuration (f496f43)
The recovery branch now accepts a paired local VSS/auth configuration only in an explicitly enabled development E2E build. Set
E2E=true,E2E_VSS_SERVER_URL=http://localhost:18657/vssandE2E_LNURL_AUTH_SERVER_URL=http://localhost:18655/authbefore building/launching; Android additionally requires device-local port forwarding for both ports. Both values are required together, must uselocalhostwith explicit ports and paths, and reject credentials, query strings and fragments. Normal production builds retain their original endpoints. The existing native LNURL authentication and encrypted storage remain in use.Final validation at
f496f436: 4,617 fresh unit tests across 255 suites passed with zero failures/skips; compile, full formatting, Detekt and Android Lint passed with no introduced findings. Complete logs and structured diagnostics were inspected, and two consecutive independent final reviews were clean. This configuration enables the controlled fixture; it does not establish J1 or a full authenticated app failure journey. The existing unchecked journey cases and draft status remain applicable.Latest retry-screen repair and authenticated fixture evidence
At
5640fad6e, a recovery Retry that finishes after Close/Send no longer writes its old backup failure into the fresh recipient form. The result and loading cleanup remain bound to the original submitted quote. The repository retry continues for the retained signed operation; no new payment or cancellation is introduced. A deterministic deferred-result regression failed before the fix. Finaljust format,just compile,just testandjust lintpassed: 4,618 tests across 255 suites, zero failures/errors/skips, and zero findings in the complete fresh Detekt and Android Lint SARIF/XML reports. Two consecutive independent final reviews were clean. The matching journey action is included in both native apps. This regression is component evidence; its new device step remains unchecked.At earlier
f496f436(installed APK SHA-25623979eef90fe4eafc3cb7df6497a0cdf6203201233d7b6db7a2ba69cace14ac0), the disposable Android API 36 fixture used the committed released Core RC11, VSS 0.5.26-rc1 and Paykit 0.1.1-rc1, real native LNURL authorization/encryption, an owned RS256 VSS/PostgreSQL service and a separate Anvil fork at Arbitrum block 518486328. Its Pubky proxy tunnels were blocked while the UI sent one synthetic USDT. Recovery acknowledgement preceded the sole bundler submission; the deployed-contract receipt confirmed the payment. The complete WALLET ciphertext sentinel remained unchanged.A second signed payment was rejected at its exact authenticated recovery-record selector. Two manual retries, Close and a cold app restart retained identical operation bytes, with no submission before acknowledgement. Once acknowledgement was permitted, that same operation was submitted once. Settlement failed: its synthetic paymaster authorization had expired and the receipt reverted with
AA32 paymaster expired or not due. This is not a successful restart-settlement pass. The original pending operation, wallet and fork are preserved; no replacement quote or re-signing was performed. All fault controls were restored healthy after capture.These observed portions do not complete J1: authenticated lost-acknowledgement, fresh remote restore/migration, concurrent writers, preserved Paykit authorization/proof behavior and the new close/reopen action still need their own final-head acceptance. The first successful outage transfer is not evidence that the second transfer settled. Local gates above validate the repaired branch; the later live master layout merge was inspected but is not claimed as a tested synthetic merge.
Published Paykit RC2 adoption — 7c21884
Normal Gradle resolution now uses published
com.synonym:paykit-android:0.1.1-rc2. Its AAR SHA-256eba55ef4b1895f1c9e615440579be3a94f14bfed26c73509be45bd5d57c9ae2ematches the downloaded publication and resolved cache. Installed app/test APKs match the built bytes; installed arm64 Paykit matches the published AAR. Core RC11 and VSS RC1 remain unchanged. This one-line pin change does not add the new payment-request preparation API or broader credential-error classification.Final 4,618 fresh unit tests across 255 suites passed with zero failures/errors/skips; compile, full formatting, Detekt and Android Lint passed with zero findings. Complete logs and structured reports were inspected. Three JVM notices, four cursor CloseGuard records and the Detekt Gradle deprecation match verified prior evidence. Two consecutive independent final reviews are clean. All eleven commits against the live base are locally verified and GitHub reports
verified=true,reason=valid.Both released apps passed 16 fresh native phases: rejection/lost-acknowledgement prepare/resume/restore and encrypted restore in both directions. Each process acknowledged its own record before its first submission attempt, retained the original signed bytes and exactly one unique operation, and preserved the complete WALLET sentinel. Repeated refresh deliberately exercises two attempts with that same operation; this is not a single-attempt claim. These use real native signing/encryption and loopback protocol/mock authorization, not a complete authenticated J1 or deployed-contract settlement.
The initial Android install failed because the package service was unavailable; one scoped cold boot preserved userdata, and unchanged APKs were then installed and verified. An earlier cross-restore crashed during ART instrumentation initialization before test execution. Its bounded retry reached an old fixture operation after expiry and correctly marked proven nonexecution Failed; the expected-backup-failure assertion was no longer applicable. Both failures and the original operation remain preserved. A separate fresh public-seed scenario passed the opposite-platform restore; no old operation was re-signed or wallet reset. Published artifact provenance also records NDK r27d despite the release workflow's r28c setting; no replacement or reproducible-binary claim is made.
This PR remains draft with its existing J1 gaps. Current-head hosted checks are pending; draft-skipped jobs are not passes.
Latest hosted validation (2026-10-26)
Hosted lint passed on current
7c218843; the complete logs and checksum/CRC-verified artifact12682111680contain zero Detekt and Android Lint SARIF/XML findings. Its synthetic merge54df56e7includes mastereb902f52, and lint compiled that composition, whose tree differs from the head. Native/unit/E2E jobs were draft-skipped, so no hosted native execution of that merge or completed J1 is claimed.