-
Notifications
You must be signed in to change notification settings - Fork 76
Separate receiver::{v1,v2} abstraction cleanly #998
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
Pull Request Test Coverage Report for Build 17276344135Details
💛 - Coveralls |
806fc20 to
58d7c6e
Compare
| Original { psbt: PARSED_ORIGINAL_PSBT.clone(), params } | ||
| } | ||
|
|
||
| // TODO: restrict to v1 only since UncheckedProposal is a v1 only type |
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 addressed later in the history
payjoin/src/core/receive/mod.rs
Outdated
| impl PsbtContext { | ||
| /// Prepare the PSBT by creating a new PSBT and copying only the fields allowed by the [spec](https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#senders-payjoin-proposal-checklist) | ||
| fn prepare_psbt(self, processed_psbt: Psbt) -> Psbt { | ||
| pub(crate) fn prepare_psbt(self, processed_psbt: Psbt) -> Psbt { |
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 function and finalize_proposal are eventually returned to private in this PR. This is temporary
|
|
||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] | ||
| pub(crate) struct PsbtContext { | ||
| pub struct PsbtContext { |
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 receive::v2::SessionEvent::ProvisionalProposal(PsbtContext) requires this visibility. It's possible to wrap this struct if desired, but PsbtContext is the actual event diff that we're using. Perhaps it could be pruned further, just to the bitcoin::Psbt that's the payjoin_psbt since we already have the original psbt at the UncheckedProposal stage.
IMO UncheckedProposal((Original, Optioncrate::HpkePublicKey)) deserves its own type as well instead of a tuple but that can come after. The UncheckedProposal tuple should probably at least be deconstructed out of the nested tuple to UncheckedProposal(Original, Option<crate::HpkePublicKey>)
payjoin/src/core/receive/mod.rs
Outdated
| /// 1. Remove all sender signatures which were received with the original PSBT as these signatures are now invalid. | ||
| /// 2. Sign and finalize the resulting PSBT using the passed `wallet_process_psbt` signing function. | ||
| pub fn finalize_proposal( | ||
| pub(crate) fn finalize_proposal( |
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.
as stated above this can be made private later on when some more shuffling is done.
| self, | ||
| v1: common::ProvisionalProposal, | ||
| ) -> ReceiveSession { | ||
| pub(crate) fn apply_provisional_proposal(self, psbt_context: PsbtContext) -> ReceiveSession { |
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 can see making a NewType/new name for this, but it's not strictly necessary yet.
TODO: reduce pub(crate) fn visibility to private visibility for all of these apply state Receiver methods.
| let psbt_context = wants_fee_range.clone()._apply_fee_range(None, None).unwrap(); | ||
| let psbt = psbt_context |
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 use of the private _apply_fee_range function is transient until further separation is done. you'll see.
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 dont quite understand this comment.
_apply_fee_range is pub(crate)'d. Did you mean to remove this in the latest commit? FWIW this method would be apply_fee_range bc it is used by methods in the crate and apply_fee should be _apply_fee but I dont think a _ prefix is necessary eitherway.
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 only problem is a naming conflict with v1 when both v1 and v2 feature flags are on.
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.
It is pub(crate), by private I mean that it's only used internally and is not part of the public api. the _ prefix reinforces this.
On a second look I think I should just get rid of this and rely on the actually private apply_fee directly on both v1 and v2 implementations to avoid this confusion.
| let persister = NoopSessionPersister::<SessionEvent>::default(); | ||
|
|
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 problem here is that the v1 typestate machine was awkwardly being used to produce the v2 types.
If we made helper transition methods that didn't save and just produced errors, we could test without any persister at all, and have the main transition functions call those helpers. But we don't really need that additional boilerplate and can just test using a NoopSessionPersister.
I tried to leave expects where there were expects and otherwise just unwrap().
| pub(crate) original_psbt: Psbt, | ||
| pub(crate) payjoin_psbt: Psbt, | ||
| pub(crate) params: Params, | ||
| pub(crate) change_vout: usize, | ||
| pub(crate) owned_vouts: Vec<usize>, |
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.
Because tests are still tangled across the common/v1 modules, this visibility change and others in this commit are required. It's reversed in a later commit which untangles the tests.
| /// Protocol-specific errors for BIP-78 v1 requests (e.g. HTTP request validation, parameter checks) | ||
| #[cfg(feature = "v1")] | ||
| V1(crate::receive::common::RequestError), | ||
| V1(crate::receive::v1::RequestError), |
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.
Ahhh now you can sense some relief. Nice abstractions are coming soon :)
58d7c6e to
d8ced52
Compare
| let original = original_from_test_vector(); | ||
| let wants_inputs = WantsOutputs::new(original, vec![0]).commit_outputs(); |
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.
Simplified so as not to use the v1-exclusive UncheckedProposal state
d8ced52 to
ccc7ff6
Compare
|
@arminsabouri There is a lot of movement here but nothing complex is added or removed. It's a pure shuffle. I left comments commit by commit so please review in that way so the comments can help you. The focus of this work is to untangle the v1/v2 logic so that there is a clean split between shared PSBT logic and isolated, version-specific serialization. In essence:
This PR stops short of a visibility or naming audit. It may go further with newtypes and even a reduction in v2 typestates, though I think those can follow #995 where you add a I'm copying much of this message to OP because I think it better describes the issue than OP |
|
I left a bunch of comments on the source so that you can understand why intermediate changes were made. Please let me know if you need any more detail. I'd like to get this in ASAP since it's such a big move and rebase on this is gonna be tricky. |
arminsabouri
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.
Approach/utACK
Shuffling seems sane. I had a couple questions about some shared abstractions that could also live in common, some struct field renames, and a question about a apply_fee_range when both v1 and v2 feature flags are on. Otherwise had some nits and stylistic nits that can be ignored and/or followed up on if applying historical is too tricky/not worth the time.
| let original = original_from_test_vector(); | ||
| let unchecked_proposal = unchecked_receiver_from_test_vector(); | ||
| let maybe_inputs_owned = | ||
| unchecked_proposal.clone().assume_interactive_receiver().save(&persister).unwrap(); |
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.
Nit: I have a prefrence over expects here. It describes exactly what error condition out of many we expect not to happen. e.g since assume_interactive_receiver is a next state only transition the only thing we expect not to happen is a storage error. Which is infallible for a noop persister. Marked as nit. Not mission critical.
| use crate::output_substitution::OutputSubstitution; | ||
| use crate::psbt::PsbtExt; | ||
| use crate::receive::{InternalPayloadError, Original, PsbtContext}; | ||
|
|
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 expected Orignial to live here as well as its a common abstraction. Any reason why its left out?
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.
Same thing for PsbtContext
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.
What you seem to be asking for is to empty receive/mod.rs in favor of common. But we want common specifically to hide types from the top level receive API where Original and PsbtContext are exported. It's not a catchall. receve::common is only for hidden shared types. Otherwise I would put everything in common into recieve/mod.rs.
Now, Original and PsbtContext almost definitely ought to become private in receive/mod.rs, but right now they're in the public interface of receive::v2::session::SessionEvent, so those variants need to encapsulate different or newtypes in order to get rid of that visibility in a follow-up.
| change_vout: self.change_vout, | ||
| receiver_inputs: self.receiver_inputs, | ||
| /// [`RequestError`] should only be produced here. | ||
| fn validate_body(headers: impl Headers, body: &[u8]) -> Result<&[u8], RequestError> { |
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.
Stylistic Nit: This method is only used in UncheckedProposal but defined in the bottom of the file. I would expect to be defined along side build_v1_pj_uri.
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.
Don't we typically put pub fn (like build_..) at the top of the file and fn at the bottom? I didn't write this, just moved it.
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.
Fair enough. If that is consistent with the rest of the repo thats good with me
payjoin/src/core/receive/v2/mod.rs
Outdated
| #[derive(Debug, Clone, PartialEq)] | ||
| pub struct WantsOutputs { | ||
| v1: v1::WantsOutputs, | ||
| v1: common::WantsOutputs, |
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.
Since this is no longer wrapping v1 typestates.
| v1: common::WantsOutputs, | |
| inner: common::WantsOutputs, |
payjoin/src/core/receive/v2/mod.rs
Outdated
| #[derive(Debug, Clone, PartialEq)] | ||
| pub struct WantsInputs { | ||
| v1: v1::WantsInputs, | ||
| v1: common::WantsInputs, |
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.
Same thing here
payjoin/src/core/receive/v2/mod.rs
Outdated
| #[derive(Debug, Clone, PartialEq)] | ||
| pub struct WantsFeeRange { | ||
| v1: v1::WantsFeeRange, | ||
| v1: common::WantsFeeRange, |
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.
And here
| output_contribution_weight | ||
| } | ||
|
|
||
| impl crate::receive::common::WantsFeeRange { |
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.
Could this be problematic if both v1 and v2 feature flags are on? If both are on IIUC the apply_fee_range impl below would override the one in common.
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.
As far as I can tell this would only be an API-side oddity. Applications cannot call the v1 apply_fee_range method if they have a v2 typestate.
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 checked that this is not an issue with cargo doc --open --all-features. This impl is scoped to v1::WantsFeeRangeand does not apply tov2::WantsFeeRangewhich is public, only tocommon::WantsFeeRange*in the v1 scope*.common::WantsFeeRangeis not part of the public interface since this type is only ever publicly exported as part of thereceive::v1` scope.
| let psbt_context = wants_fee_range.clone()._apply_fee_range(None, None).unwrap(); | ||
| let psbt = psbt_context |
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 dont quite understand this comment.
_apply_fee_range is pub(crate)'d. Did you mean to remove this in the latest commit? FWIW this method would be apply_fee_range bc it is used by methods in the crate and apply_fee should be _apply_fee but I dont think a _ prefix is necessary eitherway.
| let psbt_context = wants_fee_range.clone()._apply_fee_range(None, None).unwrap(); | ||
| let psbt = psbt_context |
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 only problem is a naming conflict with v1 when both v1 and v2 feature flags are on.
Code that was only for v1 and NOT v2 was in the common folder.
The `exclusive` name does not make sense in this context since it's really the `v1` module now. That change comes next.
Make intenal WantsFeeRange::_apply_fee_range function and external v1-exclusive after which the v1 typestates can be used since the v2 sumtype can use the underlying type directly (or eventually encapsulate them itself).
- have the parameters reflect their new names - add a docstring
That's convention.
Untangle it. Call that conditioner.
cause it just tests WantsOutputs
ccc7ff6 to
eac5e77
Compare
|
Ok I think I fixed the problems you brought up and made sure each and every commit was linted and formatted. I want to address the |
as Armin prefers.
arminsabouri
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.
utACK de883a6
|
survived |
There is a lot of movement here but nothing complex is added or removed. It's a pure shuffle.
The focus of this work is to untangle the v1/v2 logic so that there is a clean split between shared PSBT logic and isolated, version-specific serialization.
In essence:
receive::v1::exclusivebecomesreceive::v1which is only compiled whenfeature = "v1".receive::v1becomesreceive::commonand includes types used whenfeature = "_core". Typestates defined here but only used whenfeature = "v1are moved toreceive::v1. It's alwayspub(crate)so there's no conditional visibility on whether or notfeature = "v1", because that is completely separated intoreceive::v1.common.This PR stops short of a visibility, documentation, or naming audit. I may consider renaming
receive::commontoreceive::statesbut open to suggestion. It does need to be a submodule in order to stay invisibile wrt thereceive::scope. It may go further with newtypes and even a reduction in v2 typestates as follow up, though I think those tasks can follow #995 whereReceiverBuilderis added and theUninitializedstate is removed as well.Pull Request Checklist
Please confirm the following before requesting review: