Conversation
I think that should be up to the implementing wallet, this persistable trait is kind of an inversion of my suggestion which was to instead have the payjoin crate types accept a trait argument that is the thing they use to serialize themselves, i.e. something like: trait Persister {
fn save<T: Serialize>(&self, key: Key, value: T) -> Result<(), PersistableError>;
...
}then it would be the responsibility of the state machine methods to use this abstraction to call the it's not clear to me what to do with regards to pruning storage, i guess the simplest thing is to also have the ability to enumerate all of the keys (which resume would need anyway) and then just filter out those that expired, so that the trait would just need a way to iterate |
|
another thing I'm not sure about is how to keep track of IO related state. for example, when the receiver constructs their response, the state machine logic can serialize that, and then up to the integrating wallet to actually deliver that to the directory. if there is a transient error, retrying makes sense, so keeping track of a successful broadcast in the state machine might also make sense? perhaps a better abstraction, and one that does not rely on traits, is to have the return types from the state machine methods indicate what actions the integrator needs to do, e.g. you get returned a data container that first you must serialize with, that unlocks the actual request data that you need to send, and finally you need to confirm that this happened successfully before you you can load it to resume? that would be a bit more verbose, but removes all side effects including indirect ones |
Pull Request Test Coverage Report for Build 14149717767Details
💛 - Coveralls |
Im a fan of this however there are some trade offs.
Shouldn't be too hard to PoC this. |
I don't have a better suggestion than that
I think rust glue to make a trait impl that delegates to callbacks would suffice
One option is to have a blocking impl wrap around calls to an async runtime. Another is to support an async trait, instead. A third is to retain non-blocking, purely functional behavior from the main types, but implement my alternative idea, i.e. return an struct that wraps around the current return values, and adds the data to persist, and forces the caller to first serialize the data to persist (or at least pretend to do that) before giving back e.g. the extracted request or whatever it is that is returned using the current return types |
2f2e441 to
cdb1538
Compare
I was able to reduce the footprint quite a bit by passing in pub fn check_broadcast_suitability<P: Persister>(
self,
min_fee_rate: Option<FeeRate>,
can_broadcast: impl Fn(&bitcoin::Transaction) -> Result<bool, ImplementationError>,
persistor: P,
) -> Result<MaybeInputsOwned, ReplyableError>
where
P::Key: From<bitcoin::Txid>,
{
let inner = self.v1.check_broadcast_suitability(min_fee_rate, can_broadcast, persistor)?;
Ok(MaybeInputsOwned { v1: inner, context: self.context })
}It would make sense to just have the persister top level in the This resume functionality could be replaced all together using the new persister paradigm. e.g you can persist the state machine and pick up where you left off for each individual payjoin session you have. |
I like how this is looking nit: spelling varies, persistEr vs. persistOr
Hmm, wouldn't initializing such a field after deserialization kinda reduce to the same approach as passing it into various state transition functions as in your example? Perhaps a session manager struct that has that and which reduces the boilerplate is in order, but I think that's a bit premature, I think for now we should figure out the low level stuff and make sure it removes the burden from understanding when is the right time to serialize from implmentors, and then see how we can reduce the boilerplate. With that in mind, perhaps the persister API should return a kind of storage token for lack of a better term (conceptually like a ticket at a bag check), like
Do you mean as a CLI command? If not then isn't that already what's happening under the hood? |
yes the CLI command. My understanding of resume is that it always starts the type state machines from their initial states. For example, if the reciver made it all the way to
Im not sure im fully grasping this idea. The problem being solves seems to be around preventing accidental forks and multiple parallel sessions (which im not sure what the worst case senario there is. Is it possible you can have two parallel PJs spending different UTXOs? That seems like its an application responsibility). |
Specifically I'm thinking of something like the receiver responding twice with two different payjoin PSBTs to the same original PSBT due to a loss of state. The damage in this case would be leaking more UTXOs than intended to the sender. If persistence and processing is done in lockstep, then we can help the application ensure that the response is only processed once, whereas if it's the application's responsibility to persist state information at the right time and with the right semantics, a bug causing processing to happen twice is more likely, especially if treating payjoin stuff as a black box and not studying the protocol in detail.
Shouldn't the state be part of the serialized value, not the key? FWIW serde supports tagging enums with the type name, that seems like less hassle (esp. wrt database format upgrades) than assigning ints, but that's also supported. While we have struct types representing state, they are not quite the typestate pattern since there's no single parameterized type wrapping these, and there also isn't an enum representing the tagged union of possible states as a single type, i suspect there might be an idiomatic approach to this that also takes care of serialization, |
We could wrap the actual typestate struct in a top level enum. There we can take advantage of the tagged enum. #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "state", content = "data")]
pub enum PayjoinProposalState {
Unchecked(UncheckedProposalData),
MaybeInputsOwned(MaybeInputsOwnedData),
MaybeInputsSeen(MaybeInputsSeenData),
OutputsUnknown(OutputsUnknownData),
WantsOutputs(WantsOutputsData),
WantsInputs(WantsInputsData),
Provisional(ProvisionalProposalData),
Final(PayjoinProposalData),
}Using serde_json for example would give us a output of I can get a POC on this pretty quick |
Depends. If we want to store each indiviual state then it would make sense if the state type was encoded as the key. Otherwise in KVP DBs apps would just overwrite db entries |
cdb1538 to
1864bdd
Compare
If they aren't overwritten then doesn't that mean it's trivial to accidentally restore a stale state and resume the state machine from that state? |
No b/c you could sort by the last byte which is the index into state machine; Overwriting would make life simpler for the developers but my impression of the ticket discription is that we want to persist every state transition. |
In a KVS wouldn't that be better expressed by storing a list of states underneath a single key instead of needing to scan all the keys and filter by prefix to find the ones associated with a particular session, then sort them?
As long as information is not lost by overwrites, namely the original PSBT & bip21 URI, which can be useful for interpreting the final txn, I don't think all states need to be strictly stored. Also, for looking up this historical data, either the ID should eventually become the final txid for both, or we'd want to have some sort of auxiliary index to avoid needing to scan all old sessions in order to find historical information associated with a confirmed txn? In a flat KVS, this can be done in the same key space by just storing an entry with the final txid with the internal ID that was used prior to learning that final txid as the value. Either way I'd be in favor of packaging all of a session's data under a single KVS key, serializing the history of states should be negligibly costlier than just the new information, it ensures everything is sorted so there's never any ambiguity about the latest state, esp. in concurrent setting where multiple disparate keys can't be updated atomically with most KVSs (but individual ones usually can). |
yeah lets go with this. Also the application can decide how and if they want to store every state. |
543014a to
0e8ec8f
Compare
|
A couple noteable changes:
It may be prudent to also persist after we have contributed inputs. Since inputs contribution will not be deterministic, this could lead UTXO probing. But at the same time, those inputs could have been spent. I'll leave this as open work for the future as its outside the scope of the ticket. Open question: does it make sense to impl persister for any other type states? @DanGould had mentioned in a irl convo that it could make sense to save the last state EDIT: I couldn't justify having a v1 persister. Simply wanst getting used anywhere. If there is demand for it I would be trivial for me to add it back |
cdbac74 to
72a3258
Compare
FWIW this is essentially what we do with state machine closures since there is no stable, first-class syntax for “async closures”. A simple save closure could just pass |key, value| that are both
Indeed, we're using structs, and maybe "state machine" has more semantic correctness than typestate machine to describe our implementation.
The downstream wallet should save anything it signs or wishes to spend. Participants should save orignal Psbt info in order to recall fees it spent outright vs fees spent by the sender to incentivize payjoin and to show the upgrade path to a user, or to broadcast it in case payjoin fails. Receivers should save payjoin PSBTs so that they know what upgraded PSBT failed, for feerate reasons or otherwise. Enumerating Points of Persistence in
|
|
TODO:
|
8734939 to
71de223
Compare
|
Another comment brought up offline w/ @DanGould was to seperate out all persistanc related implementation details to a persist.rs in both receiver/sender |
There was a problem hiding this comment.
Even if keys had internal strings, I think we'd want them to be strong types so that save / load are constrainted to their own state machines. Which is here now. The design in general looks good to me. My comments are mostly about keeping our commit log and history tidy and the type of interface for Key that I think we should have to display it in errors.
think this is ok for now esp since there is no refrence impl for ns1r. Altenatively we could pass a noop here with a TODO that this will be implemented once there is a ns1r refrence impl. This is what I originally had.
Since the implementation is straightforward I prefer what you've done here, and actually implemented. I don't see a huge reason to take on tech debt when there are the other implementations to follow.
If debt is prefered, I think intentional exclusion, as seems to have been done with multiparty Receiver, makes more sense than a default NoopPersister.
The commit log references Persistable which is no longer.
I'd also like to see Key impl Display instead of using to_hex_string from the result of AsRef
|
|
||
| use crate::uri::url_ext::ParseReceiverPubkeyParamError; | ||
|
|
||
| pub type ImplementationError = Box<dyn std::error::Error + Send + Sync>; |
There was a problem hiding this comment.
Since this duplicates receiver::ImplementationError would be nice to de-duplicate in lib.rs or a core/error.rs module in a follow up.
There was a problem hiding this comment.
If we can do this in a follow up i would prefer that
Add persistence abstractions with `Value` and `Persister` traits to define a common interface for storing and retrieving Payjoin related datastructures. The `Value` trait enables types to generate their own storage keys, while the Persister trait provides a generic interface for saving and loading values. The no-op implementation stores values in memory without actual persistence. Issue: payjoin#336
55f8b00 to
1792479
Compare
|
Getting a 405 from codecov. Doenst seem like this is a problem on our side |
DanGould
left a comment
There was a problem hiding this comment.
As far as I can tell the commit messages still reference an API that is no longer what is implemented:
"commit adds a new Persister parameter to v2::Sender/Receiver::new" I don't think this is done with a new function any longer. May you please fix this and then do a thorough self review before the next iteration?
|
Another itch is that |
I think Persist::persist is less clear than value. Persist::Value is self-explanatory |
This commit introduces a persistence flow for constructing `v2::Receiver`. We enforce that receivers must be persisted before initiating their typestate. To accommodate this, callers will first create a `NewReceiver`, which must be persisted. Upon persistence, they receive a storage token that can be passed to `v2::Receiver::load(...)` to construct a `v2::Receiver`. For cases where persistence is unnecessary (e.g. in testing), implementers can use the `NoopPersister` available in `persist/mod.rs`. Issue: payjoin#336
This commit introduces a persistence flow for constructing `v2::Sender`. We enforce that senders must be persisted before initiating their type state. To support this, callers will first create a `NewSender`, which must be persisted. Once persisted, a storage token is returned that can be passed to `v2::Sender::load(...)` to construct the full `v2::Sender`. For use cases that do not require persistence—such as testing— can use the `NoopPersister` provided in `persist/mod.rs`. Issue: payjoin#336
1792479 to
9e137e3
Compare
There was a problem hiding this comment.
ACK 9e137e3
Hitting deadlines is a skill. 🎆
There's definitely still a lot of funkiness around the existing cli implementation that exist outside of the Persister paradigm. e.g. get_send_session knows the key is a Url to resume send_payjoin, but what in the API suggests that? Should there be a SenderToken::new function to parse it from a bip21?
Same with get_send_sessions and get_recv_sessions, Should there be a Persister function that returns an Iterator?
And finally there is clear, which is meant to mark a session as finished and no longer going to resume. Unclear whether CLI persistence should be fixed to be semantically correct first or whether a Persister implementation should abstract over current behavior, but we probably need a tracking issue to watch progress here in a series of follow ups
| /// A payjoin V2 receiver, allowing for polled requests to the | ||
| /// payjoin directory and response processing. |
There was a problem hiding this comment.
I think this comment is meant to live with Receiver, NewReceiver is the unpersisted variety whose only purpose is to be persisted
spacebear21
left a comment
There was a problem hiding this comment.
I think all my comments can be addressed in follow-ups and aren't show-stoppers but I wanted to make note of them before this gets merged.
|
|
||
| impl Receiver { | ||
| impl NewReceiver { | ||
| /// Creates a new `Receiver` with the provided parameters. |
There was a problem hiding this comment.
This docstring should be updated since it now creates a NewReceiver
| Ok(receiver) | ||
| } | ||
|
|
||
| pub fn persist<P: Persister<Receiver>>( |
| } | ||
|
|
||
| impl Receiver { | ||
| pub fn load<P: Persister<Receiver>>( |
| /// Opaque key type for the receiver | ||
| #[derive(Debug, Clone, PartialEq, Eq)] | ||
| pub struct ReceiverToken(ShortId); | ||
|
|
||
| impl Display for ReceiverToken { | ||
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } | ||
| } | ||
|
|
||
| impl From<Receiver> for ReceiverToken { | ||
| fn from(receiver: Receiver) -> Self { ReceiverToken(id(&receiver.context.s)) } | ||
| } | ||
|
|
||
| impl AsRef<[u8]> for ReceiverToken { | ||
| fn as_ref(&self) -> &[u8] { self.0.as_bytes() } | ||
| } | ||
|
|
||
| impl persist::Value for Receiver { | ||
| type Key = ReceiverToken; | ||
|
|
||
| fn key(&self) -> Self::Key { ReceiverToken(id(&self.context.s)) } | ||
| } |
There was a problem hiding this comment.
Perhaps this should live in receive/v2/persist.rs to separate concerns between persistence stuff and the receiver state machine. (With a corresponding send/v2/persist.rs for sender persistence stuff).
| let new_receiver = NewReceiver::new(mock_address, directory, bad_ohttp_keys, None)?; | ||
| let storage_token = new_receiver.persist(&mut NoopPersister)?; |
There was a problem hiding this comment.
nit: these can be chained since the "new_receiver" variable isn't really useful
| let new_receiver = NewReceiver::new(mock_address, directory, bad_ohttp_keys, None)?; | |
| let storage_token = new_receiver.persist(&mut NoopPersister)?; | |
| let storage_token = NewReceiver::new( | |
| address.clone(), | |
| directory.clone(), | |
| ohttp_keys.clone(), | |
| Some(Duration::from_secs(0)), | |
| )? | |
| .persist(&mut NoopPersister)?; |
| // This method fails if no recommendation can be made or if the PSBT is malformed. | ||
| pub fn build_recommended(self, min_fee_rate: FeeRate) -> Result<Sender, BuildSenderError> { | ||
| Ok(Sender { | ||
| pub fn build_recommended(self, min_fee_rate: FeeRate) -> Result<NewSender, BuildSenderError> { |
There was a problem hiding this comment.
Having a SenderBuilder return a NewSender that must then be persisted to get a Sender seems somewhat awkward. Usually a "XBuilder" returns a "X". I'm not sure whether it makes sense to merge the SenderBuilder and NewSender steps but this feels slightly off.
| #[derive(Clone, Debug, PartialEq, Eq)] | ||
| pub struct SenderToken(Url); | ||
|
|
||
| impl Display for SenderToken { | ||
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } | ||
| } | ||
|
|
||
| impl From<Sender> for SenderToken { | ||
| fn from(sender: Sender) -> Self { SenderToken(sender.0.endpoint().clone()) } | ||
| } | ||
|
|
||
| impl AsRef<[u8]> for SenderToken { | ||
| fn as_ref(&self) -> &[u8] { self.0.as_str().as_bytes() } | ||
| } | ||
|
|
There was a problem hiding this comment.
This seems like a duplicate of the SenderToken in send/v2/mod.rs, could that be imported here?
| } | ||
|
|
||
| impl NewSender { | ||
| pub fn persist<P: Persister<Sender>>( |
There was a problem hiding this comment.
Needs docstring. Also, the SenderBuilder docstrings may need to be updated to reflect that it now returns a NewSender.
| } | ||
|
|
||
| impl Sender { | ||
| pub fn load<P: Persister<Sender>>( |
Move persistence-related code out of `v2::Sender` and `v2::Receiver` into dedicated `persist.rs` files. This refactor helps declutter the core API and separates unrelated concerns. This change addresses a follow-up item from the [PR that introduced persistence](payjoin#552 (comment)).
These were indentical storage tokens and can be reconciled. This was a follow up item from the [PR that introduce persistence](payjoin#552 (comment))
Move persistence-related code out of `v2::Sender` and `v2::Receiver` into dedicated `persist.rs` files. This refactor helps declutter the core API and separates unrelated concerns. This change addresses a follow-up item from the [PR that introduced persistence](payjoin#552 (comment)).
These were indentical storage tokens and can be reconciled. This was a follow up item from the [PR that introduce persistence](payjoin#552 (comment))
Move persistence-related code out of `v2::Sender` and `v2::Receiver` into dedicated `persist.rs` files. This refactor helps declutter the core API and separates unrelated concerns. This change addresses a follow-up item from the [PR that introduced persistence](payjoin#552 (comment)).
These were indentical storage tokens and can be reconciled. This was a follow up item from the [PR that introduce persistence](payjoin#552 (comment))
Move persistence-related code out of `v2::Sender` and `v2::Receiver` into dedicated `persist.rs` files. This refactor helps declutter the core API and separates unrelated concerns. This change addresses a follow-up item from the [PR that introduced persistence](payjoin/rust-payjoin#552 (comment)).
These were indentical storage tokens and can be reconciled. This was a follow up item from the [PR that introduce persistence](payjoin/rust-payjoin#552 (comment))
Drafting this for now. This is at feature parity with the current functionality of payjoin-cli. Persistance is now implemented for both the sender and reciever. We are only persisting when the sender and reciver are first intialized. The trait design is architected in such a way that its easily expandable for other state of the senders and reciever. For example if a application is expecting to save the state at init and finalize and non of the intermediate steps that is an option. And for the reciever I would advocate in a seperate PR that we persist when the reciever when first crated, proposal fetched and payjoin created.
There is alot to bikeshed when we consider persisting other reciever & sender states. We'll cross that bridge that when we get there but I will say this design is more friendly towards persisting other states, compared to #560.
Alternative design of the same functionality: #560
Issue: #336 , #477