Skip to content

fix: defer proof delivery after durable handoff - #1458

Open
ben-kaufman wants to merge 1 commit into
masterfrom
codex/paykit-observation-proof-handoff
Open

ben-kaufman wants to merge 1 commit into
masterfrom
codex/paykit-observation-proof-handoff

Conversation

@ben-kaufman

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

Copy link
Copy Markdown
Contributor

This PR:

  1. Refreshes existing Paykit display projections from one validated state observation.
  2. Returns local payment completion after durable proof handoff without waiting for remote proof delivery.

Related work: SDK #183, iOS #914, and Server #78. This consumer pins the published rc73 prerelease.

Description

  • Uses the published rc73 SDK observation snapshot in the existing contact and payment-request refresh workers so display records belong to one validated identity and key generation, while keeping action, authorization, eligibility, and dispatch getters authoritative.
  • Completes proven successful payments after durable activity and exact-proof persistence so remote proof delivery does not delay local completion; the existing lifecycle-owned reconciliation and persisted queue deliver proofs after restart.

Proof persistence failure retains the existing immediate-delivery fallback. If neither persistence nor delivery succeeds, the unresolved follow-up and payment guard remain. Unknown payment outcomes remain unresolved; per-request execution claims, save-before-send ordering, exact proof/ciphertext handling, and duplicate-payment prevention are unchanged. Cancellation retains native-operation ownership until the native call completes; refreshes retain runtime, identity, sign-in, and key-generation guards.

The normal display request-snapshot read phase changes from three getter calls (identity validation, request list, and linked peers) to one SDK observation transaction. Message intake, authoritative action reads, and checkpoint-driven rereads remain separate and unchanged. Contact-only loading already used one transaction; reading a full observation adds projection/filtering work, so no contact speedup is claimed. Existing warm StateFlows, coalescing, and persistence are reused, with no new cache or cold-start instant-display promise. Observation failures preserve existing warm display; cold failures do not claim a successful refresh.

Out of Scope

  • SDK/FFI implementation and artifact publication: owned by pubky/paykit-rs#183; this consumer pins the published rc73 artifact.
  • Homeserver and Noise behavior, safety waits, payment retries, forced unlocks, and new UI copy.
  • New persistent caches, additional UI behavior, or replacing authoritative getters globally.
  • Minimum-amount PR fix: validate minimum paykit request amounts #1457 and other independently owned Android changes.
  • Total linking/payment/unlock speedup claims, releases, and merges.

Design

N/A — no UI changes.

Preview

QA Notes

Journeys

N/A — not drivable; see Manual Tests.

The payment-request journey README documents these fault-driven checks. Matching iOS coverage belongs to the iOS consumer change.

Manual Tests

  • In an authorized disposable regtest fixture, hold remote proof delivery after proven payment success → accepted activity and exact proof are durable before local completion; no remote unlock is claimed and no additional payment is dispatched — independent payment-acceptance, durable-storage, and remote-submission fault controls not in Capabilities.
  • Restart before releasing the proof-delivery hold, then restore the original identity and release it → the same proof is delivered without another payment or duplicate activity — restart delivery with controlled remote submission not in Capabilities.
  • Repeat with an unknown payment outcome, then separately fail both proof persistence and delivery → unresolved follow-up and payment guard remain; no fresh payment becomes eligible — payment-outcome and storage fault injection not in Capabilities.
  • Switch identity or sign out before delivery; separately reset disposable wallet state → no proof is sent under another identity and delayed work does not repopulate reset state — controlled in-flight delivery and disposable reset fixture not in Capabilities.

Automated Checks

  • added PaykitPaymentProofRepoTest.kt - verifies local completion after successful durable writes during blocked submission, exact-proof delivery after reconstruction, failure fallback, and identity/reset isolation.
  • added PaykitPaymentRequestRepoTest.kt - verifies readable warm flows during blocked refresh, identity clearing, warm-state retention on snapshot failure, and honest cold failure.
  • added PubkyRepoTest.kt - verifies failed observations preserve warm contacts and stale results from an ended same-identity sign-in stay hidden.
  • added PaykitSdkServiceTest.kt - verifies observation identity validation, error propagation, queued runtime replacement, and cancellation without overlapping native operations.
  • updated PaykitKeyGenerationTest.kt - verifies observation key generation matches the active key.
  • updated PaykitPaymentRequestRepoSubscriptionTest.kt, ContactSaveSessionChangeTest.kt, and AppViewModelSendFlowTest.kt - retains request/contact action coverage and verifies proof reconciliation remains lifecycle-owned and coalesced during active payment submission.
  • updated PubkyRepoTest.kt - verifies cold contact failure ends loading without marking a successful observation.
  • ran matched baseline/candidate proof-handoff fixtures with 20 successful file writes plus fsync and a deterministic 5,000 ms virtual remote-submission hold: baseline p50/p95/max local-completion wait was 5,000/5,000/5,000 ms; candidate was 0/0/0 ms of injected virtual wait. This is an isolation/recovery correctness benchmark, not Android Keystore latency, wall-clock speed, or real-network performance.
  • ran matched warm-read fixtures during blocked refresh: 20 reads took 0 ms of injected virtual wait on both baseline and candidate. Warm display isolation already existed; no warm-cache speedup is claimed.
  • ran forced fresh canonical compile and full tests against published GitHub Maven rc73 with configuration/build caches disabled and no init script or local override: 13/13 compile tasks and 44/44 test tasks executed; all 3,955 tests passed with zero failures, errors, or skips. Gradle cached AAR SHA-256 exactly matches the canonical registry artifact: 44cc0ad08e6689c5a820d399d49ad67f2d69f8ff3039f322107525fa97411725.
  • ran canonical format and final lint sequentially; complete Detekt reports contain 1,980 findings versus 1,981 on verified master, with zero introduced findings. All 223 compiler diagnostics, the manifest-removal warning, and the Gradle deprecation notice match baseline. Unrelated baseline formatter rewrites were excluded.

