mirror of https://github.com/lightninglabs/loop
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
532 lines
14 KiB
Go
532 lines
14 KiB
Go
package instantout
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
"github.com/btcsuite/btcd/txscript"
|
|
"github.com/lightninglabs/lndclient"
|
|
"github.com/lightninglabs/loop/fsm"
|
|
"github.com/lightninglabs/loop/instantout/reservation"
|
|
"github.com/lightninglabs/loop/loopdb"
|
|
loop_rpc "github.com/lightninglabs/loop/swapserverrpc"
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
|
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
)
|
|
|
|
var (
|
|
ErrInsufficientReservationBalance = errors.New(
|
|
"insufficient balance in reservations",
|
|
)
|
|
|
|
ErrReservationsMustBeSwapAmount = errors.New(
|
|
"reservations total amount must be equal swap amount",
|
|
)
|
|
|
|
UrgentConfTarget = int32(3)
|
|
NormalConfTarget = int32(6)
|
|
)
|
|
|
|
// InitInstantOutCtx contains the context for the InitInstantOutAction.
|
|
type InitInstantOutCtx struct {
|
|
cltvExpiry int32
|
|
reservations []reservation.ID
|
|
initationHeight int32
|
|
outgoingChanSet loopdb.ChannelSet
|
|
}
|
|
|
|
// InitInstantOutAction is the first action that is executed when the instant
|
|
// out FSM is started. It will send the instant out request to the server.
|
|
func (f *FSM) InitInstantOutAction(eventCtx fsm.EventContext) fsm.EventType {
|
|
initCtx, ok := eventCtx.(*InitInstantOutCtx)
|
|
if !ok {
|
|
return f.HandleError(fsm.ErrInvalidContextType)
|
|
}
|
|
|
|
if len(initCtx.reservations) == 0 {
|
|
return f.HandleError(fmt.Errorf("no reservations provided"))
|
|
}
|
|
|
|
var (
|
|
reservationAmt uint64
|
|
reservationIds = make([][]byte, 0, len(initCtx.reservations))
|
|
reservations = make(
|
|
[]*reservation.Reservation, 0, len(initCtx.reservations),
|
|
)
|
|
)
|
|
|
|
// The requested amount needs to be full reservation amounts.
|
|
for _, reservationId := range initCtx.reservations {
|
|
res, err := f.cfg.ReservationManager.GetReservation(
|
|
f.ctx, reservationId,
|
|
)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
// Check if the reservation is locked.
|
|
if res.State == reservation.Locked {
|
|
return f.HandleError(fmt.Errorf("reservation %v is "+
|
|
"locked", reservationId))
|
|
}
|
|
|
|
reservationAmt += uint64(res.Value)
|
|
reservationIds = append(reservationIds, reservationId[:])
|
|
reservations = append(reservations, res)
|
|
}
|
|
|
|
// Create the preimage for the swap.
|
|
var preimage lntypes.Preimage
|
|
if _, err := rand.Read(preimage[:]); err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
// Create the keys for the swap.
|
|
keyRes, err := f.cfg.Wallet.DeriveNextKey(f.ctx, KeyFamily)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
swapHash := preimage.Hash()
|
|
|
|
// Create a high fee rate so that the htlc will be confirmed quickly.
|
|
feeRate, err := f.cfg.Wallet.EstimateFeeRate(f.ctx, UrgentConfTarget)
|
|
if err != nil {
|
|
f.Infof("error estimating fee rate: %v", err)
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
// Send the instantout request to the server.
|
|
instantOutResponse, err := f.cfg.InstantOutClient.RequestInstantLoopOut(
|
|
f.ctx,
|
|
&loop_rpc.InstantLoopOutRequest{
|
|
Amt: reservationAmt,
|
|
ReceiverKey: keyRes.PubKey.SerializeCompressed(),
|
|
SwapHash: swapHash[:],
|
|
Expiry: initCtx.cltvExpiry,
|
|
HtlcFeeRate: uint64(feeRate),
|
|
ReservationIds: reservationIds,
|
|
ProtocolVersion: loop_rpc.
|
|
InstantOutProtocolVersion_INSTANTOUT_FULL_RESERVATION,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
// Decode the invoice to check if the hash is valid.
|
|
payReq, err := f.cfg.LndClient.DecodePaymentRequest(
|
|
f.ctx, instantOutResponse.SwapInvoice,
|
|
)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
if preimage.Hash() != payReq.Hash {
|
|
return f.HandleError(fmt.Errorf("invalid swap invoice hash: "+
|
|
"expected %x got %x", preimage.Hash(), payReq.Hash))
|
|
}
|
|
serverPubkey, err := btcec.ParsePubKey(instantOutResponse.SenderKey)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
// Create the address that we'll send the funds to
|
|
sweepAddress, err := f.cfg.Wallet.NextAddr(
|
|
f.ctx, "", walletrpc.AddressType_TAPROOT_PUBKEY, false,
|
|
)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
// Now we can create the instant out.
|
|
instantOut := &InstantOut{
|
|
SwapHash: swapHash,
|
|
SwapPreimage: preimage,
|
|
ProtocolVersion: ProtocolVersionFullReservation,
|
|
InitiationHeight: initCtx.initationHeight,
|
|
OutgoingChanSet: initCtx.outgoingChanSet,
|
|
CltvExpiry: initCtx.cltvExpiry,
|
|
ClientPubkey: keyRes.PubKey,
|
|
ServerPubkey: serverPubkey,
|
|
Value: btcutil.Amount(reservationAmt),
|
|
HtlcFeeRate: feeRate,
|
|
SwapInvoice: instantOutResponse.SwapInvoice,
|
|
Reservations: reservations,
|
|
KeyLocator: keyRes.KeyLocator,
|
|
SweepAddress: sweepAddress,
|
|
}
|
|
|
|
err = f.cfg.Store.CreateInstantLoopOut(f.ctx, instantOut)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
f.InstantOut = instantOut
|
|
|
|
return OnInit
|
|
}
|
|
|
|
// PollPaymentAcceptedAction locks the reservations, sends the payment to the
|
|
// server and polls the server for the payment status.
|
|
func (f *FSM) PollPaymentAcceptedAction(
|
|
eventCtx fsm.EventContext) fsm.EventType {
|
|
|
|
// Now that we're doing the swap, we first lock the reservations
|
|
// so that they can't be used for other swaps.
|
|
for _, reservation := range f.InstantOut.Reservations {
|
|
err := f.cfg.ReservationManager.LockReservation(
|
|
f.ctx, reservation.ID,
|
|
)
|
|
if err != nil {
|
|
return f.handleErrorAndUnlockReservations(err)
|
|
}
|
|
}
|
|
|
|
// Now we send the payment to the server.
|
|
payChan, paymentErrChan, err := f.cfg.RouterClient.SendPayment(
|
|
f.ctx,
|
|
lndclient.SendPaymentRequest{
|
|
Invoice: f.InstantOut.SwapInvoice,
|
|
Timeout: time.Minute * 5,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return f.handleErrorAndUnlockReservations(err)
|
|
}
|
|
|
|
// We'll continuously poll the server for the payment status.
|
|
for {
|
|
select {
|
|
case payRes := <-payChan:
|
|
f.Debugf("payment result: %v", payRes)
|
|
if payRes.FailureReason !=
|
|
lnrpc.PaymentFailureReason_FAILURE_REASON_NONE {
|
|
|
|
return f.handleErrorAndUnlockReservations(
|
|
fmt.Errorf("payment failed: %v",
|
|
payRes.FailureReason),
|
|
)
|
|
}
|
|
|
|
case err := <-paymentErrChan:
|
|
return f.handleErrorAndUnlockReservations(err)
|
|
|
|
case <-f.ctx.Done():
|
|
return fsm.NoOp
|
|
|
|
default:
|
|
res, err := f.cfg.InstantOutClient.PollPaymentAccepted(
|
|
f.ctx, &loop_rpc.PollPaymentAcceptedRequest{
|
|
SwapHash: f.InstantOut.SwapHash[:],
|
|
},
|
|
)
|
|
if err != nil {
|
|
return f.handleErrorAndUnlockReservations(err)
|
|
}
|
|
if res.Accepted {
|
|
return OnPaymentAccepted
|
|
}
|
|
|
|
time.Sleep(time.Second)
|
|
}
|
|
}
|
|
}
|
|
|
|
// BuildHTLCAction creates the htlc transaction, exchanges nonces with
|
|
// the server and sends the htlc signatures to the server.
|
|
func (f *FSM) BuildHTLCAction(eventCtx fsm.EventContext) fsm.EventType {
|
|
htlcSessions, htlcClientNonces, err := f.InstantOut.createMusig2Session(
|
|
f.ctx, f.cfg.Signer,
|
|
)
|
|
if err != nil {
|
|
return f.handleErrorAndUnlockReservations(err)
|
|
}
|
|
|
|
f.htlcMusig2Sessions = htlcSessions
|
|
|
|
// Send the server the client nonces.
|
|
htlcInitRes, err := f.cfg.InstantOutClient.InitHtlcSig(
|
|
f.ctx,
|
|
&loop_rpc.InitHtlcSigRequest{
|
|
SwapHash: f.InstantOut.SwapHash[:],
|
|
HtlcClientNonces: htlcClientNonces,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return f.handleErrorAndUnlockReservations(err)
|
|
}
|
|
|
|
if len(htlcInitRes.HtlcServerNonces) != len(f.InstantOut.Reservations) {
|
|
return f.handleErrorAndUnlockReservations(
|
|
errors.New("invalid number of server nonces"),
|
|
)
|
|
}
|
|
|
|
htlcServerNonces, err := toNonces(htlcInitRes.HtlcServerNonces)
|
|
if err != nil {
|
|
return f.handleErrorAndUnlockReservations(err)
|
|
}
|
|
|
|
// Now that our nonces are set, we can create and sign the htlc
|
|
// transaction.
|
|
htlcTx, err := f.InstantOut.createHtlcTransaction(f.cfg.Network)
|
|
if err != nil {
|
|
return f.handleErrorAndUnlockReservations(err)
|
|
}
|
|
|
|
// Next we'll get our sweep tx signatures.
|
|
htlcSigs, err := f.InstantOut.signMusig2Tx(
|
|
f.ctx, f.cfg.Signer, htlcTx, f.htlcMusig2Sessions,
|
|
htlcServerNonces,
|
|
)
|
|
if err != nil {
|
|
return f.handleErrorAndUnlockReservations(err)
|
|
}
|
|
|
|
// Send the server the htlc signatures.
|
|
htlcRes, err := f.cfg.InstantOutClient.PushHtlcSig(
|
|
f.ctx,
|
|
&loop_rpc.PushHtlcSigRequest{
|
|
SwapHash: f.InstantOut.SwapHash[:],
|
|
ClientSigs: htlcSigs,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return f.handleErrorAndUnlockReservations(err)
|
|
}
|
|
|
|
// We can now finalize the htlc transaction.
|
|
htlcTx, err = f.InstantOut.finalizeMusig2Transaction(
|
|
f.ctx, f.cfg.Signer, f.htlcMusig2Sessions, htlcTx,
|
|
htlcRes.ServerSigs,
|
|
)
|
|
if err != nil {
|
|
return f.handleErrorAndUnlockReservations(err)
|
|
}
|
|
|
|
f.InstantOut.FinalizedHtlcTx = htlcTx
|
|
|
|
return OnHtlcSigReceived
|
|
}
|
|
|
|
// PushPreimageAction pushes the preimage to the server. It also creates the
|
|
// sweepless sweep transaction and sends the signatures to the server. Finally,
|
|
// it publishes the sweepless sweep transaction. If any of the steps after
|
|
// pushing the preimage fail, the htlc timeout transaction will be published.
|
|
func (f *FSM) PushPreimageAction(eventCtx fsm.EventContext) fsm.EventType {
|
|
// First we'll create the musig2 context.
|
|
coopSessions, coopClientNonces, err := f.InstantOut.createMusig2Session(
|
|
f.ctx, f.cfg.Signer,
|
|
)
|
|
if err != nil {
|
|
return f.handleErrorAndUnlockReservations(err)
|
|
}
|
|
|
|
f.sweeplessSweepSessions = coopSessions
|
|
|
|
// Get the feerate for the coop sweep.
|
|
feeRate, err := f.cfg.Wallet.EstimateFeeRate(f.ctx, NormalConfTarget)
|
|
if err != nil {
|
|
return f.handleErrorAndUnlockReservations(err)
|
|
}
|
|
|
|
pushPreImageRes, err := f.cfg.InstantOutClient.PushPreimage(
|
|
f.ctx,
|
|
&loop_rpc.PushPreimageRequest{
|
|
Preimage: f.InstantOut.SwapPreimage[:],
|
|
ClientNonces: coopClientNonces,
|
|
ClientSweepAddr: f.InstantOut.SweepAddress.String(),
|
|
MusigTxFeeRate: uint64(feeRate),
|
|
},
|
|
)
|
|
// Now that we have revealed the preimage, if any following step fail,
|
|
// we'll need to publish the htlc timeout tx.
|
|
if err != nil {
|
|
f.LastActionError = err
|
|
return OnErrorPublishHtlc
|
|
}
|
|
|
|
// Now that we have the sweepless sweep signatures we can build and
|
|
// publish the sweepless sweep transaction.
|
|
sweepTx, err := f.InstantOut.createSweeplessSweepTx(feeRate)
|
|
if err != nil {
|
|
f.LastActionError = err
|
|
return OnErrorPublishHtlc
|
|
}
|
|
|
|
coopServerNonces, err := toNonces(pushPreImageRes.ServerNonces)
|
|
if err != nil {
|
|
f.LastActionError = err
|
|
return OnErrorPublishHtlc
|
|
}
|
|
|
|
// Next we'll get our sweep tx signatures.
|
|
_, err = f.InstantOut.signMusig2Tx(
|
|
f.ctx, f.cfg.Signer, sweepTx, f.sweeplessSweepSessions,
|
|
coopServerNonces,
|
|
)
|
|
if err != nil {
|
|
f.LastActionError = err
|
|
return OnErrorPublishHtlc
|
|
}
|
|
|
|
// Now we'll finalize the sweepless sweep transaction.
|
|
sweepTx, err = f.InstantOut.finalizeMusig2Transaction(
|
|
f.ctx, f.cfg.Signer, f.sweeplessSweepSessions, sweepTx,
|
|
pushPreImageRes.Musig2SweepSigs,
|
|
)
|
|
if err != nil {
|
|
f.LastActionError = err
|
|
return OnErrorPublishHtlc
|
|
}
|
|
|
|
txLabel := fmt.Sprintf("sweepless-sweep-%v",
|
|
f.InstantOut.SwapPreimage.Hash())
|
|
|
|
// Publish the sweepless sweep transaction.
|
|
err = f.cfg.Wallet.PublishTransaction(f.ctx, sweepTx, txLabel)
|
|
if err != nil {
|
|
f.LastActionError = err
|
|
return OnErrorPublishHtlc
|
|
}
|
|
|
|
f.InstantOut.FinalizedSweeplessSweepTx = sweepTx
|
|
txHash := f.InstantOut.FinalizedSweeplessSweepTx.TxHash()
|
|
|
|
f.InstantOut.SweepTxHash = &txHash
|
|
|
|
return OnSweeplessSweepPublished
|
|
}
|
|
|
|
// WaitForSweeplessSweepConfirmedAction waits for the sweepless sweep
|
|
// transaction to be confirmed.
|
|
func (f *FSM) WaitForSweeplessSweepConfirmedAction(
|
|
eventCtx fsm.EventContext) fsm.EventType {
|
|
|
|
pkscript, err := txscript.PayToAddrScript(f.InstantOut.SweepAddress)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
confChan, confErrChan, err := f.cfg.ChainNotifier.
|
|
RegisterConfirmationsNtfn(
|
|
f.ctx, f.InstantOut.SweepTxHash, pkscript,
|
|
1, f.InstantOut.InitiationHeight,
|
|
)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case spendErr := <-confErrChan:
|
|
f.LastActionError = spendErr
|
|
f.Errorf("error listening for sweepless sweep "+
|
|
"confirmation: %v", spendErr)
|
|
|
|
return OnErrorPublishHtlc
|
|
|
|
case conf := <-confChan:
|
|
f.InstantOut.
|
|
SweepConfirmationHeight = conf.BlockHeight
|
|
|
|
return OnSweeplessSweepConfirmed
|
|
}
|
|
}
|
|
}
|
|
|
|
// PublishHtlcAction publishes the htlc transaction and the htlc sweep
|
|
// transaction.
|
|
func (f *FSM) PublishHtlcAction(eventCtx fsm.EventContext) fsm.EventType {
|
|
// Publish the htlc transaction.
|
|
err := f.cfg.Wallet.PublishTransaction(
|
|
f.ctx, f.InstantOut.FinalizedHtlcTx,
|
|
fmt.Sprintf("htlc-%v", f.InstantOut.SwapPreimage.Hash()),
|
|
)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
// Create a feerate that will confirm the htlc quickly.
|
|
feeRate, err := f.cfg.Wallet.EstimateFeeRate(f.ctx, UrgentConfTarget)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
// We can immediately publish the htlc sweep transaction
|
|
htlcSweepTx, err := f.InstantOut.generateHtlcSweepTx(
|
|
f.ctx, f.cfg.Signer, feeRate, f.cfg.Network,
|
|
)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
txLabel := fmt.Sprintf("htlc-sweep-%v",
|
|
f.InstantOut.SwapPreimage.Hash())
|
|
|
|
err = f.cfg.Wallet.PublishTransaction(f.ctx, htlcSweepTx, txLabel)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
sweepTxHash := htlcSweepTx.TxHash()
|
|
|
|
f.InstantOut.SweepTxHash = &sweepTxHash
|
|
|
|
return OnHtlcSweepPublished
|
|
}
|
|
|
|
// WaitForHtlcSweepConfirmedAction waits for the htlc sweep transaction to be
|
|
// confirmed.
|
|
func (f *FSM) WaitForHtlcSweepConfirmedAction(
|
|
eventCtx fsm.EventContext) fsm.EventType {
|
|
|
|
htlc, err := f.InstantOut.getHtlc(f.cfg.Network)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
confChan, confErrChan, err := f.cfg.ChainNotifier.RegisterConfirmationsNtfn(
|
|
f.ctx, f.InstantOut.SweepTxHash, htlc.PkScript,
|
|
1, f.InstantOut.InitiationHeight,
|
|
)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case spendErr := <-confErrChan:
|
|
f.LastActionError = spendErr
|
|
return OnErrorPublishHtlc
|
|
|
|
case conf := <-confChan:
|
|
f.InstantOut.
|
|
SweepConfirmationHeight = conf.BlockHeight
|
|
|
|
return OnHtlcSwept
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleErrorAndUnlockReservations handles an error and unlocks the
|
|
// reservations.
|
|
func (f *FSM) handleErrorAndUnlockReservations(err error) fsm.EventType {
|
|
// Unlock the reservations.
|
|
for _, reservation := range f.InstantOut.Reservations {
|
|
err := f.cfg.ReservationManager.UnlockReservation(
|
|
f.ctx, reservation.ID,
|
|
)
|
|
if err != nil {
|
|
return f.HandleError(err)
|
|
}
|
|
}
|
|
|
|
return f.HandleError(err)
|
|
}
|