Skip to content

Expose proposal txid stability on receiver states#1718

Open
chavic wants to merge 1 commit into
payjoin:masterfrom
chavic:chavic/proposal-txid-stability
Open

Expose proposal txid stability on receiver states#1718
chavic wants to merge 1 commit into
payjoin:masterfrom
chavic:chavic/proposal-txid-stability

Conversation

@chavic

@chavic chavic commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

This is the 1.0-sized companion to the problem #1692 solves properly. #1692 makes non-SegWit sessions monitorable by classifying spends of the contested outpoints; this PR only makes the existing limitation visible at the right moment, without touching any frozen type. It's intended as the machine-readable form of the disclaimer suggested as the 1.0 mitigation in #1692 (document that check_for_transaction doesn't properly support non-SegWit inputs).

Today, Monitor::check_for_transaction knows the proposal's transaction ID cannot be tracked when the sender contributed non-SegWit inputs, and concludes the session as PayjoinProposalSent without checking the network. That knowledge arrives too late for receivers that do settlement accounting. A receiver that credits payments (a merchant server, an exchange) typically records the expected final transaction ID when it hands out the proposal — for SegWit-only sender inputs, unsigned_tx.compute_txid() stays valid once the sender re-signs, so pinning it enables exact reconciliation later. With any non-SegWit sender input the pinned ID is silently wrong: the sender's final signature rewrites the script_sig and changes the ID. Nothing warns the host at the moment the expectation is recorded; the first signal is the blind conclude at Monitor time.

This exposes the predicate Monitor already uses as proposal_txid_is_stable() on every state that holds the fallback transaction (the HasFallbackTx states), refactors check_for_transaction to use it (behavior unchanged), and mirrors it in payjoin-ffi. Receivers can then pick a policy as soon as the original payload is checked: decline to contribute and let the fallback pay, or reconcile that session by script/outpoint instead of by transaction ID. The BTCPay Server payjoin plugin currently implements the conservative policy by parsing the original transaction host-side (ValeraFinebits/btcpayserver-payjoin-plugin#78); this accessor lets any wallet make the same call without re-deriving protocol internals.

Relationship to #1692: strictly additive and shape-neutral, so it neither constrains nor competes with contested-outpoint classification. If/when that lands, this accessor degrades gracefully from "necessary policy input" to "convenience": hosts that reconcile by pinned transaction ID still want to know stability up front, while hosts on classify() won't need it for monitoring at all.

Disclosure: co-authored by Claude Code

Pull Request Checklist

Please confirm the following before requesting review:

Monitor::check_for_transaction already knows the proposal's
transaction ID cannot be tracked when the sender contributed
non-SegWit inputs, and concludes the session as PayjoinProposalSent
without checking the network. That knowledge arrives too late for
receivers that record the expected transaction ID when handing out
the proposal, e.g. to reconcile an incoming payment: with a
non-SegWit sender input the recorded ID is silently wrong, because
the sender's final signature rewrites the script_sig and changes
the ID.

Expose the predicate Monitor uses as proposal_txid_is_stable() on
every state that holds the fallback transaction, and mirror it in
payjoin-ffi. Receivers can now pick a policy as soon as the
original payload is known: decline to contribute and let the
fallback pay, or reconcile that session by script instead of by
transaction ID.
@coveralls

Copy link
Copy Markdown
Collaborator

Coverage Report for CI Build 29016109853

Coverage increased (+0.004%) to 85.872%

Details

  • Coverage increased (+0.004%) from the base build.
  • Patch coverage: 6 of 6 lines across 1 file are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 15558
Covered Lines: 13360
Line Coverage: 85.87%
Coverage Strength: 350.5 hits per line

💛 - Coveralls

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.

2 participants