Skip to content

fix: prevent false success for rejected on-chain sends #1211

Description

@ovitrif

Parent: #1207
Source: Pubky marketplace Bitkit team brief
Counterpart: synonymdev/bitkit-ios#717

Scope

  • Verify whether Android shares the stale-tip/non-final success condition.
  • Apply the cross-platform root-cause fix or document evidence that the Android path is unaffected.

Acceptance criteria

  • A rejected non-final transaction is never represented as sent.
  • Accepted sends retain current behavior.

Why this is required for 2.6.0

Required for Shop support in Bitkit 2.6.0: buyers must be able to tell whether an on-chain payment was accepted and recover an uncertain checkout without paying twice.

  • Prevent false success: Bitkit must not show “sent” or deliver payment proof when the backend rejected the transaction or acceptance is still unknown; the Shop order may remain unpaid.
  • Prevent accidental double payment: reopening or retrying an uncertain checkout must retain the original payment instead of starting a separate payment.
  • Preserve the merchant amount on retry: use only the original inputs and recipient amount; a Max payment without enough fee headroom must fail safely rather than reduce what the merchant receives.
  • Resolve stale Pending state: once the exact original payment succeeds and its local follow-up is saved, the app must reflect that result.
  • Preserve protection after wallet restore: restoring a backup must retain the unresolved payment guard so it cannot silently authorize another send.

These are release acceptance requirements. Final Shop checkout still needs validation against the Paykit/server versions selected for 2.6.0.

Activity

  1. self-assigned this
    on Sep 1, 2026
  2. added this to the 2.6.0 milestone on Sep 1, 2026
  3. ovitrif commented on Sep 1, 2026

    @ovitrif
    CollaboratorAuthor

    Blocked by synonymdev/ldk-node#112.

    Android confirms the same false-success path as synonymdev/bitkit-ios#717 on LDK Node 0.7.0-rc.63:

    • LightningService.send calls sendToAddress / sendAllToAddress and receives a Txid.
    • LightningRepo.sendOnChain immediately creates sent metadata and activity from that Txid.
    • AppViewModel immediately opens the Bitcoin Sent result.
    • LDK Node only enqueues the transaction at that point; Electrum, Esplora, or bitcoind submission runs later, and rejection/timeout cannot reach the caller.

    An Android-local acceptance check is not safe: the binding exposes neither the raw transaction nor a transaction-keyed broadcast result, locally persisted wallet state is not backend acceptance, and wallet sync races the independent broadcast queue.

    The production fix belongs in LDK Node. Android should remain In progress until a released binding exposes accepted versus rejected submission and this app consumes that contract. No Android files were changed.

  4. modified the milestones: 2.6.0, 2.7.0 on Sep 25, 2026
  5. ovitrif commented on Sep 30, 2026

    @ovitrif
    CollaboratorAuthor

    Refs:

    The current fix separates backend acceptance from transaction creation. Normal sends, Max, transfers and Shop payments use the explicit node outcome; rejected or unknown results keep the original attempt protected rather than authorizing another payment.

    The app saves a bounded attempt guard before dispatch. Accepted results can finish local storage/activity/proof work without another send; independent positive reconciliation must identify the exact original transaction. Shop proof and backup state also carries explicit acceptance evidence, so a txid-shaped value alone cannot mark a request paid.

    Hardware Shop callbacks require a fresh observation of the exact outgoing transaction in the original persisted hardware wallet. The original identity/request/wallet context is retained; missing observation remains Pending and cannot mint a proof or start a replacement payment.

    This approach intentionally has no transaction journal, automatic rebroadcast, RBF recovery, abandonment or reorg redesign. A lost result can leave the original attempt blocked indefinitely. Backend acknowledgement does not guarantee confirmation, and the bounded local guard does not provide exactly-once guarantees across devices or restored backups.

  6. ovitrif commented on Sep 30, 2026

    @ovitrif
    CollaboratorAuthor

    The implemented acceptance change is in #1384. Review follow-up preserves a stricter boundary: only a proven failure before native dispatch can release its attempt; accepted payments resume original local work without another broadcast.

    A refused result, as well as an unknown/lost result, can leave on-chain sends blocked indefinitely if the transaction never becomes positively observed. The pinned transport can lose earlier delivery history during retries, so a refusal response or negative sync is not a safe no-payment certificate. Refusal release or input-pinning recovery remains an open review discussion and was not implemented.

    Pre-upgrade software proofs from opted-in released builds can also remain undelivered: a queued-era transaction ID lacks positive acceptance and original-wallet provenance. This PR does not promote a local Sent row into acceptance or guess the current wallet. The limitation and proposed migration remain documented in the open review thread.

  7. ovitrif commented on Sep 30, 2026

    @ovitrif
    CollaboratorAuthor

    The accepted-funding local follow-up correction is published in 6c75463: after the original transfer and paid order are durably saved, an activity metadata/readback failure preserves paid-success navigation. Existing resumption repairs the original local work without funding a second order. Funding-persistence failures still retain the original guarded attempt. Compilation and 169 focused tests passed, including three regressions that funded a second order on the old source; actual storage-fault device QA remains unrun.

    @jvsena42 accepted the documented legacy software-proof limitation as non-blocking: released builds can contain opt-in records, but Paykit default-on has not shipped. Migration remains excluded from this PR. The refusal-release discussion remains open.

  8. modified the milestones: 2.7.0, 2.6.0 on Oct 5, 2026
  9. ovitrif commented on Oct 5, 2026

    @ovitrif
    CollaboratorAuthor

    The draft fix now includes explicitly authenticated recovery of the original on-chain payment using LDK rc69. It retains the signed transaction ID and actual input set before broadcast, keeps the original recipient amount/payer/request or funding order, and preserves the active guard in the shared wallet backup. A retry cannot choose replacement inputs, create another Shop request/order, or lower a Max recipient amount.

    Published recovery and backup changes are in PR #1384 at 152b43d. Focused regressions and app/test APK builds pass against the hosted package; current funded retry, native fault and final Shop release-pair QA remain pending. Both app PRs remain draft for Bitkit 2.6.0.

  10. ovitrif commented on Oct 8, 2026

    @ovitrif
    CollaboratorAuthor

    The recovery changes in #1384 are published at 6f46682 and open for human review against the merged, published LDK rc71. An explicit retry now shows the exact signed mining fee and applies normal fee warnings before payment authentication; approval submits that same transaction with the original amount and inputs.

    Restart cleanup uses a durable marker written immediately before native broadcast: preparation proven never dispatched can release its original proof/private-consumption boundary; submitted or uncertain payments remain guarded. A marker write failure prevents dispatch. Saved hardware-payment tags are applied only to the original wallet/transaction.

    Local regression coverage is complete for these fixes. Fresh-device wallet/VSS restore, a funded Shop order with merchant proof/paid-order confirmation, and physical hardware acceptance remain unrun. Human approval and current CI are still outstanding; review status does not claim release acceptance.

  11. ovitrif commented on Oct 9, 2026

    @ovitrif
    CollaboratorAuthor

    The base conflict in #1384 is resolved and published in feb7b16. The fix now inherits the reviewed master branch’s Paykit 0.1.0-rc72, which the contact-link preparation changes require, while native LDK remains 0.7.0-rc.71.

    Focused compatibility checks cover private contact/link/reservation work together with original-payment recovery and the inherited migration paths. This dependency inheritance does not broaden payment-recovery scope or establish funded Shop, fresh-device wallet/VSS or physical hardware acceptance; those checks remain unrun.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions