mirror of https://github.com/lightninglabs/loop
instantout: add fsm and actions
parent
89b5c00cfa
commit
56ed6f7ccb
@ -0,0 +1,615 @@
|
|||||||
|
package instantout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"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"
|
||||||
|
"github.com/lightninglabs/loop/swap"
|
||||||
|
loop_rpc "github.com/lightninglabs/loop/swapserverrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Define route independent max routing fees. We have currently no way
|
||||||
|
// to get a reliable estimate of the routing fees. Best we can do is
|
||||||
|
// the minimum routing fees, which is not very indicative.
|
||||||
|
maxRoutingFeeBase = btcutil.Amount(10)
|
||||||
|
|
||||||
|
maxRoutingFeeRate = int64(20000)
|
||||||
|
|
||||||
|
// urgentConfTarget is the target number of blocks for the htlc to be
|
||||||
|
// confirmed quickly.
|
||||||
|
urgentConfTarget = int32(3)
|
||||||
|
|
||||||
|
// normalConfTarget is the target number of blocks for the sweepless
|
||||||
|
// sweep to be confirmed.
|
||||||
|
normalConfTarget = int32(6)
|
||||||
|
|
||||||
|
// defaultMaxParts is the default maximum number of parts for the swap.
|
||||||
|
defaultMaxParts = uint32(5)
|
||||||
|
|
||||||
|
// defaultSendpaymentTimeout is the default timeout for the swap invoice.
|
||||||
|
defaultSendpaymentTimeout = time.Minute * 5
|
||||||
|
|
||||||
|
// defaultPollPaymentTime is the default time to poll the server for the
|
||||||
|
// payment status.
|
||||||
|
defaultPollPaymentTime = time.Second * 15
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitInstantOutCtx contains the context for the InitInstantOutAction.
|
||||||
|
type InitInstantOutCtx struct {
|
||||||
|
cltvExpiry int32
|
||||||
|
reservations []reservation.ID
|
||||||
|
initationHeight int32
|
||||||
|
outgoingChanSet loopdb.ChannelSet
|
||||||
|
protocolVersion ProtocolVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
resId := reservationId
|
||||||
|
res, err := f.cfg.ReservationManager.GetReservation(
|
||||||
|
f.ctx, resId,
|
||||||
|
)
|
||||||
|
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, resId[:])
|
||||||
|
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{
|
||||||
|
ReceiverKey: keyRes.PubKey.SerializeCompressed(),
|
||||||
|
SwapHash: swapHash[:],
|
||||||
|
Expiry: initCtx.cltvExpiry,
|
||||||
|
HtlcFeeRate: uint64(feeRate),
|
||||||
|
ReservationIds: reservationIds,
|
||||||
|
ProtocolVersion: CurrentRpcProtocolVersion(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
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 swapHash != 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(_ 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: defaultSendpaymentTimeout,
|
||||||
|
MaxParts: defaultMaxParts,
|
||||||
|
MaxFee: getMaxRoutingFee(f.InstantOut.value),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
f.Errorf("error sending payment: %v", err)
|
||||||
|
return f.handleErrorAndUnlockReservations(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll continuously poll the server for the payment status.
|
||||||
|
pollPaymentTries := 0
|
||||||
|
|
||||||
|
// We want to poll quickly the first time.
|
||||||
|
timer := time.NewTimer(time.Second)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case payRes := <-payChan:
|
||||||
|
f.Debugf("payment result: %v", payRes)
|
||||||
|
if payRes.State == lnrpc.Payment_FAILED {
|
||||||
|
return f.handleErrorAndUnlockReservations(
|
||||||
|
fmt.Errorf("payment failed: %v",
|
||||||
|
payRes.FailureReason),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case err := <-paymentErrChan:
|
||||||
|
f.Errorf("error sending payment: %v", err)
|
||||||
|
return f.handleErrorAndUnlockReservations(err)
|
||||||
|
|
||||||
|
case <-f.ctx.Done():
|
||||||
|
return f.handleErrorAndUnlockReservations(nil)
|
||||||
|
|
||||||
|
case <-timer.C:
|
||||||
|
res, err := f.cfg.InstantOutClient.PollPaymentAccepted(
|
||||||
|
f.ctx, &loop_rpc.PollPaymentAcceptedRequest{
|
||||||
|
SwapHash: f.InstantOut.SwapHash[:],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
pollPaymentTries++
|
||||||
|
if pollPaymentTries > 20 {
|
||||||
|
return f.handleErrorAndUnlockReservations(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if res != nil && res.Accepted {
|
||||||
|
return OnPaymentAccepted
|
||||||
|
}
|
||||||
|
timer.Reset(defaultPollPaymentTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
txHash := f.InstantOut.finalizedHtlcTx.TxHash()
|
||||||
|
f.Debugf("published htlc tx: %v", txHash)
|
||||||
|
|
||||||
|
// We'll now wait for the htlc to be confirmed.
|
||||||
|
confChan, confErrChan, err := f.cfg.ChainNotifier.
|
||||||
|
RegisterConfirmationsNtfn(
|
||||||
|
f.ctx, &txHash,
|
||||||
|
f.InstantOut.finalizedHtlcTx.TxOut[0].PkScript,
|
||||||
|
1, f.InstantOut.initiationHeight,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return f.HandleError(err)
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case spendErr := <-confErrChan:
|
||||||
|
return f.HandleError(spendErr)
|
||||||
|
|
||||||
|
case <-confChan:
|
||||||
|
return OnHtlcPublished
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublishHtlcSweepAction publishes the htlc sweep transaction.
|
||||||
|
func (f *FSM) PublishHtlcSweepAction(eventCtx fsm.EventContext) fsm.EventType {
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
getInfo, err := f.cfg.LndClient.GetInfo(f.ctx)
|
||||||
|
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, getInfo.BlockHeight,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return f.HandleError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
label := fmt.Sprintf("htlc-sweep-%v", f.InstantOut.swapPreimage.Hash())
|
||||||
|
|
||||||
|
err = f.cfg.Wallet.PublishTransaction(f.ctx, htlcSweepTx, label)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error publishing htlc sweep tx: %v", err)
|
||||||
|
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 {
|
||||||
|
|
||||||
|
sweepPkScript, 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, sweepPkScript,
|
||||||
|
1, f.InstantOut.initiationHeight,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return f.HandleError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Debugf("waiting for htlc sweep tx %v to be confirmed",
|
||||||
|
f.InstantOut.SweepTxHash)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case spendErr := <-confErrChan:
|
||||||
|
return f.HandleError(spendErr)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
// We might get here from a canceled context, we create a new context
|
||||||
|
// with a timeout to unlock the reservations.
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Unlock the reservations.
|
||||||
|
for _, reservation := range f.InstantOut.reservations {
|
||||||
|
err := f.cfg.ReservationManager.UnlockReservation(
|
||||||
|
ctx, reservation.ID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
f.Errorf("error unlocking reservation: %v", err)
|
||||||
|
return f.HandleError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're also sending the server a cancel message so that it can
|
||||||
|
// release the reservations. This can be done in a goroutine as we
|
||||||
|
// wan't to fail the fsm early.
|
||||||
|
go func() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||||
|
defer cancel()
|
||||||
|
_, cancelErr := f.cfg.InstantOutClient.CancelInstantSwap(
|
||||||
|
ctx, &loop_rpc.CancelInstantSwapRequest{
|
||||||
|
SwapHash: f.InstantOut.SwapHash[:],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if cancelErr != nil {
|
||||||
|
// We'll log the error but not return it as we want to return the
|
||||||
|
// original error.
|
||||||
|
f.Debugf("error sending cancel message: %v", cancelErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return f.HandleError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMaxRoutingFee(amt btcutil.Amount) btcutil.Amount {
|
||||||
|
return swap.CalcFee(amt, maxRoutingFeeBase, maxRoutingFeeRate)
|
||||||
|
}
|
@ -0,0 +1,401 @@
|
|||||||
|
package instantout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
|
"github.com/lightninglabs/lndclient"
|
||||||
|
"github.com/lightninglabs/loop/fsm"
|
||||||
|
loop_rpc "github.com/lightninglabs/loop/swapserverrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/input"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProtocolVersion uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ProtocolVersionUndefined is the undefined protocol version.
|
||||||
|
ProtocolVersionUndefined ProtocolVersion = 0
|
||||||
|
|
||||||
|
// ProtocolVersionFullReservation is the protocol version that uses
|
||||||
|
// the full reservation amount without change.
|
||||||
|
ProtocolVersionFullReservation ProtocolVersion = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// CurrentProtocolVersion returns the current protocol version.
|
||||||
|
func CurrentProtocolVersion() ProtocolVersion {
|
||||||
|
return ProtocolVersionFullReservation
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrentRpcProtocolVersion returns the current rpc protocol version.
|
||||||
|
func CurrentRpcProtocolVersion() loop_rpc.InstantOutProtocolVersion {
|
||||||
|
return loop_rpc.InstantOutProtocolVersion(CurrentProtocolVersion())
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// defaultObserverSize is the size of the fsm observer channel.
|
||||||
|
defaultObserverSize = 15
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrProtocolVersionNotSupported = errors.New(
|
||||||
|
"protocol version not supported",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// States.
|
||||||
|
var (
|
||||||
|
// Init is the initial state of the instant out FSM.
|
||||||
|
Init = fsm.StateType("Init")
|
||||||
|
|
||||||
|
// SendPaymentAndPollAccepted is the state where the payment is sent
|
||||||
|
// and the server is polled for the accepted state.
|
||||||
|
SendPaymentAndPollAccepted = fsm.StateType("SendPaymentAndPollAccepted")
|
||||||
|
|
||||||
|
// BuildHtlc is the state where the htlc transaction is built.
|
||||||
|
BuildHtlc = fsm.StateType("BuildHtlc")
|
||||||
|
|
||||||
|
// PushPreimage is the state where the preimage is pushed to the server.
|
||||||
|
PushPreimage = fsm.StateType("PushPreimage")
|
||||||
|
|
||||||
|
// WaitForSweeplessSweepConfirmed is the state where we wait for the
|
||||||
|
// sweepless sweep to be confirmed.
|
||||||
|
WaitForSweeplessSweepConfirmed = fsm.StateType(
|
||||||
|
"WaitForSweeplessSweepConfirmed")
|
||||||
|
|
||||||
|
// FinishedSweeplessSweep is the state where the swap is finished by
|
||||||
|
// publishing the sweepless sweep.
|
||||||
|
FinishedSweeplessSweep = fsm.StateType("FinishedSweeplessSweep")
|
||||||
|
|
||||||
|
// PublishHtlc is the state where the htlc transaction is published.
|
||||||
|
PublishHtlc = fsm.StateType("PublishHtlc")
|
||||||
|
|
||||||
|
// PublishHtlcSweep is the state where the htlc sweep transaction is
|
||||||
|
// published.
|
||||||
|
PublishHtlcSweep = fsm.StateType("PublishHtlcSweep")
|
||||||
|
|
||||||
|
// FinishedHtlcPreimageSweep is the state where the swap is finished by
|
||||||
|
// publishing the htlc preimage sweep.
|
||||||
|
FinishedHtlcPreimageSweep = fsm.StateType("FinishedHtlcPreimageSweep")
|
||||||
|
|
||||||
|
// WaitForHtlcSweepConfirmed is the state where we wait for the htlc
|
||||||
|
// sweep to be confirmed.
|
||||||
|
WaitForHtlcSweepConfirmed = fsm.StateType("WaitForHtlcSweepConfirmed")
|
||||||
|
|
||||||
|
// FailedHtlcSweep is the state where the htlc sweep failed.
|
||||||
|
FailedHtlcSweep = fsm.StateType("FailedHtlcSweep")
|
||||||
|
|
||||||
|
// Failed is the state where the swap failed.
|
||||||
|
Failed = fsm.StateType("InstantOutFailed")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Events.
|
||||||
|
var (
|
||||||
|
// OnStart is the event that is sent when the FSM is started.
|
||||||
|
OnStart = fsm.EventType("OnStart")
|
||||||
|
|
||||||
|
// OnInit is the event that is triggered when the FSM is initialized.
|
||||||
|
OnInit = fsm.EventType("OnInit")
|
||||||
|
|
||||||
|
// OnPaymentAccepted is the event that is triggered when the payment
|
||||||
|
// is accepted by the server.
|
||||||
|
OnPaymentAccepted = fsm.EventType("OnPaymentAccepted")
|
||||||
|
|
||||||
|
// OnHtlcSigReceived is the event that is triggered when the htlc sig
|
||||||
|
// is received.
|
||||||
|
OnHtlcSigReceived = fsm.EventType("OnHtlcSigReceived")
|
||||||
|
|
||||||
|
// OnPreimagePushed is the event that is triggered when the preimage
|
||||||
|
// is pushed to the server.
|
||||||
|
OnPreimagePushed = fsm.EventType("OnPreimagePushed")
|
||||||
|
|
||||||
|
// OnSweeplessSweepPublished is the event that is triggered when the
|
||||||
|
// sweepless sweep is published.
|
||||||
|
OnSweeplessSweepPublished = fsm.EventType("OnSweeplessSweepPublished")
|
||||||
|
|
||||||
|
// OnSweeplessSweepConfirmed is the event that is triggered when the
|
||||||
|
// sweepless sweep is confirmed.
|
||||||
|
OnSweeplessSweepConfirmed = fsm.EventType("OnSweeplessSweepConfirmed")
|
||||||
|
|
||||||
|
// OnErrorPublishHtlc is the event that is triggered when the htlc
|
||||||
|
// sweep is published after an error.
|
||||||
|
OnErrorPublishHtlc = fsm.EventType("OnErrorPublishHtlc")
|
||||||
|
|
||||||
|
// OnInvalidCoopSweep is the event that is triggered when the coop
|
||||||
|
// sweep is invalid.
|
||||||
|
OnInvalidCoopSweep = fsm.EventType("OnInvalidCoopSweep")
|
||||||
|
|
||||||
|
// OnHtlcPublished is the event that is triggered when the htlc
|
||||||
|
// transaction is published.
|
||||||
|
OnHtlcPublished = fsm.EventType("OnHtlcPublished")
|
||||||
|
|
||||||
|
// OnHtlcSweepPublished is the event that is triggered when the htlc
|
||||||
|
// sweep is published.
|
||||||
|
OnHtlcSweepPublished = fsm.EventType("OnHtlcSweepPublished")
|
||||||
|
|
||||||
|
// OnHtlcSwept is the event that is triggered when the htlc sweep is
|
||||||
|
// confirmed.
|
||||||
|
OnHtlcSwept = fsm.EventType("OnHtlcSwept")
|
||||||
|
|
||||||
|
// OnRecover is the event that is triggered when the FSM recovers from
|
||||||
|
// a restart.
|
||||||
|
OnRecover = fsm.EventType("OnRecover")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config contains the services required for the instant out FSM.
|
||||||
|
type Config struct {
|
||||||
|
// Store is used to store the instant out.
|
||||||
|
Store InstantLoopOutStore
|
||||||
|
|
||||||
|
// LndClient is used to decode the swap invoice.
|
||||||
|
LndClient lndclient.LightningClient
|
||||||
|
|
||||||
|
// RouterClient is used to send the offchain payment to the server.
|
||||||
|
RouterClient lndclient.RouterClient
|
||||||
|
|
||||||
|
// ChainNotifier is used to be notified of on-chain events.
|
||||||
|
ChainNotifier lndclient.ChainNotifierClient
|
||||||
|
|
||||||
|
// Signer is used to sign transactions.
|
||||||
|
Signer lndclient.SignerClient
|
||||||
|
|
||||||
|
// Wallet is used to derive keys.
|
||||||
|
Wallet lndclient.WalletKitClient
|
||||||
|
|
||||||
|
// InstantOutClient is used to communicate with the swap server.
|
||||||
|
InstantOutClient loop_rpc.InstantSwapServerClient
|
||||||
|
|
||||||
|
// ReservationManager is used to get the reservations and lock them.
|
||||||
|
ReservationManager ReservationManager
|
||||||
|
|
||||||
|
// Network is the network that is used for the swap.
|
||||||
|
Network *chaincfg.Params
|
||||||
|
}
|
||||||
|
|
||||||
|
// FSM is the state machine that handles the instant out.
|
||||||
|
type FSM struct {
|
||||||
|
*fsm.StateMachine
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
|
|
||||||
|
// cfg contains all the services that the reservation manager needs to
|
||||||
|
// operate.
|
||||||
|
cfg *Config
|
||||||
|
|
||||||
|
// InstantOut contains all the information about the instant out.
|
||||||
|
InstantOut *InstantOut
|
||||||
|
|
||||||
|
// htlcMusig2Sessions contains all the reservations input musig2
|
||||||
|
// sessions that will be used for the htlc transaction.
|
||||||
|
htlcMusig2Sessions []*input.MuSig2SessionInfo
|
||||||
|
|
||||||
|
// sweeplessSweepSessions contains all the reservations input musig2
|
||||||
|
// sessions that will be used for the sweepless sweep transaction.
|
||||||
|
sweeplessSweepSessions []*input.MuSig2SessionInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFSM creates a new instant out FSM.
|
||||||
|
func NewFSM(ctx context.Context, cfg *Config,
|
||||||
|
protocolVersion ProtocolVersion) (*FSM, error) {
|
||||||
|
|
||||||
|
instantOut := &InstantOut{
|
||||||
|
State: fsm.EmptyState,
|
||||||
|
protocolVersion: protocolVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewFSMFromInstantOut(ctx, cfg, instantOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFSMFromInstantOut creates a new instantout FSM from an existing instantout
|
||||||
|
// recovered from the database.
|
||||||
|
func NewFSMFromInstantOut(ctx context.Context, cfg *Config,
|
||||||
|
instantOut *InstantOut) (*FSM, error) {
|
||||||
|
|
||||||
|
instantOutFSM := &FSM{
|
||||||
|
ctx: ctx,
|
||||||
|
cfg: cfg,
|
||||||
|
InstantOut: instantOut,
|
||||||
|
}
|
||||||
|
switch instantOut.protocolVersion {
|
||||||
|
case ProtocolVersionFullReservation:
|
||||||
|
instantOutFSM.StateMachine = fsm.NewStateMachineWithState(
|
||||||
|
instantOutFSM.GetV1ReservationStates(),
|
||||||
|
instantOut.State, defaultObserverSize,
|
||||||
|
)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, ErrProtocolVersionNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
instantOutFSM.ActionEntryFunc = instantOutFSM.updateInstantOut
|
||||||
|
|
||||||
|
return instantOutFSM, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetV1ReservationStates returns the states for the v1 reservation.
|
||||||
|
func (f *FSM) GetV1ReservationStates() fsm.States {
|
||||||
|
return fsm.States{
|
||||||
|
fsm.EmptyState: fsm.State{
|
||||||
|
Transitions: fsm.Transitions{
|
||||||
|
OnStart: Init,
|
||||||
|
},
|
||||||
|
Action: nil,
|
||||||
|
},
|
||||||
|
Init: fsm.State{
|
||||||
|
Transitions: fsm.Transitions{
|
||||||
|
OnInit: SendPaymentAndPollAccepted,
|
||||||
|
fsm.OnError: Failed,
|
||||||
|
OnRecover: Failed,
|
||||||
|
},
|
||||||
|
Action: f.InitInstantOutAction,
|
||||||
|
},
|
||||||
|
SendPaymentAndPollAccepted: fsm.State{
|
||||||
|
Transitions: fsm.Transitions{
|
||||||
|
OnPaymentAccepted: BuildHtlc,
|
||||||
|
fsm.OnError: Failed,
|
||||||
|
OnRecover: Failed,
|
||||||
|
},
|
||||||
|
Action: f.PollPaymentAcceptedAction,
|
||||||
|
},
|
||||||
|
BuildHtlc: fsm.State{
|
||||||
|
Transitions: fsm.Transitions{
|
||||||
|
OnHtlcSigReceived: PushPreimage,
|
||||||
|
fsm.OnError: Failed,
|
||||||
|
OnRecover: Failed,
|
||||||
|
},
|
||||||
|
Action: f.BuildHTLCAction,
|
||||||
|
},
|
||||||
|
PushPreimage: fsm.State{
|
||||||
|
Transitions: fsm.Transitions{
|
||||||
|
OnSweeplessSweepPublished: WaitForSweeplessSweepConfirmed,
|
||||||
|
fsm.OnError: Failed,
|
||||||
|
OnErrorPublishHtlc: PublishHtlc,
|
||||||
|
OnRecover: PushPreimage,
|
||||||
|
},
|
||||||
|
Action: f.PushPreimageAction,
|
||||||
|
},
|
||||||
|
WaitForSweeplessSweepConfirmed: fsm.State{
|
||||||
|
Transitions: fsm.Transitions{
|
||||||
|
OnSweeplessSweepConfirmed: FinishedSweeplessSweep,
|
||||||
|
OnRecover: WaitForSweeplessSweepConfirmed,
|
||||||
|
fsm.OnError: PublishHtlc,
|
||||||
|
},
|
||||||
|
Action: f.WaitForSweeplessSweepConfirmedAction,
|
||||||
|
},
|
||||||
|
FinishedSweeplessSweep: fsm.State{
|
||||||
|
Transitions: fsm.Transitions{},
|
||||||
|
Action: fsm.NoOpAction,
|
||||||
|
},
|
||||||
|
PublishHtlc: fsm.State{
|
||||||
|
Transitions: fsm.Transitions{
|
||||||
|
fsm.OnError: FailedHtlcSweep,
|
||||||
|
OnRecover: PublishHtlc,
|
||||||
|
OnHtlcPublished: PublishHtlcSweep,
|
||||||
|
},
|
||||||
|
Action: f.PublishHtlcAction,
|
||||||
|
},
|
||||||
|
PublishHtlcSweep: fsm.State{
|
||||||
|
Transitions: fsm.Transitions{
|
||||||
|
OnHtlcSweepPublished: WaitForHtlcSweepConfirmed,
|
||||||
|
OnRecover: PublishHtlcSweep,
|
||||||
|
fsm.OnError: FailedHtlcSweep,
|
||||||
|
},
|
||||||
|
Action: f.PublishHtlcSweepAction,
|
||||||
|
},
|
||||||
|
WaitForHtlcSweepConfirmed: fsm.State{
|
||||||
|
Transitions: fsm.Transitions{
|
||||||
|
OnHtlcSwept: FinishedHtlcPreimageSweep,
|
||||||
|
OnRecover: WaitForHtlcSweepConfirmed,
|
||||||
|
fsm.OnError: FailedHtlcSweep,
|
||||||
|
},
|
||||||
|
Action: f.WaitForHtlcSweepConfirmedAction,
|
||||||
|
},
|
||||||
|
FinishedHtlcPreimageSweep: fsm.State{
|
||||||
|
Transitions: fsm.Transitions{},
|
||||||
|
Action: fsm.NoOpAction,
|
||||||
|
},
|
||||||
|
FailedHtlcSweep: fsm.State{
|
||||||
|
Action: fsm.NoOpAction,
|
||||||
|
Transitions: fsm.Transitions{
|
||||||
|
OnRecover: PublishHtlcSweep,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Failed: fsm.State{
|
||||||
|
Action: fsm.NoOpAction,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateInstantOut is called after every action and updates the reservation
|
||||||
|
// in the db.
|
||||||
|
func (f *FSM) updateInstantOut(notification fsm.Notification) {
|
||||||
|
f.Infof("Previous: %v, Event: %v, Next: %v", notification.PreviousState,
|
||||||
|
notification.Event, notification.NextState)
|
||||||
|
|
||||||
|
// Skip the update if the reservation is not yet initialized.
|
||||||
|
if f.InstantOut == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f.InstantOut.State = notification.NextState
|
||||||
|
|
||||||
|
// If we're in the early stages we don't have created the reservation
|
||||||
|
// in the store yet and won't need to update it.
|
||||||
|
if f.InstantOut.State == Init ||
|
||||||
|
f.InstantOut.State == fsm.EmptyState ||
|
||||||
|
(notification.PreviousState == Init &&
|
||||||
|
f.InstantOut.State == Failed) {
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := f.cfg.Store.UpdateInstantLoopOut(f.ctx, f.InstantOut)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error updating instant out: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infof logs an info message with the reservation hash as prefix.
|
||||||
|
func (f *FSM) Infof(format string, args ...interface{}) {
|
||||||
|
log.Infof(
|
||||||
|
"InstantOut %v: "+format,
|
||||||
|
append(
|
||||||
|
[]interface{}{f.InstantOut.swapPreimage.Hash()},
|
||||||
|
args...,
|
||||||
|
)...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debugf logs a debug message with the reservation hash as prefix.
|
||||||
|
func (f *FSM) Debugf(format string, args ...interface{}) {
|
||||||
|
log.Debugf(
|
||||||
|
"InstantOut %v: "+format,
|
||||||
|
append(
|
||||||
|
[]interface{}{f.InstantOut.swapPreimage.Hash()},
|
||||||
|
args...,
|
||||||
|
)...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errorf logs an error message with the reservation hash as prefix.
|
||||||
|
func (f *FSM) Errorf(format string, args ...interface{}) {
|
||||||
|
log.Errorf(
|
||||||
|
"InstantOut %v: "+format,
|
||||||
|
append(
|
||||||
|
[]interface{}{f.InstantOut.swapPreimage.Hash()},
|
||||||
|
args...,
|
||||||
|
)...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isFinalState returns true if the state is a final state.
|
||||||
|
func isFinalState(state fsm.StateType) bool {
|
||||||
|
switch state {
|
||||||
|
case Failed, FinishedHtlcPreimageSweep,
|
||||||
|
FinishedSweeplessSweep:
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
@ -0,0 +1,488 @@
|
|||||||
|
package instantout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||||
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/lightninglabs/lndclient"
|
||||||
|
"github.com/lightninglabs/loop/fsm"
|
||||||
|
"github.com/lightninglabs/loop/instantout/reservation"
|
||||||
|
"github.com/lightninglabs/loop/loopdb"
|
||||||
|
"github.com/lightninglabs/loop/swap"
|
||||||
|
"github.com/lightningnetwork/lnd/input"
|
||||||
|
"github.com/lightningnetwork/lnd/keychain"
|
||||||
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InstantOut holds the necessary information to execute an instant out swap.
|
||||||
|
type InstantOut struct {
|
||||||
|
// SwapHash is the hash of the swap.
|
||||||
|
SwapHash lntypes.Hash
|
||||||
|
|
||||||
|
// swapPreimage is the preimage that is used for the swap.
|
||||||
|
swapPreimage lntypes.Preimage
|
||||||
|
|
||||||
|
// State is the current state of the swap.
|
||||||
|
State fsm.StateType
|
||||||
|
|
||||||
|
// cltvExpiry is the expiry of the swap.
|
||||||
|
cltvExpiry int32
|
||||||
|
|
||||||
|
// outgoingChanSet optionally specifies the short channel ids of the
|
||||||
|
// channels that may be used to loop out.
|
||||||
|
outgoingChanSet loopdb.ChannelSet
|
||||||
|
|
||||||
|
// reservations are the reservations that are used in as inputs for the
|
||||||
|
// instant out swap.
|
||||||
|
reservations []*reservation.Reservation
|
||||||
|
|
||||||
|
// protocolVersion is the version of the protocol that is used for the
|
||||||
|
// swap.
|
||||||
|
protocolVersion ProtocolVersion
|
||||||
|
|
||||||
|
// initiationHeight is the height at which the swap was initiated.
|
||||||
|
initiationHeight int32
|
||||||
|
|
||||||
|
// value is the amount that is swapped.
|
||||||
|
value btcutil.Amount
|
||||||
|
|
||||||
|
// keyLocator is the key locator that is used for the swap.
|
||||||
|
keyLocator keychain.KeyLocator
|
||||||
|
|
||||||
|
// clientPubkey is the pubkey of the client that is used for the swap.
|
||||||
|
clientPubkey *btcec.PublicKey
|
||||||
|
|
||||||
|
// serverPubkey is the pubkey of the server that is used for the swap.
|
||||||
|
serverPubkey *btcec.PublicKey
|
||||||
|
|
||||||
|
// swapInvoice is the invoice that is used for the swap.
|
||||||
|
swapInvoice string
|
||||||
|
|
||||||
|
// htlcFeeRate is the fee rate that is used for the htlc transaction.
|
||||||
|
htlcFeeRate chainfee.SatPerKWeight
|
||||||
|
|
||||||
|
// sweepAddress is the address that is used to sweep the funds to.
|
||||||
|
sweepAddress btcutil.Address
|
||||||
|
|
||||||
|
// finalizedHtlcTx is the finalized htlc transaction that is used in the
|
||||||
|
// non-cooperative path for the instant out swap.
|
||||||
|
finalizedHtlcTx *wire.MsgTx
|
||||||
|
|
||||||
|
// SweepTxHash is the hash of the sweep transaction.
|
||||||
|
SweepTxHash *chainhash.Hash
|
||||||
|
|
||||||
|
// finalizedSweeplessSweepTx is the transaction that is used to sweep
|
||||||
|
// the funds in the cooperative path.
|
||||||
|
finalizedSweeplessSweepTx *wire.MsgTx
|
||||||
|
|
||||||
|
// sweepConfirmationHeight is the height at which the sweep
|
||||||
|
// transaction was confirmed.
|
||||||
|
sweepConfirmationHeight uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// getHtlc returns the swap.htlc for the instant out.
|
||||||
|
func (i *InstantOut) getHtlc(chainParams *chaincfg.Params) (*swap.Htlc, error) {
|
||||||
|
return swap.NewHtlcV2(
|
||||||
|
i.cltvExpiry, pubkeyTo33ByteSlice(i.serverPubkey),
|
||||||
|
pubkeyTo33ByteSlice(i.clientPubkey), i.SwapHash, chainParams,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// createMusig2Session creates a musig2 session for the instant out.
|
||||||
|
func (i *InstantOut) createMusig2Session(ctx context.Context,
|
||||||
|
signer lndclient.SignerClient) ([]*input.MuSig2SessionInfo,
|
||||||
|
[][]byte, error) {
|
||||||
|
|
||||||
|
// Create the htlc musig2 context.
|
||||||
|
musig2Sessions := make([]*input.MuSig2SessionInfo, len(i.reservations))
|
||||||
|
clientNonces := make([][]byte, len(i.reservations))
|
||||||
|
|
||||||
|
// Create the sessions and nonces from the reservations.
|
||||||
|
for idx, reservation := range i.reservations {
|
||||||
|
session, err := reservation.Musig2CreateSession(ctx, signer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
musig2Sessions[idx] = session
|
||||||
|
clientNonces[idx] = session.PublicNonce[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return musig2Sessions, clientNonces, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getInputReservation returns the input reservation for the instant out.
|
||||||
|
func (i *InstantOut) getInputReservations() (InputReservations, error) {
|
||||||
|
if len(i.reservations) == 0 {
|
||||||
|
return nil, errors.New("no reservations")
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs := make(InputReservations, len(i.reservations))
|
||||||
|
for idx, reservation := range i.reservations {
|
||||||
|
pkScript, err := reservation.GetPkScript()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs[idx] = InputReservation{
|
||||||
|
Outpoint: *reservation.Outpoint,
|
||||||
|
Value: reservation.Value,
|
||||||
|
PkScript: pkScript,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createHtlcTransaction creates the htlc transaction for the instant out.
|
||||||
|
func (i *InstantOut) createHtlcTransaction(network *chaincfg.Params) (
|
||||||
|
*wire.MsgTx, error) {
|
||||||
|
|
||||||
|
if network == nil {
|
||||||
|
return nil, errors.New("no network provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
inputReservations, err := i.getInputReservations()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// First Create the tx.
|
||||||
|
msgTx := wire.NewMsgTx(2)
|
||||||
|
|
||||||
|
// add the reservation inputs
|
||||||
|
for _, reservation := range inputReservations {
|
||||||
|
msgTx.AddTxIn(&wire.TxIn{
|
||||||
|
PreviousOutPoint: reservation.Outpoint,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Estimate the fee
|
||||||
|
weight := htlcWeight(len(inputReservations))
|
||||||
|
fee := i.htlcFeeRate.FeeForWeight(weight)
|
||||||
|
if fee > i.value/5 {
|
||||||
|
return nil, errors.New("fee is higher than 20% of " +
|
||||||
|
"sweep value")
|
||||||
|
}
|
||||||
|
|
||||||
|
htlc, err := i.getHtlc(network)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the sweep output
|
||||||
|
sweepOutput := &wire.TxOut{
|
||||||
|
Value: int64(i.value) - int64(fee),
|
||||||
|
PkScript: htlc.PkScript,
|
||||||
|
}
|
||||||
|
|
||||||
|
msgTx.AddTxOut(sweepOutput)
|
||||||
|
|
||||||
|
return msgTx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createSweeplessSweepTx creates the sweepless sweep transaction for the
|
||||||
|
// instant out.
|
||||||
|
func (i *InstantOut) createSweeplessSweepTx(feerate chainfee.SatPerKWeight) (
|
||||||
|
*wire.MsgTx, error) {
|
||||||
|
|
||||||
|
inputReservations, err := i.getInputReservations()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// First Create the tx.
|
||||||
|
msgTx := wire.NewMsgTx(2)
|
||||||
|
|
||||||
|
// add the reservation inputs
|
||||||
|
for _, reservation := range inputReservations {
|
||||||
|
msgTx.AddTxIn(&wire.TxIn{
|
||||||
|
PreviousOutPoint: reservation.Outpoint,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Estimate the fee
|
||||||
|
weight := sweeplessSweepWeight(len(inputReservations))
|
||||||
|
fee := feerate.FeeForWeight(weight)
|
||||||
|
if fee > i.value/5 {
|
||||||
|
return nil, errors.New("fee is higher than 20% of " +
|
||||||
|
"sweep value")
|
||||||
|
}
|
||||||
|
|
||||||
|
pkscript, err := txscript.PayToAddrScript(i.sweepAddress)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the sweep output
|
||||||
|
sweepOutput := &wire.TxOut{
|
||||||
|
Value: int64(i.value) - int64(fee),
|
||||||
|
PkScript: pkscript,
|
||||||
|
}
|
||||||
|
|
||||||
|
msgTx.AddTxOut(sweepOutput)
|
||||||
|
|
||||||
|
return msgTx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// signMusig2Tx adds the server nonces to the musig2 sessions and signs the
|
||||||
|
// transaction.
|
||||||
|
func (i *InstantOut) signMusig2Tx(ctx context.Context,
|
||||||
|
signer lndclient.SignerClient, tx *wire.MsgTx,
|
||||||
|
musig2sessions []*input.MuSig2SessionInfo,
|
||||||
|
counterPartyNonces [][66]byte) ([][]byte, error) {
|
||||||
|
|
||||||
|
inputs, err := i.getInputReservations()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
prevOutFetcher := inputs.GetPrevoutFetcher()
|
||||||
|
sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher)
|
||||||
|
sigs := make([][]byte, len(inputs))
|
||||||
|
|
||||||
|
for idx, reservation := range inputs {
|
||||||
|
if !reflect.DeepEqual(tx.TxIn[idx].PreviousOutPoint,
|
||||||
|
reservation.Outpoint) {
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("tx input does not match " +
|
||||||
|
"reservation")
|
||||||
|
}
|
||||||
|
|
||||||
|
taprootSigHash, err := txscript.CalcTaprootSignatureHash(
|
||||||
|
sigHashes, txscript.SigHashDefault,
|
||||||
|
tx, idx, prevOutFetcher,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var digest [32]byte
|
||||||
|
copy(digest[:], taprootSigHash)
|
||||||
|
|
||||||
|
// Register the server's nonce before attempting to create our
|
||||||
|
// partial signature.
|
||||||
|
haveAllNonces, err := signer.MuSig2RegisterNonces(
|
||||||
|
ctx, musig2sessions[idx].SessionID,
|
||||||
|
[][musig2.PubNonceSize]byte{counterPartyNonces[idx]},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity check that we have all the nonces.
|
||||||
|
if !haveAllNonces {
|
||||||
|
return nil, fmt.Errorf("invalid MuSig2 session: " +
|
||||||
|
"nonces missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since our MuSig2 session has all nonces, we can now create
|
||||||
|
// the local partial signature by signing the sig hash.
|
||||||
|
sig, err := signer.MuSig2Sign(
|
||||||
|
ctx, musig2sessions[idx].SessionID, digest, false,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sigs[idx] = sig
|
||||||
|
}
|
||||||
|
|
||||||
|
return sigs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// finalizeMusig2Transaction creates the finalized transactions for either
|
||||||
|
// the htlc or the cooperative close.
|
||||||
|
func (i *InstantOut) finalizeMusig2Transaction(ctx context.Context,
|
||||||
|
signer lndclient.SignerClient,
|
||||||
|
musig2Sessions []*input.MuSig2SessionInfo,
|
||||||
|
tx *wire.MsgTx, serverSigs [][]byte) (*wire.MsgTx, error) {
|
||||||
|
|
||||||
|
inputs, err := i.getInputReservations()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx := range inputs {
|
||||||
|
haveAllSigs, finalSig, err := signer.MuSig2CombineSig(
|
||||||
|
ctx, musig2Sessions[idx].SessionID,
|
||||||
|
[][]byte{serverSigs[idx]},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !haveAllSigs {
|
||||||
|
return nil, fmt.Errorf("missing sigs")
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.TxIn[idx].Witness = wire.TxWitness{finalSig}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateHtlcSweepTx creates the htlc sweep transaction for the instant out.
|
||||||
|
func (i *InstantOut) generateHtlcSweepTx(ctx context.Context,
|
||||||
|
signer lndclient.SignerClient, feeRate chainfee.SatPerKWeight,
|
||||||
|
network *chaincfg.Params, blockheight uint32) (
|
||||||
|
*wire.MsgTx, error) {
|
||||||
|
|
||||||
|
if network == nil {
|
||||||
|
return nil, errors.New("no network provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
if i.finalizedHtlcTx == nil {
|
||||||
|
return nil, errors.New("no finalized htlc tx")
|
||||||
|
}
|
||||||
|
|
||||||
|
htlc, err := i.getHtlc(network)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the sweep transaction.
|
||||||
|
sweepTx := wire.NewMsgTx(2)
|
||||||
|
sweepTx.LockTime = blockheight
|
||||||
|
|
||||||
|
var weightEstimator input.TxWeightEstimator
|
||||||
|
weightEstimator.AddP2TROutput()
|
||||||
|
|
||||||
|
err = htlc.AddSuccessToEstimator(&weightEstimator)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
htlcHash := i.finalizedHtlcTx.TxHash()
|
||||||
|
|
||||||
|
// Add the htlc input.
|
||||||
|
sweepTx.AddTxIn(&wire.TxIn{
|
||||||
|
PreviousOutPoint: wire.OutPoint{
|
||||||
|
Hash: htlcHash,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
SignatureScript: htlc.SigScript,
|
||||||
|
Sequence: htlc.SuccessSequence(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add the sweep output.
|
||||||
|
sweepPkScript, err := txscript.PayToAddrScript(i.sweepAddress)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fee := feeRate.FeeForWeight(int64(weightEstimator.Weight()))
|
||||||
|
|
||||||
|
htlcOutValue := i.finalizedHtlcTx.TxOut[0].Value
|
||||||
|
output := &wire.TxOut{
|
||||||
|
Value: htlcOutValue - int64(fee),
|
||||||
|
PkScript: sweepPkScript,
|
||||||
|
}
|
||||||
|
|
||||||
|
sweepTx.AddTxOut(output)
|
||||||
|
|
||||||
|
signDesc := lndclient.SignDescriptor{
|
||||||
|
WitnessScript: htlc.SuccessScript(),
|
||||||
|
Output: &wire.TxOut{
|
||||||
|
Value: htlcOutValue,
|
||||||
|
PkScript: htlc.PkScript,
|
||||||
|
},
|
||||||
|
HashType: htlc.SigHash(),
|
||||||
|
InputIndex: 0,
|
||||||
|
KeyDesc: keychain.KeyDescriptor{
|
||||||
|
KeyLocator: i.keyLocator,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
rawSigs, err := signer.SignOutputRaw(
|
||||||
|
ctx, sweepTx, []*lndclient.SignDescriptor{&signDesc}, nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("sign output error: %v", err)
|
||||||
|
}
|
||||||
|
sig := rawSigs[0]
|
||||||
|
|
||||||
|
// Add witness stack to the tx input.
|
||||||
|
sweepTx.TxIn[0].Witness, err = htlc.GenSuccessWitness(
|
||||||
|
sig, i.swapPreimage,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sweepTx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// htlcWeight returns the weight for the htlc transaction.
|
||||||
|
func htlcWeight(numInputs int) int64 {
|
||||||
|
var weightEstimator input.TxWeightEstimator
|
||||||
|
for i := 0; i < numInputs; i++ {
|
||||||
|
weightEstimator.AddTaprootKeySpendInput(
|
||||||
|
txscript.SigHashDefault,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
weightEstimator.AddP2WSHOutput()
|
||||||
|
|
||||||
|
return int64(weightEstimator.Weight())
|
||||||
|
}
|
||||||
|
|
||||||
|
// sweeplessSweepWeight returns the weight for the sweepless sweep transaction.
|
||||||
|
func sweeplessSweepWeight(numInputs int) int64 {
|
||||||
|
var weightEstimator input.TxWeightEstimator
|
||||||
|
for i := 0; i < numInputs; i++ {
|
||||||
|
weightEstimator.AddTaprootKeySpendInput(
|
||||||
|
txscript.SigHashDefault,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
weightEstimator.AddP2TROutput()
|
||||||
|
|
||||||
|
return int64(weightEstimator.Weight())
|
||||||
|
}
|
||||||
|
|
||||||
|
// pubkeyTo33ByteSlice converts a pubkey to a 33 byte slice.
|
||||||
|
func pubkeyTo33ByteSlice(pubkey *btcec.PublicKey) [33]byte {
|
||||||
|
var pubkeyBytes [33]byte
|
||||||
|
copy(pubkeyBytes[:], pubkey.SerializeCompressed())
|
||||||
|
|
||||||
|
return pubkeyBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
// toNonces converts a byte slice to a 66 byte slice.
|
||||||
|
func toNonces(nonces [][]byte) ([][66]byte, error) {
|
||||||
|
res := make([][66]byte, 0, len(nonces))
|
||||||
|
for _, n := range nonces {
|
||||||
|
n := n
|
||||||
|
nonce, err := byteSliceTo66ByteSlice(n)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res = append(res, nonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// byteSliceTo66ByteSlice converts a byte slice to a 66 byte slice.
|
||||||
|
func byteSliceTo66ByteSlice(b []byte) ([66]byte, error) {
|
||||||
|
if len(b) != 66 {
|
||||||
|
return [66]byte{}, fmt.Errorf("invalid byte slice length")
|
||||||
|
}
|
||||||
|
|
||||||
|
var res [66]byte
|
||||||
|
copy(res[:], b)
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
package instantout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/lightninglabs/loop/instantout/reservation"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
KeyFamily = int32(42069)
|
||||||
|
)
|
||||||
|
|
||||||
|
// InstantLoopOutStore is the interface that needs to be implemented by a
|
||||||
|
// store that wants to be used by the instant loop out manager.
|
||||||
|
type InstantLoopOutStore interface {
|
||||||
|
// CreateInstantLoopOut adds a new instant loop out to the store.
|
||||||
|
CreateInstantLoopOut(ctx context.Context, instantOut *InstantOut) error
|
||||||
|
|
||||||
|
// UpdateInstantLoopOut updates an existing instant loop out in the
|
||||||
|
// store.
|
||||||
|
UpdateInstantLoopOut(ctx context.Context, instantOut *InstantOut) error
|
||||||
|
|
||||||
|
// GetInstantLoopOut returns the instant loop out for the given swap
|
||||||
|
// hash.
|
||||||
|
GetInstantLoopOut(ctx context.Context, swapHash []byte) (*InstantOut, error)
|
||||||
|
|
||||||
|
// ListInstantLoopOuts returns all instant loop outs that are in the
|
||||||
|
// store.
|
||||||
|
ListInstantLoopOuts(ctx context.Context) ([]*InstantOut, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReservationManager handles fetching and locking of reservations.
|
||||||
|
type ReservationManager interface {
|
||||||
|
// GetReservation returns the reservation for the given id.
|
||||||
|
GetReservation(ctx context.Context, id reservation.ID) (
|
||||||
|
*reservation.Reservation, error)
|
||||||
|
|
||||||
|
// LockReservation locks the reservation for the given id.
|
||||||
|
LockReservation(ctx context.Context, id reservation.ID) error
|
||||||
|
|
||||||
|
// UnlockReservation unlocks the reservation for the given id.
|
||||||
|
UnlockReservation(ctx context.Context, id reservation.ID) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputReservations is a helper struct for the input reservations.
|
||||||
|
type InputReservations []InputReservation
|
||||||
|
|
||||||
|
// InputReservation is a helper struct for the input reservation.
|
||||||
|
type InputReservation struct {
|
||||||
|
Outpoint wire.OutPoint
|
||||||
|
Value btcutil.Amount
|
||||||
|
PkScript []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output returns the output for the input reservation.
|
||||||
|
func (r InputReservation) Output() *wire.TxOut {
|
||||||
|
return wire.NewTxOut(int64(r.Value), r.PkScript)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrevoutFetcher returns a prevout fetcher for the input reservations.
|
||||||
|
func (i InputReservations) GetPrevoutFetcher() txscript.PrevOutputFetcher {
|
||||||
|
prevOuts := make(map[wire.OutPoint]*wire.TxOut)
|
||||||
|
|
||||||
|
// add the reservation inputs
|
||||||
|
for _, reservation := range i {
|
||||||
|
prevOuts[reservation.Outpoint] = reservation.Output()
|
||||||
|
}
|
||||||
|
|
||||||
|
return txscript.NewMultiPrevOutFetcher(prevOuts)
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package instantout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/btcsuite/btclog"
|
||||||
|
"github.com/lightningnetwork/lnd/build"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Subsystem defines the sub system name of this package.
|
||||||
|
const Subsystem = "INST"
|
||||||
|
|
||||||
|
// log is a logger that is initialized with no output filters. This
|
||||||
|
// means the package will not perform any logging by default until the caller
|
||||||
|
// requests it.
|
||||||
|
var log btclog.Logger
|
||||||
|
|
||||||
|
// The default amount of logging is none.
|
||||||
|
func init() {
|
||||||
|
UseLogger(build.NewSubLogger(Subsystem, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseLogger uses a specified Logger to output package logging info.
|
||||||
|
// This should be used in preference to SetLogWriter if the caller is also
|
||||||
|
// using btclog.
|
||||||
|
func UseLogger(logger btclog.Logger) {
|
||||||
|
log = logger
|
||||||
|
}
|
Loading…
Reference in New Issue