Skip to content

Seal fallback_tx and deprecate extract_tx_to_schedule_broadcast - #1994

Open
spacebear21 wants to merge 3 commits into
payjoin:masterfrom
spacebear21:seal-fallback-tx
Open

spacebear21 wants to merge 3 commits into
payjoin:masterfrom
spacebear21:seal-fallback-tx

Conversation

@spacebear21

@spacebear21 spacebear21 commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

The sealed FallbackTx trait behind HasFallbackTx was pub inside a private module. That keeps it unnameable from outside the crate, but Rust still lets generic code call the methods of a reachable supertrait, so a downstream function bounded on S: HasFallbackTx could call receiver.fallback_tx() and read the fallback transaction from any mid-protocol state, including Monitor, without going through cancel() and the Cancelled event it records. The first commit narrows the trait to pub(crate), which makes the compiler reject that call with E0624 while code written against HasFallbackTx keeps compiling, and adds a compile_fail doctest that guards the seal.

The remaining commits follow the same principle for the one accessor that handed the fallback out early on purpose. Receiver<MaybeInputsOwned>::extract_tx_to_schedule_broadcast exists so an application can schedule the fallback broadcast out of band, but a broadcast scheduled that way leaves no trace in the session log. Scheduling it should go through cancel(), which is available from every state that holds a verified fallback, records the event, and yields Receiver<PendingFallback> with the transaction to broadcast. The v2 method is now deprecated with its docs pointing at cancel(), and payjoin-cli stops printing the fallback hex when the Original PSBT arrives and points at payjoin-cli cancel <session-id> instead.

Points for review:

  • This PR only makes sense to merge if we agree that not exposing fallback_tx on every typestate is the right way forward.
  • This is technically a breaking change. The seal closes an undocumented leak, but code that relied on it stops compiling. The changelog entry says so plainly.
  • The deprecation uses the 0.0.0-unreleased placeholder in the Rust attribute and as since payjoin 0.0.0-unreleased in the FFI docstring, for bump.sh to fill in.
  • The v1 receiver's extract_tx_to_schedule_broadcast is untouched. v1 has no session or cancel path to point to.
  • UniFFI has no deprecation attribute, so the FFI wrapper carries the deprecation in its docstring and an #[allow(deprecated)] on the call. The generated bindings are gitignored and built from the FFI source, so no binding files change.
  • Changelog lines were added under Unreleased for payjoin, payjoin-cli, and payjoin-ffi.

The sealed `FallbackTx` trait was `pub` inside a private module, which
makes it unnameable from outside the crate but still reachable through
the public `HasFallbackTx` bound. Rust lets generic code call the
methods of a reachable supertrait, so a downstream function bounded on
`S: HasFallbackTx` could call `receiver.fallback_tx()` and read the
fallback transaction from any state, bypassing the `cancel` path and
the Cancelled event it records.

Narrow the trait to `pub(crate)`. The compiler then rejects the call
with E0624 even inside a generic function, while code written against
`HasFallbackTx` keeps compiling. A private supertrait on a public trait
has only been the `private_bounds` lint since Rust 1.74, which the MSRV
of 1.85 covers, so the two allows mark the mismatch as deliberate. A
compile_fail doctest guards the seal; its error code is enforced on
nightly and ignored on stable.
@coveralls

coveralls commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Coverage Report for CI Build 39269677647

Coverage decreased (-0.02%) to 87.45%

Details

  • Coverage decreased (-0.02%) from the base build.
  • Patch coverage: 5 of 5 lines across 2 files are fully covered (100%).
  • 3 coverage regressions across 1 file.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

3 previously-covered lines in 1 file lost coverage.

File Lines Losing Coverage Coverage
payjoin/src/core/receive/v2/mod.rs 3 92.34%

Coverage Stats

Coverage Status
Relevant Lines: 17849
Covered Lines: 15609
Line Coverage: 87.45%
Coverage Strength: 319.74 hits per line

💛 - Coveralls

@spacebear21
spacebear21 marked this pull request as draft October 19, 2026 14:27
When the Original PSBT arrived, the v2 receiver printed its hex and
suggested broadcasting it by hand if the payjoin failed. That is the
pattern payjoin is moving away from: a fallback broadcast should go
through the cancel path, which records the Cancelled event in the
session log and hands back the transaction as Receiver<PendingFallback>,
so the session history and the chain agree on what happened. The CLI
already implements that path as `payjoin-cli cancel <session-id>`.

Replace the hex dump with a pointer to the cancel command, matching the
wording used when a session is interrupted or fails. This also removes
the CLI's only call to extract_tx_to_schedule_broadcast ahead of its
deprecation.
Receiver<MaybeInputsOwned>::extract_tx_to_schedule_broadcast handed out
the fallback transaction up front so an application could schedule its
broadcast out of band. A broadcast scheduled that way leaves no trace in
the session event log: the session stays Active while the fallback is
already on the chain, and the log only learns of it if the session later
reaches Monitor and check_for_transaction finds the fallback.

Scheduling a fallback broadcast should instead go through cancel(). It
is available from every state that holds a verified fallback, records
the Cancelled event, and yields Receiver<PendingFallback>, whose
fallback_tx is the transaction to broadcast before close() ends the
session. Applications that need the transaction after the fact can read
it from SessionHistory::fallback_tx.

Mark the v2 method deprecated and point its docs at cancel(). The v1
receiver keeps its method: it has no session or cancel path. Move the
session tests that used it as a shortcut to the original PSBT, drop the
unused call from the integration test, and keep the FFI wrapper
compiling with an allow while its docstring carries the deprecation,
since UniFFI has no deprecation attribute.
@spacebear21
spacebear21 marked this pull request as ready for review October 19, 2026 14:32

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

About ready to ACK these changes but seems like it needs a rebase and a CI job is failing.

Also, these changes must be shipped in the same CLI version as #1971 (which I don't think will be a problem), otherwise when check_for_transaction is called for non-segwit input txs the sessions will close automatically without giving recourse to broadcast fallback tx.

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