Receiever Monitor Typestate#1061
Conversation
3c034fd to
9ac3c5f
Compare
9ac3c5f to
7b63061
Compare
Pull Request Test Coverage Report for Build 18196173117Details
💛 - Coveralls |
3d3d801 to
d06b610
Compare
spacebear21
left a comment
There was a problem hiding this comment.
Approach ACK. I agree using callbacks in monitor makes sense as it's cohesive with the rest of the Receiver state machine.
d06b610 to
2e195f3
Compare
|
@spacebear21 does this error make any sense to you. Coming from |
|
This is a bug in uniffi-dart, looks like a typo possibly... Ignore for now while I investigate with @chavic |
|
Confirmed locally that it was a simple typo, I opened a fix PR here: Uniffi-Dart/uniffi-dart#85 You could change the uniffi-dart git rev in Cargo.toml to point to that branch to unblock CI in the meantime until it gets merged. |
2e195f3 to
07b8821
Compare
The latest merge fixed a bug in callbacks that return Option<T>, which popped up in payjoin#1061
`430fe0b` fixed a bug in callbacks that return Option<T>, which popped up in payjoin#1061
`430fe0b` fixed a bug in callbacks that return Option<T>, which popped up in payjoin#1061
2ecd9de to
8ed4389
Compare
| return MaybeFatalOrSuccessTransition::transient(Error::Implementation( | ||
| ImplementationError::from(format!("Payjoin transaction ID mismatch. Expected: {payjoin_txid}, Got: {tx_id}").as_str()), | ||
| )); |
There was a problem hiding this comment.
Is this an implementation error? I think it should be an API error.
There was a problem hiding this comment.
I'm not following why this branch should error, it says "If we have a payjoin transaction with segwit inputs, we can check for the txid". But here wouldn't any non-segwit transaction always fail the check and immediately return a transient error?
There was a problem hiding this comment.
The error branch here indicates that the user has confirmed the payjoin was broadcasted but they gave the API an entirely different transaction -- meaning we cannot close the session with the correct witness/scriptsig
But here wouldn't any non-segwit transaction always fail the check and immediately return a transient error?
No b/c a tx with non-segwit inputs should hit the Ok(None) branch
8ed4389 to
149296a
Compare
spacebear21
left a comment
There was a problem hiding this comment.
There is a typo in the commit message: "The typestate accepts two fallbacks" should be "callbacks".
Left code/approach comments, I haven't looked closely at FFI changes or the tests but will review those more closely in a second pass.
| proposal: Receiver<Monitor>, | ||
| persister: &ReceiverPersister, | ||
| ) -> Result<()> { | ||
| // On a session resumption, the receiver will resume again in this state. |
There was a problem hiding this comment.
Isn't this implicit for all of these helper methods?
There was a problem hiding this comment.
Yeah this comment is out of place. My intention was to illustrate that we are not explictly not looping and sleeping here. I'll just remove this comment.
payjoin/src/core/persist.rs
Outdated
| where | ||
| Err: std::error::Error, | ||
| { | ||
| #[cfg(test)] |
There was a problem hiding this comment.
why were these changed to test-only?
There was a problem hiding this comment.
Previously they were being used by the last typestate of the receiver statemachine. That typestate now uses a different state transition.
| return MaybeFatalOrSuccessTransition::success(SessionEvent::Closed( | ||
| SessionOutcome::FallbackBroadcasted, |
There was a problem hiding this comment.
In Payjoin terms shouldn't this be considered a Failure? The wallet may consider the payment successful but a Payjoin did not occur.
There was a problem hiding this comment.
I was going back and forth on this. Will change to a plain failure
Edit: the outcome of this comment depends on #1061 (comment)
payjoin/src/core/receive/v2/mod.rs
Outdated
| let mut outpoints_spend = vec![]; | ||
| for ot in payjoin_proposal.unsigned_tx.input.iter() { | ||
| match outpoint_spent(ot.previous_output) { | ||
| Ok(false) => {} | ||
| Ok(true) => outpoints_spend.push(ot.previous_output), | ||
| Err(e) => | ||
| return MaybeFatalOrSuccessTransition::transient(Error::Implementation(e)), | ||
| } | ||
| } | ||
|
|
||
| if outpoints_spend.len() == payjoin_proposal.unsigned_tx.input.len() { |
There was a problem hiding this comment.
Could this be simplified to:
if payjoin.proposal.unsigned_tx.input.iter().all(|ot|
match outpoint_spent(ot.previous_output) {
Ok(detected) => detected,
Err(e) =>
return MaybeFatalOrSuccessTransition::transient(Error::Implementation(e)),
}
) {
// All the payjoin proposal outpoints were spent. This means our payjoin proposal has non-segwit inputs and is broadcasted.
// ...
}There was a problem hiding this comment.
Would this not complicate the else if block? We would have to interate once more and is .some()
There was a problem hiding this comment.
Ah yeah I missed that block. Maybe something like iter().filter(|ot| ...).count(); would work, I think an iterator looks cleaner but this is a nit.
| if outpoints_spend.len() == payjoin_proposal.unsigned_tx.input.len() { | ||
| // All the payjoin proposal outpoints were spent. This means our payjoin proposal has non-segwit inputs and is broadcasted. | ||
| return MaybeFatalOrSuccessTransition::success(SessionEvent::Closed( | ||
| // TODO: there seems to be not great way to get the tx of the tx that spent these outpoints. |
There was a problem hiding this comment.
Do you mean the signatures? What if outpoints_spend returned a Option<TxOut> or similar instead of a bool?
There was a problem hiding this comment.
Option
My comment is suggesting that there is no way to get the tx that spent an outpoint/utxo -- we would then use that tx to get the missing witness/scriptsig. Bitcoind rpc only returns a non-null value when the txout exists in the utxoset db
https://bitcoincore.org/en/doc/29.0.0/rpc/blockchain/gettxout/
There was a problem hiding this comment.
I see, that seems like a bitcoind implementation detail though? I'm not sure our API should make this baked in assumption. In theory the callback would need to return Result<Option<TxIn>, ImplementationError>, and the question becomes "how to obtain those TxIn with the RPC"?
There was a problem hiding this comment.
In theory the callback would need to return Result<Option, ImplementationError>,
We already have the TxIn. The questions is if the outpoint is still in the utxoset. i.e spent.
and the question becomes "how to obtain those TxIn with the RPC"?
yes exactly. Implementers would need to keep an index of outpoint/utxo to txid. Which is uncommon. Alternatively, implementers can look up the txid by having index of what txs paid them. This is more common however, outside the scope of the API
| /// Fallback transaction was broadcasted | ||
| FallbackBroadcasted, |
There was a problem hiding this comment.
I'm not sure that this new variant is necessary, wouldn't a Failure or Cancel be appropriate?
There was a problem hiding this comment.
I guess the answer depends on wether or not we want a dedicated fallback broadcasted session status.
There was a problem hiding this comment.
I think we should be able to deduce that a fallback tx was broadcasted by replaying the event log up to the monitor typestate if that results in a Failure? Therefore a distinct outcome doesn't provide additional information. I would like @nothingmuch 's input on this though if he's available.
79a03c1 to
7bc0138
Compare
| AppliedFeeRange(PsbtContext), | ||
| FinalizedProposal(bitcoin::Psbt), | ||
| GotReplyableError(JsonReply), | ||
| PostedPayjoin(), |
There was a problem hiding this comment.
nit: PostedPayjoinProposal()
| /// TODO: this should include the sender's witnesses once tx monitoring is implemented | ||
| /// (this will ensure the payjoin txid can be computed) | ||
| Success, | ||
| Success(Vec<(bitcoin::ScriptBuf, bitcoin::Witness)>), |
There was a problem hiding this comment.
Do we need to expose a txid helper that reconstructs the final txid from the payjoin psbt + sender signatures?
There was a problem hiding this comment.
Yes and no. witness/scripsig is useful incase we have to re-broadcast the tx as the receiver. However, if segwit inputs are used then we do not need to reconstruct the txid. If non-segwit inputs are used we will need to reconstruct the txid but it seems impractical to get the witness/scripsig for the sender inputs
re: #1061 (comment)
| /// Fallback transaction was broadcasted | ||
| FallbackBroadcasted, |
There was a problem hiding this comment.
I think we should be able to deduce that a fallback tx was broadcasted by replaying the event log up to the monitor typestate if that results in a Failure? Therefore a distinct outcome doesn't provide additional information. I would like @nothingmuch 's input on this though if he's available.
spacebear21
left a comment
There was a problem hiding this comment.
We should follow up with integration tests on the various monitoring outcomes: payjoin tx broadcasted, fallback tx broadcasted, doublespend detected, segwit/non-segwit.... seems important to get these right.
A payjoin session is closed once there is nothing more to do in the protocol but wait for expiry, or confirmation of one of the possible transactions to settle the outcome. This commit introduces a monitoring typestate as the final receiver typestate. Monitoring checks for the confirmation of session relevant transactions. Concretely we check for: * The payjoin proposal via txid if all inputs are segwit. * The fallback tx via txid. * The payjoin proposal via spent outpoints if at least one input is not segwit. * Any spent outpoint which would indicate a double spend of an outpoint that was used in the payjoin. The typestate accepts two callbacks: * `transaction_exists(txid)` * `outpoint_spent(outpoint)` If a broadcasted payjoin proposal was detected the session will close and the success event will include the sender `scriptsig` and/or `witness`. Fallbacks and double spends will also close the session and the event logged will include which condition closed the session.
7bc0138 to
0c67af7
Compare
spacebear21
left a comment
There was a problem hiding this comment.
code ACK 0c67af7
I'm still on the fence about SessionOutcome::FallbackBroadcasted vs. using Failure but that doesn't need to block this PR from getting merged, we can make a note to discuss this on the next dev call.
A payjoin session is closed once there is nothing more to do in the
protocol but wait for expiry, or confirmation of one of the possible
transactions to settle the outcome.
This commit introduces a monitoring typestate as the final receiver
typestate. Monitoring checks for the confirmation of session
relevant transactions. Concretely we check for:
segwit.
that was used in the payjoin.
The typestate accepts two fallbacks:
transaction_exists(txid)outpoint_spent(outpoint)If a broadcasted payjoin proposal was detected the session will close
and the success event will include the sender
scriptsigand/orwitness. Fallbacks and double spends will also close the session andthe event logged will include which condition closed the session.
Related to #807
Pull Request Checklist
Please confirm the following before requesting review:
AI
in the body of this PR.