-
Notifications
You must be signed in to change notification settings - Fork 78
Fix pjos handling in v2 receiver and sender
#847
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Notably, assert that the receiver's output substitution preference is respected.
Pull Request Test Coverage Report for Build 16036827797Details
💛 - Coveralls |
payjoin/src/core/send/v2/mod.rs
Outdated
| // V2 senders may always ignore the receiver's `pjos` output substitution preference, | ||
| // because all communications with the receiver are end-to-end authenticated. | ||
| if self.0.output_substitution == OutputSubstitution::Enabled { | ||
| v1.output_substitution = OutputSubstitution::Enabled; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bit of a hack but I couldn't think of another way to enforce this override without many breaking changes in the v1 SenderBuilder.
Additionally, this doesn't account for V2 senders that downgrade to V1 for a V1 receiver but as Dan said elsewhere "I don't really like that that's not just a v1 sender in the first place" - so maybe that should be handled in a follow-up?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah IMO best to separate as much as possible, this feels like the kind of confusion that often arises with OO due to inheritance, where code reuse and subtyping relationships are at odds...
IMO it makes more sense to have separate logic for v2 than to have complicated interactions in the name of code reuse, so the right thing to me seems to first check the URI and determine which version of payjoin it is as an enum, where each branch corresponds to a separate SenderBuilder. This is more burdensome for app developers but i think it's an important distinction to make.
V2 receivers should always signal `pjos=0` to preserve backwards compatibility with V1 senders, since V1 communication is unauthenticated and goes through an untrusted payjoin directory. V2 senders may safely ignore this flag because all communications are end-to-end authenticated and allowing output substitution enables more interesting use cases for receivers. A V2 sender may still signal their own output substitution preference via the `disableoutputsubstitution` request param.
Per [BIP77](https://github.com/bitcoin/bips/blob/master/bip-0077.md#payjoin-uri): > Since BIP 78 payloads are neither encrypted nor authenticated, a directory used for backwards-compatible payloads is known as an "unsecured payjoin server" in BIP 78 parlance. Backwards-compatible receivers MUST disable output substitution by setting pjos=0 to prevent modification by a malicious directory. Note that pjos=0 is ignored by V2 senders per the previous commit.
nothingmuch
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(this is from yesterday, i just noticed i neglected to post it, so it's out of date... i will return to this shortly sorry)
utACK, assuming the hackyness is going to addressed as followup. I'm concerned that without doing that something similar might crop up, as arguably the bug is the result of tech debt in the first place
payjoin/src/core/send/v2/mod.rs
Outdated
| Ok(inner) => inner, | ||
| Err(e) => return MaybeBadInitInputsTransition::bad_init_inputs(e), | ||
| }; | ||
| // V2 senders may always ignore the receiver's `pjos` output substitution preference, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // V2 senders may always ignore the receiver's `pjos` output substitution preference, | |
| // V2 senders interacting with V2 receivers may ignore the receiver's `pjos` output substitution preference, |
"always" seems a bit too broad, and could be interpreted to mean v2 senders interacting with v1 receivers that explicitly disabled it unrelated to BIP 77's requirement
payjoin/src/core/send/v2/mod.rs
Outdated
| // V2 senders may always ignore the receiver's `pjos` output substitution preference, | ||
| // because all communications with the receiver are end-to-end authenticated. | ||
| if self.0.output_substitution == OutputSubstitution::Enabled { | ||
| v1.output_substitution = OutputSubstitution::Enabled; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah IMO best to separate as much as possible, this feels like the kind of confusion that often arises with OO due to inheritance, where code reuse and subtyping relationships are at odds...
IMO it makes more sense to have separate logic for v2 than to have complicated interactions in the name of code reuse, so the right thing to me seems to first check the URI and determine which version of payjoin it is as an enum, where each branch corresponds to a separate SenderBuilder. This is more burdensome for app developers but i think it's an important distinction to make.
| .build_recommended(FeeRate::BROADCAST_MIN) | ||
| .save(&NoopSessionPersister::default()) | ||
| .expect("sender should succeed"); | ||
| // v2 senders may always override the receiver's `pjos` parameter to enable output |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the "always" phrasing invites even more ambiguity in this testing context, see above comment & suggestion
| // V2 senders may always ignore the receiver's `pjos` output substitution preference, | ||
| // because all communications with the receiver are end-to-end authenticated. | ||
| if self.0.output_substitution == OutputSubstitution::Enabled { | ||
| v1.output_substitution = OutputSubstitution::Enabled; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have w similar concern as @nothingmuch regarding this override. yes it works to override v1 fields with v2 preferences but it might be more legible if the hierarchy of v1/v2 preferences were evaluated at the specific version's code and passed into a general function on sender::[v1::?]PsbtContext logic that took into account the net preference alone.
I had toyed around with tearing out PsbtContext from v1::Sender to fix #809 last week but couldn't justify the additional code. This change might change that balance in favor of the abstraction.
DanGould
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we're taking on some tech debt with this one but it does seem to fix and test for problem it purports to address
ACK dee6890
| min_fee_rate: FeeRate, | ||
| ) -> MaybeBadInitInputsTransition<SessionEvent, Sender<WithReplyKey>, BuildSenderError> { | ||
| let v1 = match self.0.build_non_incentivizing(min_fee_rate) { | ||
| self.v2_sender_from_v1(self.0.clone().build_non_incentivizing(min_fee_rate)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These self.0.clone() smell like we're taking on tech debt but I couldn't find an easy solution in my 15 minutes of play with it.
| ) | ||
| .build_recommended(FeeRate::MIN); | ||
| assert!(sender.is_ok(), "{:#?}", sender.err()); | ||
| assert_eq!(NON_WITNESS_INPUT_WEIGHT, bitcoin::Weight::from_wu(160)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I missed the removal of this assert, we need it to catch mutants on the logic for this const.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This just prevents any mutants from being formed on the operands adding the index and sequence
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, sorry for removing it but it seemed unrelated to that specific test. #875 presents a good opportunity to add this back in an appropriate unit test.
`{v1,v2,multiparty}::Sender{,Builder}` types have been built off of a
heirarchy where multiparty:: has a v2:: and v2:: has a v1::, which was
done for quick convenience and not because that relationship actually
makes functional sense.
This PR is the first in a few which I believe are necessary to rectify
this distinction. It does so by drawing the separation between the
sender's `PsbtContext` checks / response processing and the
version-specific serialization for networked messaging. However, I don't
think it goes far enough.
For one, it only really rectifies this issue between v1 and v2.
Multiparty is left abstract over v2. Second, There are still distinct
SenderBuilders that can't tell whether or not they're handling a v1 or
v2 URIs. Since the information necessary to distinguish between a v1/v2
URI is in the URI itself, it seems that ought to be the first order of
business for the sender to do even before calling `SenderBuilder::new`.
The lack of this distinction leads to a
[problem](bitcoindevkit/bdk-cli#200 (comment))
with a hacky
[solution](bitcoindevkit/bdk-cli#200 (comment))
where downstream users need to wait all the way until they attempt to
create a v2 request and handle an error there in order to figure out the
version. The `SenderBuilder` also ought to behave differently for each
version, and I'm not sure our current fix of #847 does this completely
(Does a v2 SenderBuilder sending to v1 URI honor pjos? it should). In
order to do so I reckon we could create an actual `PjUri` type, rather
than an alias, that enumerates over the versions when
`check_pj_supported` or its replacement is called. In order to do *that*
effectively by making sure the correct parameters are there and we're
not just switching on the presence of a fragment, I think `UrlExt` also
needs to check for the parameter presence and validity.
The other issue with v1::Sender flow is that it doesn't use the generic
typestate machine pattern to match v2, which would be nice as well but
out of the scope of this PR.
re: #809
Closes #872, closes #836, closes #771, closes #890 This addressed #778 for now but I still would like to create a test instead of excluding it I also added the basic test that should address the mutation from #847 (comment) The exclusions are catogerized as follows. - timeouts - mutations that allow code to simply skip or follow a code path that does not actually cause or prevent an error - expiry time checks I found not worth creating a test for. - TODO exclusions
Closes #872, closes #836, closes #771, closes #890 This addressed #778 for now but I still would like to create a test instead of excluding it I also added the basic test that should address the mutation from payjoin/rust-payjoin#847 (comment) The exclusions are catogerized as follows. - timeouts - mutations that allow code to simply skip or follow a code path that does not actually cause or prevent an error - expiry time checks I found not worth creating a test for. - TODO exclusions
This fixes #843 and ensures the desired behavior is tested explicitly.