Introduce SessionEvent::Closed#1078
Conversation
Pull Request Test Coverage Report for Build 17920670454Details
💛 - Coveralls |
payjoin/src/core/receive/v2/mod.rs
Outdated
|
|
||
| (_, SessionEvent::SessionInvalid(_, _)) => Ok(ReceiveSession::TerminalFailure), | ||
|
|
||
| (current_state, SessionEvent::Closed) => Ok(current_state), |
There was a problem hiding this comment.
I'm not sure if this is actually necessary, in theory a closed session should never get replayed.
There was a problem hiding this comment.
We would replay a closed session for getting the session history.
E.g if session expires we replay and get the fallback
arminsabouri
left a comment
There was a problem hiding this comment.
it's still TBD whether this should be an inner value like Closed(Reason) or distinct SessionEvent variants for each known reason. I'm leaning towards the former if we can make it work.
I also agree with the former
54660f6 to
88895f0
Compare
88895f0 to
586199e
Compare
|
Instead of |
arminsabouri
left a comment
There was a problem hiding this comment.
Approach ACK. The one thing I am ambivalent on is the inner string on the Abort variant
| /// Payjoin failed to complete | ||
| Failure, | ||
| /// Payjoin was aborted by the caller with the provided reason | ||
| Abort(String), |
There was a problem hiding this comment.
Do we need to store the user provided reason on our event log? This does feel like purely an application concern.
| Success, | ||
| /// Payjoin failed to complete | ||
| Failure, | ||
| /// Payjoin was aborted by the caller with the provided reason |
There was a problem hiding this comment.
Is the caller strictly the application here?
There was a problem hiding this comment.
I think there may be situations where the state machine aborts, e.g. if the monitoring typestate detects a doublespend? It is primarily there to expose an "abort" method to the application though.
There was a problem hiding this comment.
i think there should be a separate UserAbort (unit type) to clearly indicate that the user (not the app) chose to abort for whatever arbitrary reason (and if that reason is relevant the app should track it in its own data).
i'm not convinced we need another kind of abort though, a Failure (also still unit type) seems sufficient? my reasoning for this is kinda long sorry...
for double spends, from the pov of the receiver, and assuming the receiver is honest, i believe the full tree of protocol outcomes, happy path first is:
- receiver generates pj URI
- sender sends fallback via pj endpoint
- receiver replies with payjoin psbt
- sender broadcasts payjoin txn
- receiver broadcasts fallback txn after expiration
- sender broadcasts fallback txn
- sender broadcasts payment txn (e.g. due to partial loss of state maybe?)
- sender broadcasts double spend fallback/payjoin txn (without paying receiver)
- receiver times out trying to send reply, or fails to sign, and broadcasts fallback txn
- sender broadcasts fallback txn
- sender broadcasts payment txn
- sender double spends fallback txn (without paying receiver)
- receiver replies with payjoin psbt
- sender broadcasts payment txn
- sender sends fallback via pj endpoint
and subsequently, the txn replacement and confirmation (and reorg) state machine is the wallet's responsibility, like it is for its non-payjoin transactions (i.e. Success also indicates successfuly producing a payjoin txn, not necessarily confirming it or getting paid).
if a human user is involved in receiving, double spends may be intended, and wallets should not do anything automatically but could propose fee bumping txns that pay the wallet.
if the receiver is automated, it may be appropriate to respond to double spends by aggressive fee bumping, especially if UTXO probing is a consideration.
in both settings a double spend of the fallback is arguably a deviation from the protocol. imagine a scenario where responding takes a long time, e.g. the receiver must access a hardware wallet to respond with a payjoin psbt, and detects a double spend of the fallback before producing a payjoin psbt, or also covering the automated setting, maybe the directory is temporarily unavailable.
- continue (in which case a malicious sender may learn a UTXO but not end up signing and still get in a fee bumping war over the double spend in the worst case, but without a reply the sender is definitely not signing the payjoin psbt, whereas if it does receive a reply there's at least a non 0 chance)
- consider this a protocol deviation (which it arguably is) and mark the session as failed due to double spend (another
Abortvariant? orFailedwith a reason?) - alert the user (only in the interactive setting) who can abort or not (in which case it's continue)
failing requires the app to notify us of this failure so in some sense it's just reporting a reason to close the session, and that reason might be a specific txn of interest, but it doesn't really make sense to treat this txid as necessary event data IMO, it is enough that we identify the txids of the success case(s) and let the wallet figure out that a conflicting txn exists, is winning a fee race, or got confirmed in line with its chain querying capabilities, as is the decision whether to engage in a fee bidding war.
where such an explicit failure or abort due to double spend event does make sense is when continuing is deemed inappropriate, and we want to prevent accidental replying if the session is resumed later.
if the log is replayed at this point and the failure outcome is recorded, then we would still have the fallback txn to give to the wallet and it could use its chainstate logic to figure out whether the failure was a double spend.
| /// Payjoin failed to complete | ||
| Failure, |
There was a problem hiding this comment.
How are we defined Failure? Is it just protocol and session errors? e.g would session expired get its own variant?
There was a problem hiding this comment.
there's two kinds of success that IMO are worth distinguishing:
- payment succeeded (valid fallback received) but payjoin failed
- payjoin succeeded (the payjoin state machine has be notified of this by the wallet, since we don't know the full payjoin txn until it's relayed or confirmed, the proposal is missing signatures)
in the 2nd scenario we can tell the wallet what txid confirmations to monitor for confirmation as well as produce the payjoin txn to (re)broadcast as appropriate
if the sender only used segwit inputs, we also know the txid to expect for the payjoin txn if the sender completes it but we can't rebroadcast it without being notified about it due to the signature data being missing
and the failure cases as i see them:
- abort (user decided to cancel)
- expiration (can be implicit as just lack of successs & expiry time having elapsed? not sure an event is needed unless we distrust the receiver's sytem clock)
- counterparty deviated from the protocol (invalid messages)
other errors like the directory or relay failing could be seen as transient until expiration unless the user aborts so i don't see much sense in recording those
finally i think we should be clear in documenting these that this is purely for the payjoin session's success, i.e. a (partly) successful payjoin session does not imply the payment itself succeeded as that depends on the confirmation which is the wallet's purview
So to make this actionable:
Successshould contain the missing sender's witnesses that allow completion of the payjoin psbt into a confirmable transaction and guarantee its txid can be computed (after Receiever Monitor Typestate #1061)- The unit
Failurevariant should be renamed to indicate a deviation from the counterparty (maybeSenderFailurefrom the receiver's POV?) Expiredcould make sense as an additional variant if it can't be inferred- there should be a method on the history that allows querying the status and returns an
enum { Active, Closed(Outcome) }whereOutcomeis an enum that also contains the inferred failure modes that don't need an explicit event since they don't have any prior information to contribute to the log (e.g. Expired if it's not represented as a serialized event)
There was a problem hiding this comment.
Thanks for writing this up. This all checks out with me.
Success should contain the missing sender's witnesses that allow completion of the payjoin psbt into a confirmable transaction and guarantee its txid can be computed (after #1061)
ack. However, should sender witness live in a seperate receiver session event? As I understand it, the closed event is payjoin role agnostic.
There was a problem hiding this comment.
As currently written the Closed event is specific to the Receiver (it lives inside receive::v2::session::SessionEvent). I think we'll also want to add a Closed variant to the Sender but it would be a separate type.
| /// Payjoin failed to complete | ||
| Failure, |
There was a problem hiding this comment.
there's two kinds of success that IMO are worth distinguishing:
- payment succeeded (valid fallback received) but payjoin failed
- payjoin succeeded (the payjoin state machine has be notified of this by the wallet, since we don't know the full payjoin txn until it's relayed or confirmed, the proposal is missing signatures)
in the 2nd scenario we can tell the wallet what txid confirmations to monitor for confirmation as well as produce the payjoin txn to (re)broadcast as appropriate
if the sender only used segwit inputs, we also know the txid to expect for the payjoin txn if the sender completes it but we can't rebroadcast it without being notified about it due to the signature data being missing
and the failure cases as i see them:
- abort (user decided to cancel)
- expiration (can be implicit as just lack of successs & expiry time having elapsed? not sure an event is needed unless we distrust the receiver's sytem clock)
- counterparty deviated from the protocol (invalid messages)
other errors like the directory or relay failing could be seen as transient until expiration unless the user aborts so i don't see much sense in recording those
finally i think we should be clear in documenting these that this is purely for the payjoin session's success, i.e. a (partly) successful payjoin session does not imply the payment itself succeeded as that depends on the confirmation which is the wallet's purview
So to make this actionable:
Successshould contain the missing sender's witnesses that allow completion of the payjoin psbt into a confirmable transaction and guarantee its txid can be computed (after Receiever Monitor Typestate #1061)- The unit
Failurevariant should be renamed to indicate a deviation from the counterparty (maybeSenderFailurefrom the receiver's POV?) Expiredcould make sense as an additional variant if it can't be inferred- there should be a method on the history that allows querying the status and returns an
enum { Active, Closed(Outcome) }whereOutcomeis an enum that also contains the inferred failure modes that don't need an explicit event since they don't have any prior information to contribute to the log (e.g. Expired if it's not represented as a serialized event)
586199e to
26212ac
Compare
|
@nothingmuch The third commit adds the status() helper method. Instead of nesting another enum here I think it makes sense to just enumerate all variants in the Status enum. I kept the variants pretty basic for now but I expect #1060 and #1061 will add more as needed. We may also want more variants instead of just "Active" to differentiate between the "waiting for sender" and "receiver actively processing" stages (see also payjoin-cli |
nothingmuch
left a comment
There was a problem hiding this comment.
Ack, just one bikeshedding comment re abort (already posted because i couldn't find the thread in the submit review ui how is github this incoherent it's amazing)
@nothingmuch The third commit adds the status() helper method. Instead of nesting another enum here I think it makes sense to just enumerate all variants in the Status enum.
👍 I can see why this would be more ergonomic
I kept the variants pretty basic for now but I expect #1060 and #1061 will add more as needed.
👍
We may also want more variants instead of just "Active" to differentiate between the "waiting for sender" and "receiver actively processing" stages (see also payjoin-cli
StatusTexthttps://github.com/payjoin/rust-payjoin/blob/master/payjoin-cli/src/app/v2/mod.rs#L60).
Hmm, isn't replaying more appropriate to figure that out since the typestate machinery already interprets events into a more granular and statically typed thing?
My reasoning for such a status method instead of representing the different terminal states as typestates is that for the final outcome you only have to look at the last event so it's simpler and more efficient than a full log replay.
For active session, would expect in a long running app there would be some sort of task management where the task that owns the Receiver<T> would make status information available much like payjoin-cli does, just pushing the status updates into a channel or writing them into shared state so a UI thread can display the status.
| /// TODO: this should include the sender's witnesses once tx monitoring is implemented | ||
| /// (this will ensure the payjoin txid can be computed) | ||
| Success, |
There was a problem hiding this comment.
Do we want to include sender witness in this pub enum? If the purpose if to store and provide the payjoin txid, then we should store the sender witness after monitoring in a receiver session event and expose a seperate payjoin_txid method of SessionHistory.
There was a problem hiding this comment.
I think the Monitoring success transition would result in a Closed(Success(witnesses)) instead of the PayjoinTransactionDetected event currently implemented in #1061 .
b5caa54 to
966b6c6
Compare
nothingmuch
left a comment
There was a problem hiding this comment.
some nits and the bikeshedding suggestion i made in DMs
|
|
||
| // Represents the status of a session that can be inferred from the information in the session | ||
| // event log. | ||
| #[non_exhaustive] |
There was a problem hiding this comment.
hmm, what other variants would we expect? not convinced non_exhaustive is needed
There was a problem hiding this comment.
My thinking was we might add more granular failure statuses per:
querying the status and returns an enum { Active, Closed(Outcome) } where Outcome is an enum that also contains the inferred failure modes that don't need an explicit event
But for more detail I guess doing a full replay and using the typestate machinery is more appropriate. Removed non_exhaustive in the latest push.
This allows capturing session closures explicitly in the session event log.
The outcome variants are kept to a minimum because we should always be able to deduce the reason an outcome was reached, by replaying the SessionHistory.
It returns a SessionStatus, which should enumerate the various statuses that are useful to know about but do not require an explicit SessionEvent, since they can be inferred from the event log.
966b6c6 to
81e287e
Compare
|
Merging this based on prior approvals since nits have been addressed. |
The first commit introduces the
Closedsession event. The second commit introducesClosed(SessionOutcome)which can be a Success, Failure, or Abort with a provided reason (e.g. for manual closures by implementers).This PR only uses the
Successreason when a response is successfully processed, but #1061 should update this to close the session only once a payjoin tx was actually detected. #1060 will use the Failure reason for closing sessions after an error has been handled. We'll also want to expose a method for implementers to close a session with a custom reason in another follow-up.There is some overlap between this change and #1075 since this also modifies
MaybeSuccessTransitioncc @arminsabouriPull Request Checklist
Please confirm the following before requesting review:
AI
in the body of this PR.