One fresh combined source correctness and quality review and bounded integration follow-up are complete. Controlled payment/restart fault checks above remain incomplete, not passed.

Remote Measurements and Payment QA

  • Published canonical rc72/rc73 native artifacts against the actual staging Homeserver: stored-request refresh measured midpoint medians 2,618.688 ms versus 879.058 ms across 12 samples per arm in ABBA blocks; all 24 passed. Fixture: 32 contacts, empty request/peer/receipt collections, no injected delays or shortened safety waits. Timer covers the repository refresh and model publication, not rendered UI, cold startup, populated history, linking or unlock. Contacts-only and already-cached display have no demonstrated improvement.
  • A separately authorized isolated regtest trial reached native Accepted, but immediate bookkeeping failed with Winning transaction fee is unavailable before this PR's changed deferred-delivery branch. The activity/fee path is byte-identical to base. The fixture omitted the normal post-Accepted wallet synchronization and periodic recovery, so it does not prove a persistent production blocker or explain the reported stuck-wallet incident. No successful payment-completion speedup was measured. The separate validated fee-retention fix is fix: retain prepared transaction fees #1459; unknown-outcome and duplicate-payment guards remain unchanged.

Comment thread app/src/main/java/to/bitkit/repositories/PaykitPaymentProofRepo.kt
@ben-kaufman
ben-kaufman marked this pull request as ready for review October 11, 2026 04:50
@greptile-apps

greptile-apps Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no actionable defects were found.

Summary

This PR uses one checked Paykit observation to refresh contact and payment-request displays. It also lets successful payments finish locally once their exact proof has been saved.

  • Successful payments finish locally after their proof is saved.
  • Contact and payment-request displays refresh from one checked snapshot.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Proven successful payment] --> B[Save exact proof]
  B --> C{Save succeeded?}
  C -->|Yes| D[Finish locally]
  D --> E[Lifecycle reconciliation or restart]
  E --> F[Check original identity]
  F --> G[Submit saved proof]
  C -->|No| H[Try immediate delivery]
  H --> I{Delivery succeeded?}
  I -->|Yes| D
  I -->|No| J[Retry saving proof]
  J --> K[Keep unresolved guard if both fail]
Loading

Reviews (1) · Last reviewed commit: "fix: consolidate paykit reads and defer ..." · Reviewed by Greptile

@github-actions

Copy link
Copy Markdown
Contributor

Regtest APK

Built from 268293a (run).

Download bitkit-dev-debug universal APK (expires in 30 days).

@talosmachina talosmachina 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.

No findings. Bumps paykit to rc73, moves the contact and payment-request display refreshes onto the single observeState() transaction, and lets proven Lightning, on-chain and hardware completions return once the proof is persisted, leaving delivery to reconcile(). Reviewed 268293a, full tier. Reasoned from the code and CI; per the repo's agent rules this branch is not built here.

What I checked, and 4 candidates I ruled out

Read in full: the diff of all 17 files; persistAndSubmit, reconcile, reconcileProof, reconcileLightningProof, reconcileOnchainProof, reconcileHardwareOnchainProof, submitReady, completeOnchainPayment and paymentRequestStateChanges in PaykitPaymentProofRepo.kt; refresh and fetchRequestSnapshot in PaykitPaymentRequestRepo.kt; loadContacts, currentSignIn and isCurrent in PubkyRepo.kt; observeState, withPaykitKey and refreshPaykitKey in PaykitSdkService.kt; observe_state and state_observation_in_transaction in pubky/paykit-rs#183.

Call sites traced: completeLightningPayment (handlePaymentSuccessful), completeOnchainPayment (completeOnchainPaymentProofInBackground, completeRecoveredOnchainPayment), the hardware original-payment observer, every reconcile() caller (refreshIncomingPaykitPaymentRequests, the not-dispatched send error, BackupRepo).

CI: CI (build, unit tests), build-local, lint, detekt and Greptile green at this head; the e2e-tests-local shards were still pending when I read them.

Ruled out

  • A persisted proof that nothing delivers: the persist bumps PaykitPaymentProofStore.backupStateVersion, paymentRequestStateChanges sees proofData move from null to the preimage or txid, and the observer now passes reconcileProofs = true, so reconcile() reaches submitReady. That covers the one-time on-chain path too, where completeOnchainPaymentProofInBackground only refreshes when billingPeriod != null. The Lightning path also gets the FULL refresh right after completeLightningPayment.
  • Reconcile looping on its own writes: submitReady removing the delivered proof changes the observed state once, then distinctUntilChanged holds; an undeliverable proof is not re-persisted by reconcile(), so offline does not spin. Two concurrent reconciles are covered by the alreadyQueued check under operationMutex.
  • Changed meaning of completed for the Shop follow-up: persistAndSubmit already returned true on a successful persist before this change, so completeAcceptedShopFollowup fires on the same condition; only the wait for submitReady is gone, and the persist-failure branch still delivers immediately.
  • Contacts on a session without the Paykit secret: observe_state requires the identity secret, while the old contact_records() needed only an initialized identity, so a restored session whose local key cannot be read (a Ring-adopted identity while the Ring provider fails the trust check) now fails the contacts load where it used to succeed. Warm contacts are kept and the trigger is a transient provider failure, so I ruled it out, narrowly.

Merge confidence: 4/5, the contacts candidate above was ruled out only narrowly and the e2e shards had not finished.

This branch has not been deployed

No deployments
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.

3 participants