Skip to content
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

taproot swaps #28

Merged
merged 20 commits into from
Jan 16, 2025
Merged

taproot swaps #28

merged 20 commits into from
Jan 16, 2025

Conversation

JssDWt
Copy link
Collaborator

@JssDWt JssDWt commented Dec 13, 2024

This PR adds taproot swap support.

Changes in a nutshell:

  • the term 'redeem' was renamed to 'claim'
  • the public api has changed
  • swaps are now taproot swaps, with
    • a claim script spend path
    • a refund script spend path
    • a cooperative musig key spend path
  • there's a cooperative refund added
  • lock time is now absolute. The default locktime is 1008 blocks from when the swap address was made.

Note about the musig2 implementation:

There is no musig2 implementation in rust-bitcoin at the moment. There is a PR in rust-secp256k1 though. Combining the version from that PR with the taproot parts of rust-bitcoin goes well, as long as keys are serialized/deserialized to convert the incompatible structs from one package to another.

review notes

Apologies for the huge PR. For review I think the best option is:

  • check the public server interface in swap.proto
  • check the pay and refund logic in public_server.rs
  • check the taproot logic in swap_service.rs

@JssDWt JssDWt marked this pull request as ready for review December 13, 2024 17:17
@JssDWt JssDWt force-pushed the jssdwt-taproot-swaps branch from a3c38b3 to 2ee3174 Compare December 16, 2024 09:53
MusigSessionId was renamed to MusigSecRand.
Copy link
Member

@roeierez roeierez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks really good. Everything seems to be in place.
Added review comments.

{
Ok(_) => {}
Err(LockSwapError::AlreadyLocked) => {
return Err(Status::failed_precondition("swap is locked"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean we only have one attempt to pay the user? I am thinking of a case where payment failed and user tries again to complete the swap.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is supposed to unlock after the payment, see #28 (comment)

"failed to persist pay result: {:?}",
e
);
}
};

Ok(Response::new(GetSwapPaymentReply::default()))
let _ = self.swap_repository.unlock_swap(&swap_state.swap).await;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if we attempted to pay and there was some error before getting into this point? For example if the lightning_client.pay failed? Will the swap stay locked forever?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would remain locked forever, yes.

The lightning clients should return PaymentResult::Failure, rather than an error in most circumstances. (Which they don't! TODO!).

The thing with the locking is, I want to avoid races. If this line here is reached, you know the swap was locked by the current call to pay_swap. So it's safe to unlock in this function. We could add some logic to allow relocking the swap for payment, to allow payment in case of a crash. Which should be fine if there are no htlcs pending. But then the unlock mechanism needs to change, because the 'unlock' just releases the lock, it has no counter. You definitely want to make sure the cooperative refund can't happen while paying.

I don't like this whole database lock, but I don't really see a good way to do this safely, across restarts as well. If we start reasoning from the refund:

  • The fact there was a refund definitely needs to be stored. If there ever was a refund issued, you never want to pay again.
  • In order to avoid races with refund/pay, the refund and pay have to use the same lock.

If you have a better solution, please share!

Copy link
Member

@roeierez roeierez Dec 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking using the lock only for refund means that before we refund we lock and by that we mark the swap as eligible for refund and not eligible for payment anymore. This covers one side of the race.
For the other side (not refunding while payment in progress) perhaps we can use the payment attempts table?
I see that before payment we add an entry there and after we record the results (which is only handled by the preimage monitor so it should fix things after crashes while payment is in progress) so perhaps only allow cooperative refund if there is no payment attempt without result?

{
Ok(false) => {}
Ok(true) => {
if !was_locked {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems that if the server has some unplanned crash we can end up with the locked state forever?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in this case, for refund, that seems like the only viable solution though. If you don't know whether you returned a refund signature or not, you have to consider it refunded.

@JssDWt JssDWt requested a review from roeierez January 13, 2025 10:33
@JssDWt JssDWt force-pushed the jssdwt-taproot-swaps branch 2 times, most recently from d0365a8 to 2e365e5 Compare January 13, 2025 10:52
Swaps are now locked by a row in the database. This allows simultaneous
refund locks and allows removing payment locks based on their label.
@JssDWt JssDWt force-pushed the jssdwt-taproot-swaps branch from 2e365e5 to e36c0e5 Compare January 13, 2025 11:16
Copy link
Member

@roeierez roeierez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Locking mechanism looks good to me. Added one comment regarding refund unlocking.

Ok(())
}

async fn unlock_swap_refund(&self, swap: &Swap, refund_id: &str) -> Result<(), LockSwapError> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we need this function. Now that we have reliable locking mechanism do you see a case where we need to unlock the refund?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theoretically no. I do like the extra safety check in refund_swap for the has_pending_or_complete_payment. It shouldn't happen due to the way the locking works, but perhaps a manual intervention or a bug causes the swap to be accidentally refundable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense.

Copy link
Member

@roeierez roeierez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@JssDWt JssDWt merged commit e36c0e5 into main Jan 16, 2025
16 checks passed
@JssDWt JssDWt deleted the jssdwt-taproot-swaps branch January 16, 2025 09:59
This was referenced Jan 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants