mirror of https://github.com/lightninglabs/loop
Merge pull request #651 from sputn1ck/instantloopout_4
[4/?] Instant loop out: Add instant loop outspull/700/head
commit
c6e8664281
@ -0,0 +1,165 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/lightninglabs/loop/instantout/reservation"
|
||||
"github.com/lightninglabs/loop/looprpc"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var instantOutCommand = cli.Command{
|
||||
Name: "instantout",
|
||||
Usage: "perform an instant off-chain to on-chain swap (looping out)",
|
||||
Description: `
|
||||
Attempts to instantly loop out into the backing lnd's wallet. The amount
|
||||
will be chosen via the cli.
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "channel",
|
||||
Usage: "the comma-separated list of short " +
|
||||
"channel IDs of the channels to loop out",
|
||||
},
|
||||
},
|
||||
Action: instantOut,
|
||||
}
|
||||
|
||||
func instantOut(ctx *cli.Context) error {
|
||||
// Parse outgoing channel set. Don't string split if the flag is empty.
|
||||
// Otherwise, strings.Split returns a slice of length one with an empty
|
||||
// element.
|
||||
var outgoingChanSet []uint64
|
||||
if ctx.IsSet("channel") {
|
||||
chanStrings := strings.Split(ctx.String("channel"), ",")
|
||||
for _, chanString := range chanStrings {
|
||||
chanID, err := strconv.ParseUint(chanString, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing channel id "+
|
||||
"\"%v\"", chanString)
|
||||
}
|
||||
outgoingChanSet = append(outgoingChanSet, chanID)
|
||||
}
|
||||
}
|
||||
|
||||
// First set up the swap client itself.
|
||||
client, cleanup, err := getClient(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
// Now we fetch all the confirmed reservations.
|
||||
reservations, err := client.ListReservations(
|
||||
context.Background(), &looprpc.ListReservationsRequest{},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
confirmedReservations []*looprpc.ClientReservation
|
||||
totalAmt int64
|
||||
idx int
|
||||
)
|
||||
|
||||
for _, res := range reservations.Reservations {
|
||||
if res.State != string(reservation.Confirmed) {
|
||||
continue
|
||||
}
|
||||
|
||||
confirmedReservations = append(confirmedReservations, res)
|
||||
}
|
||||
|
||||
if len(confirmedReservations) == 0 {
|
||||
fmt.Printf("No confirmed reservations found \n")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("Available reservations: \n\n")
|
||||
for _, res := range confirmedReservations {
|
||||
idx++
|
||||
fmt.Printf("Reservation %v: %v \n", idx, res.Amount)
|
||||
totalAmt += int64(res.Amount)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("Max amount to instant out: %v\n", totalAmt)
|
||||
fmt.Println()
|
||||
|
||||
fmt.Println("Select reservations for instantout (e.g. '1,2,3')")
|
||||
fmt.Println("Type 'ALL' to use all available reservations.")
|
||||
|
||||
var answer string
|
||||
fmt.Scanln(&answer)
|
||||
|
||||
// Parse
|
||||
var selectedReservations [][]byte
|
||||
switch answer {
|
||||
case "ALL":
|
||||
for _, res := range confirmedReservations {
|
||||
selectedReservations = append(
|
||||
selectedReservations,
|
||||
res.ReservationId,
|
||||
)
|
||||
}
|
||||
|
||||
case "":
|
||||
return fmt.Errorf("no reservations selected")
|
||||
|
||||
default:
|
||||
selectedIndexes := strings.Split(answer, ",")
|
||||
selectedIndexMap := make(map[int]struct{})
|
||||
for _, idxStr := range selectedIndexes {
|
||||
idx, err := strconv.Atoi(idxStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if idx < 0 {
|
||||
return fmt.Errorf("invalid index %v", idx)
|
||||
}
|
||||
|
||||
if idx > len(confirmedReservations) {
|
||||
return fmt.Errorf("invalid index %v", idx)
|
||||
}
|
||||
if _, ok := selectedIndexMap[idx]; ok {
|
||||
return fmt.Errorf("duplicate index %v", idx)
|
||||
}
|
||||
|
||||
selectedReservations = append(
|
||||
selectedReservations,
|
||||
confirmedReservations[idx-1].ReservationId,
|
||||
)
|
||||
|
||||
selectedIndexMap[idx] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Starting instant swap out")
|
||||
|
||||
// Now we can request the instant out swap.
|
||||
instantOutRes, err := client.InstantOut(
|
||||
context.Background(),
|
||||
&looprpc.InstantOutRequest{
|
||||
ReservationIds: selectedReservations,
|
||||
OutgoingChanSet: outgoingChanSet,
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Instant out swap initiated with ID: %x, State: %v \n",
|
||||
instantOutRes.InstantOutHash, instantOutRes.State)
|
||||
|
||||
if instantOutRes.SweepTxId != "" {
|
||||
fmt.Printf("Sweepless sweep tx id: %v \n",
|
||||
instantOutRes.SweepTxId)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -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,36 @@
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Init: OnStart
|
||||
BuildHtlc
|
||||
BuildHtlc --> PushPreimage: OnHtlcSigReceived
|
||||
BuildHtlc --> InstantFailedOutFailed: OnError
|
||||
BuildHtlc --> InstantFailedOutFailed: OnRecover
|
||||
FailedHtlcSweep
|
||||
FinishedSweeplessSweep
|
||||
Init
|
||||
Init --> SendPaymentAndPollAccepted: OnInit
|
||||
Init --> InstantFailedOutFailed: OnError
|
||||
Init --> InstantFailedOutFailed: OnRecover
|
||||
InstantFailedOutFailed
|
||||
PublishHtlc
|
||||
PublishHtlc --> FailedHtlcSweep: OnError
|
||||
PublishHtlc --> PublishHtlc: OnRecover
|
||||
PublishHtlc --> WaitForHtlcSweepConfirmed: OnHtlcBroadcasted
|
||||
PushPreimage
|
||||
PushPreimage --> PushPreimage: OnRecover
|
||||
PushPreimage --> WaitForSweeplessSweepConfirmed: OnSweeplessSweepPublished
|
||||
PushPreimage --> InstantFailedOutFailed: OnError
|
||||
PushPreimage --> PublishHtlc: OnErrorPublishHtlc
|
||||
SendPaymentAndPollAccepted
|
||||
SendPaymentAndPollAccepted --> BuildHtlc: OnPaymentAccepted
|
||||
SendPaymentAndPollAccepted --> InstantFailedOutFailed: OnError
|
||||
SendPaymentAndPollAccepted --> InstantFailedOutFailed: OnRecover
|
||||
WaitForHtlcSweepConfirmed
|
||||
WaitForHtlcSweepConfirmed --> FinishedHtlcPreimageSweep: OnHtlcSwept
|
||||
WaitForHtlcSweepConfirmed --> WaitForHtlcSweepConfirmed: OnRecover
|
||||
WaitForHtlcSweepConfirmed --> FailedHtlcSweep: OnError
|
||||
WaitForSweeplessSweepConfirmed
|
||||
WaitForSweeplessSweepConfirmed --> FinishedSweeplessSweep: OnSweeplessSweepConfirmed
|
||||
WaitForSweeplessSweepConfirmed --> WaitForSweeplessSweepConfirmed: OnRecover
|
||||
WaitForSweeplessSweepConfirmed --> PublishHtlc: OnError
|
||||
```
|
@ -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
|
||||
}
|
@ -0,0 +1,201 @@
|
||||
package instantout
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/lightninglabs/loop/instantout/reservation"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultStateWaitTime = 30 * time.Second
|
||||
defaultCltv = 100
|
||||
ErrSwapDoesNotExist = errors.New("swap does not exist")
|
||||
)
|
||||
|
||||
// Manager manages the instantout state machines.
|
||||
type Manager struct {
|
||||
// cfg contains all the services that the reservation manager needs to
|
||||
// operate.
|
||||
cfg *Config
|
||||
|
||||
// activeInstantOuts contains all the active instantouts.
|
||||
activeInstantOuts map[lntypes.Hash]*FSM
|
||||
|
||||
// currentHeight stores the currently best known block height.
|
||||
currentHeight int32
|
||||
|
||||
// blockEpochChan receives new block heights.
|
||||
blockEpochChan chan int32
|
||||
|
||||
runCtx context.Context
|
||||
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
// NewInstantOutManager creates a new instantout manager.
|
||||
func NewInstantOutManager(cfg *Config) *Manager {
|
||||
return &Manager{
|
||||
cfg: cfg,
|
||||
activeInstantOuts: make(map[lntypes.Hash]*FSM),
|
||||
blockEpochChan: make(chan int32),
|
||||
}
|
||||
}
|
||||
|
||||
// Run runs the instantout manager.
|
||||
func (m *Manager) Run(ctx context.Context, initChan chan struct{},
|
||||
height int32) error {
|
||||
|
||||
log.Debugf("Starting instantout manager")
|
||||
defer func() {
|
||||
log.Debugf("Stopping instantout manager")
|
||||
}()
|
||||
|
||||
runCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
m.runCtx = runCtx
|
||||
m.currentHeight = height
|
||||
|
||||
err := m.recoverInstantOuts(runCtx)
|
||||
if err != nil {
|
||||
close(initChan)
|
||||
return err
|
||||
}
|
||||
|
||||
newBlockChan, newBlockErrChan, err := m.cfg.ChainNotifier.
|
||||
RegisterBlockEpochNtfn(ctx)
|
||||
if err != nil {
|
||||
close(initChan)
|
||||
return err
|
||||
}
|
||||
|
||||
close(initChan)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-runCtx.Done():
|
||||
return nil
|
||||
|
||||
case height := <-newBlockChan:
|
||||
m.Lock()
|
||||
m.currentHeight = height
|
||||
m.Unlock()
|
||||
|
||||
case err := <-newBlockErrChan:
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// recoverInstantOuts recovers all the active instantouts from the database.
|
||||
func (m *Manager) recoverInstantOuts(ctx context.Context) error {
|
||||
// Fetch all the active instantouts from the database.
|
||||
activeInstantOuts, err := m.cfg.Store.ListInstantLoopOuts(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, instantOut := range activeInstantOuts {
|
||||
if isFinalState(instantOut.State) {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debugf("Recovering instantout %v", instantOut.SwapHash)
|
||||
|
||||
instantOutFSM, err := NewFSMFromInstantOut(
|
||||
ctx, m.cfg, instantOut,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.activeInstantOuts[instantOut.SwapHash] = instantOutFSM
|
||||
|
||||
// As SendEvent can block, we'll start a goroutine to process
|
||||
// the event.
|
||||
go func() {
|
||||
err := instantOutFSM.SendEvent(OnRecover, nil)
|
||||
if err != nil {
|
||||
log.Errorf("FSM %v Error sending recover "+
|
||||
"event %v, state: %v",
|
||||
instantOutFSM.InstantOut.SwapHash, err,
|
||||
instantOutFSM.InstantOut.State)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewInstantOut creates a new instantout.
|
||||
func (m *Manager) NewInstantOut(ctx context.Context,
|
||||
reservations []reservation.ID) (*FSM, error) {
|
||||
|
||||
m.Lock()
|
||||
// Create the instantout request.
|
||||
request := &InitInstantOutCtx{
|
||||
cltvExpiry: m.currentHeight + int32(defaultCltv),
|
||||
reservations: reservations,
|
||||
initationHeight: m.currentHeight,
|
||||
protocolVersion: CurrentProtocolVersion(),
|
||||
}
|
||||
|
||||
instantOut, err := NewFSM(
|
||||
m.runCtx, m.cfg, ProtocolVersionFullReservation,
|
||||
)
|
||||
if err != nil {
|
||||
m.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
m.activeInstantOuts[instantOut.InstantOut.SwapHash] = instantOut
|
||||
m.Unlock()
|
||||
|
||||
// Start the instantout FSM.
|
||||
go func() {
|
||||
err := instantOut.SendEvent(OnStart, request)
|
||||
if err != nil {
|
||||
log.Errorf("Error sending event: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// If everything went well, we'll wait for the instant out to be
|
||||
// waiting for sweepless sweep to be confirmed.
|
||||
err = instantOut.DefaultObserver.WaitForState(
|
||||
ctx, defaultStateWaitTime, WaitForSweeplessSweepConfirmed,
|
||||
)
|
||||
if err != nil {
|
||||
if instantOut.LastActionError != nil {
|
||||
return instantOut, fmt.Errorf(
|
||||
"error waiting for sweepless sweep "+
|
||||
"confirmed: %w", instantOut.LastActionError,
|
||||
)
|
||||
}
|
||||
return instantOut, nil
|
||||
}
|
||||
|
||||
return instantOut, nil
|
||||
}
|
||||
|
||||
// GetActiveInstantOut returns an active instant out.
|
||||
func (m *Manager) GetActiveInstantOut(swapHash lntypes.Hash) (*FSM, error) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
fsm, ok := m.activeInstantOuts[swapHash]
|
||||
if !ok {
|
||||
return nil, ErrSwapDoesNotExist
|
||||
}
|
||||
|
||||
// If the instant out is in a final state, we'll remove it from the
|
||||
// active instant outs.
|
||||
if isFinalState(fsm.InstantOut.State) {
|
||||
delete(m.activeInstantOuts, swapHash)
|
||||
}
|
||||
|
||||
return fsm, nil
|
||||
}
|
@ -0,0 +1,432 @@
|
||||
package instantout
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/loop/fsm"
|
||||
"github.com/lightninglabs/loop/instantout/reservation"
|
||||
"github.com/lightninglabs/loop/loopdb"
|
||||
"github.com/lightninglabs/loop/loopdb/sqlc"
|
||||
"github.com/lightningnetwork/lnd/clock"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
)
|
||||
|
||||
// InstantOutBaseDB is the interface that contains all the queries generated
|
||||
// by sqlc for the instantout table.
|
||||
type InstantOutBaseDB interface {
|
||||
// InsertSwap inserts a new base swap.
|
||||
InsertSwap(ctx context.Context, arg sqlc.InsertSwapParams) error
|
||||
|
||||
// InsertHtlcKeys inserts the htlc keys for a swap.
|
||||
InsertHtlcKeys(ctx context.Context, arg sqlc.InsertHtlcKeysParams) error
|
||||
|
||||
// InsertInstantOut inserts a new instant out swap.
|
||||
InsertInstantOut(ctx context.Context,
|
||||
arg sqlc.InsertInstantOutParams) error
|
||||
|
||||
// InsertInstantOutUpdate inserts a new instant out update.
|
||||
InsertInstantOutUpdate(ctx context.Context,
|
||||
arg sqlc.InsertInstantOutUpdateParams) error
|
||||
|
||||
// UpdateInstantOut updates an instant out swap.
|
||||
UpdateInstantOut(ctx context.Context,
|
||||
arg sqlc.UpdateInstantOutParams) error
|
||||
|
||||
// GetInstantOutSwap retrieves an instant out swap.
|
||||
GetInstantOutSwap(ctx context.Context,
|
||||
swapHash []byte) (sqlc.GetInstantOutSwapRow, error)
|
||||
|
||||
// GetInstantOutSwapUpdates retrieves all instant out swap updates.
|
||||
GetInstantOutSwapUpdates(ctx context.Context,
|
||||
swapHash []byte) ([]sqlc.InstantoutUpdate, error)
|
||||
|
||||
// GetInstantOutSwaps retrieves all instant out swaps.
|
||||
GetInstantOutSwaps(ctx context.Context) ([]sqlc.GetInstantOutSwapsRow,
|
||||
error)
|
||||
|
||||
// ExecTx allows for executing a function in the context of a database
|
||||
// transaction.
|
||||
ExecTx(ctx context.Context, txOptions loopdb.TxOptions,
|
||||
txBody func(*sqlc.Queries) error) error
|
||||
}
|
||||
|
||||
// ReservationStore is the interface that is required to load the reservations
|
||||
// based on the stored reservation ids.
|
||||
type ReservationStore interface {
|
||||
// GetReservation returns the reservation for the given id.
|
||||
GetReservation(ctx context.Context, id reservation.ID) (
|
||||
*reservation.Reservation, error)
|
||||
}
|
||||
|
||||
type SQLStore struct {
|
||||
baseDb InstantOutBaseDB
|
||||
reservationStore ReservationStore
|
||||
clock clock.Clock
|
||||
network *chaincfg.Params
|
||||
}
|
||||
|
||||
// NewSQLStore creates a new SQLStore.
|
||||
func NewSQLStore(db InstantOutBaseDB, clock clock.Clock,
|
||||
reservationStore ReservationStore, network *chaincfg.Params) *SQLStore {
|
||||
|
||||
return &SQLStore{
|
||||
baseDb: db,
|
||||
clock: clock,
|
||||
reservationStore: reservationStore,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateInstantLoopOut adds a new instant loop out to the store.
|
||||
func (s *SQLStore) CreateInstantLoopOut(ctx context.Context,
|
||||
instantOut *InstantOut) error {
|
||||
|
||||
swapArgs := sqlc.InsertSwapParams{
|
||||
SwapHash: instantOut.SwapHash[:],
|
||||
Preimage: instantOut.swapPreimage[:],
|
||||
InitiationTime: s.clock.Now(),
|
||||
AmountRequested: int64(instantOut.value),
|
||||
CltvExpiry: instantOut.cltvExpiry,
|
||||
MaxMinerFee: 0,
|
||||
MaxSwapFee: 0,
|
||||
InitiationHeight: instantOut.initiationHeight,
|
||||
ProtocolVersion: int32(instantOut.protocolVersion),
|
||||
Label: "",
|
||||
}
|
||||
|
||||
htlcKeyArgs := sqlc.InsertHtlcKeysParams{
|
||||
SwapHash: instantOut.SwapHash[:],
|
||||
SenderScriptPubkey: instantOut.serverPubkey.
|
||||
SerializeCompressed(),
|
||||
ReceiverScriptPubkey: instantOut.clientPubkey.
|
||||
SerializeCompressed(),
|
||||
ClientKeyFamily: int32(instantOut.keyLocator.Family),
|
||||
ClientKeyIndex: int32(instantOut.keyLocator.Index),
|
||||
}
|
||||
|
||||
reservationIdByteSlice := reservationIdsToByteSlice(
|
||||
instantOut.reservations,
|
||||
)
|
||||
instantOutArgs := sqlc.InsertInstantOutParams{
|
||||
SwapHash: instantOut.SwapHash[:],
|
||||
Preimage: instantOut.swapPreimage[:],
|
||||
SweepAddress: instantOut.sweepAddress.String(),
|
||||
OutgoingChanSet: instantOut.outgoingChanSet.String(),
|
||||
HtlcFeeRate: int64(instantOut.htlcFeeRate),
|
||||
ReservationIds: reservationIdByteSlice,
|
||||
SwapInvoice: instantOut.swapInvoice,
|
||||
}
|
||||
|
||||
updateArgs := sqlc.InsertInstantOutUpdateParams{
|
||||
SwapHash: instantOut.SwapHash[:],
|
||||
UpdateTimestamp: s.clock.Now(),
|
||||
UpdateState: string(instantOut.State),
|
||||
}
|
||||
|
||||
return s.baseDb.ExecTx(ctx, &loopdb.SqliteTxOptions{},
|
||||
func(q *sqlc.Queries) error {
|
||||
err := q.InsertSwap(ctx, swapArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = q.InsertHtlcKeys(ctx, htlcKeyArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = q.InsertInstantOut(ctx, instantOutArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return q.InsertInstantOutUpdate(ctx, updateArgs)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateInstantLoopOut updates an existing instant loop out in the
|
||||
// store.
|
||||
func (s *SQLStore) UpdateInstantLoopOut(ctx context.Context,
|
||||
instantOut *InstantOut) error {
|
||||
|
||||
// Serialize the FinalHtlcTx.
|
||||
var finalHtlcTx []byte
|
||||
if instantOut.finalizedHtlcTx != nil {
|
||||
var buffer bytes.Buffer
|
||||
err := instantOut.finalizedHtlcTx.Serialize(
|
||||
&buffer,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
finalHtlcTx = buffer.Bytes()
|
||||
}
|
||||
|
||||
var finalSweeplessSweepTx []byte
|
||||
if instantOut.finalizedSweeplessSweepTx != nil {
|
||||
var buffer bytes.Buffer
|
||||
err := instantOut.finalizedSweeplessSweepTx.Serialize(
|
||||
&buffer,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
finalSweeplessSweepTx = buffer.Bytes()
|
||||
}
|
||||
|
||||
var sweepTxHash []byte
|
||||
if instantOut.SweepTxHash != nil {
|
||||
sweepTxHash = instantOut.SweepTxHash[:]
|
||||
}
|
||||
|
||||
updateParams := sqlc.UpdateInstantOutParams{
|
||||
SwapHash: instantOut.SwapHash[:],
|
||||
FinalizedHtlcTx: finalHtlcTx,
|
||||
SweepTxHash: sweepTxHash,
|
||||
FinalizedSweeplessSweepTx: finalSweeplessSweepTx,
|
||||
SweepConfirmationHeight: serializeNullInt32(
|
||||
int32(instantOut.sweepConfirmationHeight),
|
||||
),
|
||||
}
|
||||
|
||||
updateArgs := sqlc.InsertInstantOutUpdateParams{
|
||||
SwapHash: instantOut.SwapHash[:],
|
||||
UpdateTimestamp: s.clock.Now(),
|
||||
UpdateState: string(instantOut.State),
|
||||
}
|
||||
|
||||
return s.baseDb.ExecTx(ctx, &loopdb.SqliteTxOptions{},
|
||||
func(q *sqlc.Queries) error {
|
||||
err := q.UpdateInstantOut(ctx, updateParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return q.InsertInstantOutUpdate(ctx, updateArgs)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// GetInstantLoopOut returns the instant loop out for the given swap
|
||||
// hash.
|
||||
func (s *SQLStore) GetInstantLoopOut(ctx context.Context, swapHash []byte) (
|
||||
*InstantOut, error) {
|
||||
|
||||
row, err := s.baseDb.GetInstantOutSwap(ctx, swapHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updates, err := s.baseDb.GetInstantOutSwapUpdates(ctx, swapHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.sqlInstantOutToInstantOut(ctx, row, updates)
|
||||
}
|
||||
|
||||
// ListInstantLoopOuts returns all instant loop outs that are in the
|
||||
// store.
|
||||
func (s *SQLStore) ListInstantLoopOuts(ctx context.Context) ([]*InstantOut,
|
||||
error) {
|
||||
|
||||
rows, err := s.baseDb.GetInstantOutSwaps(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var instantOuts []*InstantOut
|
||||
for _, row := range rows {
|
||||
updates, err := s.baseDb.GetInstantOutSwapUpdates(
|
||||
ctx, row.SwapHash,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
instantOut, err := s.sqlInstantOutToInstantOut(
|
||||
ctx, sqlc.GetInstantOutSwapRow(row), updates,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
instantOuts = append(instantOuts, instantOut)
|
||||
}
|
||||
|
||||
return instantOuts, nil
|
||||
}
|
||||
|
||||
// sqlInstantOutToInstantOut converts sql rows to an instant out struct.
|
||||
func (s *SQLStore) sqlInstantOutToInstantOut(ctx context.Context,
|
||||
row sqlc.GetInstantOutSwapRow, updates []sqlc.InstantoutUpdate) (
|
||||
*InstantOut, error) {
|
||||
|
||||
swapHash, err := lntypes.MakeHash(row.SwapHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
swapPreImage, err := lntypes.MakePreimage(row.Preimage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serverKey, err := btcec.ParsePubKey(row.SenderScriptPubkey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clientKey, err := btcec.ParsePubKey(row.ReceiverScriptPubkey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var finalizedHtlcTx *wire.MsgTx
|
||||
if row.FinalizedHtlcTx != nil {
|
||||
finalizedHtlcTx = &wire.MsgTx{}
|
||||
err := finalizedHtlcTx.Deserialize(bytes.NewReader(
|
||||
row.FinalizedHtlcTx,
|
||||
))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var finalizedSweepLessSweepTx *wire.MsgTx
|
||||
if row.FinalizedSweeplessSweepTx != nil {
|
||||
finalizedSweepLessSweepTx = &wire.MsgTx{}
|
||||
err := finalizedSweepLessSweepTx.Deserialize(bytes.NewReader(
|
||||
row.FinalizedSweeplessSweepTx,
|
||||
))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var sweepTxHash *chainhash.Hash
|
||||
if row.SweepTxHash != nil {
|
||||
sweepTxHash, err = chainhash.NewHash(row.SweepTxHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var outgoingChanSet loopdb.ChannelSet
|
||||
if row.OutgoingChanSet != "" {
|
||||
outgoingChanSet, err = loopdb.ConvertOutgoingChanSet(
|
||||
row.OutgoingChanSet,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
reservationIds, err := byteSliceToReservationIds(row.ReservationIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reservations := make([]*reservation.Reservation, 0, len(reservationIds))
|
||||
for _, id := range reservationIds {
|
||||
reservation, err := s.reservationStore.GetReservation(
|
||||
ctx, id,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reservations = append(reservations, reservation)
|
||||
}
|
||||
|
||||
sweepAddress, err := btcutil.DecodeAddress(row.SweepAddress, s.network)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
instantOut := &InstantOut{
|
||||
SwapHash: swapHash,
|
||||
swapPreimage: swapPreImage,
|
||||
cltvExpiry: row.CltvExpiry,
|
||||
outgoingChanSet: outgoingChanSet,
|
||||
reservations: reservations,
|
||||
protocolVersion: ProtocolVersion(row.ProtocolVersion),
|
||||
initiationHeight: row.InitiationHeight,
|
||||
value: btcutil.Amount(row.AmountRequested),
|
||||
keyLocator: keychain.KeyLocator{
|
||||
Family: keychain.KeyFamily(row.ClientKeyFamily),
|
||||
Index: uint32(row.ClientKeyIndex),
|
||||
},
|
||||
clientPubkey: clientKey,
|
||||
serverPubkey: serverKey,
|
||||
swapInvoice: row.SwapInvoice,
|
||||
htlcFeeRate: chainfee.SatPerKWeight(row.HtlcFeeRate),
|
||||
sweepAddress: sweepAddress,
|
||||
finalizedHtlcTx: finalizedHtlcTx,
|
||||
SweepTxHash: sweepTxHash,
|
||||
finalizedSweeplessSweepTx: finalizedSweepLessSweepTx,
|
||||
sweepConfirmationHeight: uint32(deserializeNullInt32(
|
||||
row.SweepConfirmationHeight,
|
||||
)),
|
||||
}
|
||||
|
||||
if len(updates) > 0 {
|
||||
lastUpdate := updates[len(updates)-1]
|
||||
instantOut.State = fsm.StateType(lastUpdate.UpdateState)
|
||||
}
|
||||
|
||||
return instantOut, nil
|
||||
}
|
||||
|
||||
// reservationIdsToByteSlice converts a slice of reservation ids to a byte
|
||||
// slice.
|
||||
func reservationIdsToByteSlice(reservations []*reservation.Reservation) []byte {
|
||||
var reservationIds []byte
|
||||
for _, reservation := range reservations {
|
||||
reservationIds = append(reservationIds, reservation.ID[:]...)
|
||||
}
|
||||
|
||||
return reservationIds
|
||||
}
|
||||
|
||||
// byteSliceToReservationIds converts a byte slice to a slice of reservation
|
||||
// ids.
|
||||
func byteSliceToReservationIds(byteSlice []byte) ([]reservation.ID, error) {
|
||||
if len(byteSlice)%32 != 0 {
|
||||
return nil, fmt.Errorf("invalid byte slice length")
|
||||
}
|
||||
|
||||
var reservationIds []reservation.ID
|
||||
for i := 0; i < len(byteSlice); i += 32 {
|
||||
var id reservation.ID
|
||||
copy(id[:], byteSlice[i:i+32])
|
||||
reservationIds = append(reservationIds, id)
|
||||
}
|
||||
|
||||
return reservationIds, nil
|
||||
}
|
||||
|
||||
// serializeNullInt32 serializes an int32 to a sql.NullInt32.
|
||||
func serializeNullInt32(i int32) sql.NullInt32 {
|
||||
return sql.NullInt32{
|
||||
Int32: i,
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
// deserializeNullInt32 deserializes an int32 from a sql.NullInt32.
|
||||
func deserializeNullInt32(i sql.NullInt32) int32 {
|
||||
if i.Valid {
|
||||
return i.Int32
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package instantout
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/lightninglabs/loop/instantout/reservation"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConvertingReservations(t *testing.T) {
|
||||
var resId1, resId2 reservation.ID
|
||||
|
||||
// fill the ids with random values.
|
||||
if _, err := rand.Read(resId1[:]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := rand.Read(resId2[:]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
reservations := []*reservation.Reservation{
|
||||
{ID: resId1}, {ID: resId2},
|
||||
}
|
||||
|
||||
byteSlice := reservationIdsToByteSlice(reservations)
|
||||
require.Len(t, byteSlice, 64)
|
||||
|
||||
reservationIds, err := byteSliceToReservationIds(byteSlice)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, reservationIds, 2)
|
||||
require.Equal(t, resId1, reservationIds[0])
|
||||
require.Equal(t, resId2, reservationIds[1])
|
||||
}
|
@ -0,0 +1,329 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.17.2
|
||||
// source: instantout.sql
|
||||
|
||||
package sqlc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
const getInstantOutSwap = `-- name: GetInstantOutSwap :one
|
||||
SELECT
|
||||
swaps.id, swaps.swap_hash, swaps.preimage, swaps.initiation_time, swaps.amount_requested, swaps.cltv_expiry, swaps.max_miner_fee, swaps.max_swap_fee, swaps.initiation_height, swaps.protocol_version, swaps.label,
|
||||
instantout_swaps.swap_hash, instantout_swaps.preimage, instantout_swaps.sweep_address, instantout_swaps.outgoing_chan_set, instantout_swaps.htlc_fee_rate, instantout_swaps.reservation_ids, instantout_swaps.swap_invoice, instantout_swaps.finalized_htlc_tx, instantout_swaps.sweep_tx_hash, instantout_swaps.finalized_sweepless_sweep_tx, instantout_swaps.sweep_confirmation_height,
|
||||
htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index
|
||||
FROM
|
||||
swaps
|
||||
JOIN
|
||||
instantout_swaps ON swaps.swap_hash = instantout_swaps.swap_hash
|
||||
JOIN
|
||||
htlc_keys ON swaps.swap_hash = htlc_keys.swap_hash
|
||||
WHERE
|
||||
swaps.swap_hash = $1
|
||||
`
|
||||
|
||||
type GetInstantOutSwapRow struct {
|
||||
ID int32
|
||||
SwapHash []byte
|
||||
Preimage []byte
|
||||
InitiationTime time.Time
|
||||
AmountRequested int64
|
||||
CltvExpiry int32
|
||||
MaxMinerFee int64
|
||||
MaxSwapFee int64
|
||||
InitiationHeight int32
|
||||
ProtocolVersion int32
|
||||
Label string
|
||||
SwapHash_2 []byte
|
||||
Preimage_2 []byte
|
||||
SweepAddress string
|
||||
OutgoingChanSet string
|
||||
HtlcFeeRate int64
|
||||
ReservationIds []byte
|
||||
SwapInvoice string
|
||||
FinalizedHtlcTx []byte
|
||||
SweepTxHash []byte
|
||||
FinalizedSweeplessSweepTx []byte
|
||||
SweepConfirmationHeight sql.NullInt32
|
||||
SwapHash_3 []byte
|
||||
SenderScriptPubkey []byte
|
||||
ReceiverScriptPubkey []byte
|
||||
SenderInternalPubkey []byte
|
||||
ReceiverInternalPubkey []byte
|
||||
ClientKeyFamily int32
|
||||
ClientKeyIndex int32
|
||||
}
|
||||
|
||||
func (q *Queries) GetInstantOutSwap(ctx context.Context, swapHash []byte) (GetInstantOutSwapRow, error) {
|
||||
row := q.db.QueryRowContext(ctx, getInstantOutSwap, swapHash)
|
||||
var i GetInstantOutSwapRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SwapHash,
|
||||
&i.Preimage,
|
||||
&i.InitiationTime,
|
||||
&i.AmountRequested,
|
||||
&i.CltvExpiry,
|
||||
&i.MaxMinerFee,
|
||||
&i.MaxSwapFee,
|
||||
&i.InitiationHeight,
|
||||
&i.ProtocolVersion,
|
||||
&i.Label,
|
||||
&i.SwapHash_2,
|
||||
&i.Preimage_2,
|
||||
&i.SweepAddress,
|
||||
&i.OutgoingChanSet,
|
||||
&i.HtlcFeeRate,
|
||||
&i.ReservationIds,
|
||||
&i.SwapInvoice,
|
||||
&i.FinalizedHtlcTx,
|
||||
&i.SweepTxHash,
|
||||
&i.FinalizedSweeplessSweepTx,
|
||||
&i.SweepConfirmationHeight,
|
||||
&i.SwapHash_3,
|
||||
&i.SenderScriptPubkey,
|
||||
&i.ReceiverScriptPubkey,
|
||||
&i.SenderInternalPubkey,
|
||||
&i.ReceiverInternalPubkey,
|
||||
&i.ClientKeyFamily,
|
||||
&i.ClientKeyIndex,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getInstantOutSwapUpdates = `-- name: GetInstantOutSwapUpdates :many
|
||||
SELECT
|
||||
instantout_updates.id, instantout_updates.swap_hash, instantout_updates.update_state, instantout_updates.update_timestamp
|
||||
FROM
|
||||
instantout_updates
|
||||
WHERE
|
||||
instantout_updates.swap_hash = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetInstantOutSwapUpdates(ctx context.Context, swapHash []byte) ([]InstantoutUpdate, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getInstantOutSwapUpdates, swapHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []InstantoutUpdate
|
||||
for rows.Next() {
|
||||
var i InstantoutUpdate
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.SwapHash,
|
||||
&i.UpdateState,
|
||||
&i.UpdateTimestamp,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getInstantOutSwaps = `-- name: GetInstantOutSwaps :many
|
||||
SELECT
|
||||
swaps.id, swaps.swap_hash, swaps.preimage, swaps.initiation_time, swaps.amount_requested, swaps.cltv_expiry, swaps.max_miner_fee, swaps.max_swap_fee, swaps.initiation_height, swaps.protocol_version, swaps.label,
|
||||
instantout_swaps.swap_hash, instantout_swaps.preimage, instantout_swaps.sweep_address, instantout_swaps.outgoing_chan_set, instantout_swaps.htlc_fee_rate, instantout_swaps.reservation_ids, instantout_swaps.swap_invoice, instantout_swaps.finalized_htlc_tx, instantout_swaps.sweep_tx_hash, instantout_swaps.finalized_sweepless_sweep_tx, instantout_swaps.sweep_confirmation_height,
|
||||
htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index
|
||||
FROM
|
||||
swaps
|
||||
JOIN
|
||||
instantout_swaps ON swaps.swap_hash = instantout_swaps.swap_hash
|
||||
JOIN
|
||||
htlc_keys ON swaps.swap_hash = htlc_keys.swap_hash
|
||||
ORDER BY
|
||||
swaps.id
|
||||
`
|
||||
|
||||
type GetInstantOutSwapsRow struct {
|
||||
ID int32
|
||||
SwapHash []byte
|
||||
Preimage []byte
|
||||
InitiationTime time.Time
|
||||
AmountRequested int64
|
||||
CltvExpiry int32
|
||||
MaxMinerFee int64
|
||||
MaxSwapFee int64
|
||||
InitiationHeight int32
|
||||
ProtocolVersion int32
|
||||
Label string
|
||||
SwapHash_2 []byte
|
||||
Preimage_2 []byte
|
||||
SweepAddress string
|
||||
OutgoingChanSet string
|
||||
HtlcFeeRate int64
|
||||
ReservationIds []byte
|
||||
SwapInvoice string
|
||||
FinalizedHtlcTx []byte
|
||||
SweepTxHash []byte
|
||||
FinalizedSweeplessSweepTx []byte
|
||||
SweepConfirmationHeight sql.NullInt32
|
||||
SwapHash_3 []byte
|
||||
SenderScriptPubkey []byte
|
||||
ReceiverScriptPubkey []byte
|
||||
SenderInternalPubkey []byte
|
||||
ReceiverInternalPubkey []byte
|
||||
ClientKeyFamily int32
|
||||
ClientKeyIndex int32
|
||||
}
|
||||
|
||||
func (q *Queries) GetInstantOutSwaps(ctx context.Context) ([]GetInstantOutSwapsRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getInstantOutSwaps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetInstantOutSwapsRow
|
||||
for rows.Next() {
|
||||
var i GetInstantOutSwapsRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.SwapHash,
|
||||
&i.Preimage,
|
||||
&i.InitiationTime,
|
||||
&i.AmountRequested,
|
||||
&i.CltvExpiry,
|
||||
&i.MaxMinerFee,
|
||||
&i.MaxSwapFee,
|
||||
&i.InitiationHeight,
|
||||
&i.ProtocolVersion,
|
||||
&i.Label,
|
||||
&i.SwapHash_2,
|
||||
&i.Preimage_2,
|
||||
&i.SweepAddress,
|
||||
&i.OutgoingChanSet,
|
||||
&i.HtlcFeeRate,
|
||||
&i.ReservationIds,
|
||||
&i.SwapInvoice,
|
||||
&i.FinalizedHtlcTx,
|
||||
&i.SweepTxHash,
|
||||
&i.FinalizedSweeplessSweepTx,
|
||||
&i.SweepConfirmationHeight,
|
||||
&i.SwapHash_3,
|
||||
&i.SenderScriptPubkey,
|
||||
&i.ReceiverScriptPubkey,
|
||||
&i.SenderInternalPubkey,
|
||||
&i.ReceiverInternalPubkey,
|
||||
&i.ClientKeyFamily,
|
||||
&i.ClientKeyIndex,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const insertInstantOut = `-- name: InsertInstantOut :exec
|
||||
INSERT INTO instantout_swaps (
|
||||
swap_hash,
|
||||
preimage,
|
||||
sweep_address,
|
||||
outgoing_chan_set,
|
||||
htlc_fee_rate,
|
||||
reservation_ids,
|
||||
swap_invoice
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
$7
|
||||
)
|
||||
`
|
||||
|
||||
type InsertInstantOutParams struct {
|
||||
SwapHash []byte
|
||||
Preimage []byte
|
||||
SweepAddress string
|
||||
OutgoingChanSet string
|
||||
HtlcFeeRate int64
|
||||
ReservationIds []byte
|
||||
SwapInvoice string
|
||||
}
|
||||
|
||||
func (q *Queries) InsertInstantOut(ctx context.Context, arg InsertInstantOutParams) error {
|
||||
_, err := q.db.ExecContext(ctx, insertInstantOut,
|
||||
arg.SwapHash,
|
||||
arg.Preimage,
|
||||
arg.SweepAddress,
|
||||
arg.OutgoingChanSet,
|
||||
arg.HtlcFeeRate,
|
||||
arg.ReservationIds,
|
||||
arg.SwapInvoice,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const insertInstantOutUpdate = `-- name: InsertInstantOutUpdate :exec
|
||||
INSERT INTO instantout_updates (
|
||||
swap_hash,
|
||||
update_state,
|
||||
update_timestamp
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3
|
||||
)
|
||||
`
|
||||
|
||||
type InsertInstantOutUpdateParams struct {
|
||||
SwapHash []byte
|
||||
UpdateState string
|
||||
UpdateTimestamp time.Time
|
||||
}
|
||||
|
||||
func (q *Queries) InsertInstantOutUpdate(ctx context.Context, arg InsertInstantOutUpdateParams) error {
|
||||
_, err := q.db.ExecContext(ctx, insertInstantOutUpdate, arg.SwapHash, arg.UpdateState, arg.UpdateTimestamp)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateInstantOut = `-- name: UpdateInstantOut :exec
|
||||
UPDATE instantout_swaps
|
||||
SET
|
||||
finalized_htlc_tx = $2,
|
||||
sweep_tx_hash = $3,
|
||||
finalized_sweepless_sweep_tx = $4,
|
||||
sweep_confirmation_height = $5
|
||||
WHERE
|
||||
instantout_swaps.swap_hash = $1
|
||||
`
|
||||
|
||||
type UpdateInstantOutParams struct {
|
||||
SwapHash []byte
|
||||
FinalizedHtlcTx []byte
|
||||
SweepTxHash []byte
|
||||
FinalizedSweeplessSweepTx []byte
|
||||
SweepConfirmationHeight sql.NullInt32
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateInstantOut(ctx context.Context, arg UpdateInstantOutParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateInstantOut,
|
||||
arg.SwapHash,
|
||||
arg.FinalizedHtlcTx,
|
||||
arg.SweepTxHash,
|
||||
arg.FinalizedSweeplessSweepTx,
|
||||
arg.SweepConfirmationHeight,
|
||||
)
|
||||
return err
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
DROP INDEX IF EXISTS instantout_updates_swap_hash_idx;
|
||||
DROP INDEX IF EXISTS instantout_swap_hash_idx;
|
||||
DROP TABLE IF EXISTS instantout_updates;
|
||||
DROP TABLE IF EXISTS instantout_swaps;
|
@ -0,0 +1,52 @@
|
||||
CREATE TABLE IF NOT EXISTS instantout_swaps (
|
||||
-- swap_hash points to the parent swap hash.
|
||||
swap_hash BLOB PRIMARY KEY,
|
||||
|
||||
-- preimage is the preimage of the swap.
|
||||
preimage BLOB NOT NULL,
|
||||
|
||||
-- sweep_address is the address that the server should sweep the funds to.
|
||||
sweep_address TEXT NOT NULL,
|
||||
|
||||
-- outgoing_chan_set is the set of short ids of channels that may be used.
|
||||
-- If empty, any channel may be used.
|
||||
outgoing_chan_set TEXT NOT NULL,
|
||||
|
||||
-- htlc_fee_rate is the fee rate in sat/kw that is used for the htlc transaction.
|
||||
htlc_fee_rate BIGINT NOT NULL,
|
||||
|
||||
-- reservation_ids is a list of ids of the reservations that are used for this swap.
|
||||
reservation_ids BLOB NOT NULL,
|
||||
|
||||
-- swap_invoice is the invoice that is to be paid by the client to
|
||||
-- initiate the loop out swap.
|
||||
swap_invoice TEXT NOT NULL,
|
||||
|
||||
-- finalized_htlc_tx contains the fully signed htlc transaction.
|
||||
finalized_htlc_tx BLOB,
|
||||
|
||||
-- sweep_tx_hash is the hash of the transaction that sweeps the htlc.
|
||||
sweep_tx_hash BLOB,
|
||||
|
||||
-- finalized_sweepless_sweep_tx contains the fully signed sweepless sweep transaction.
|
||||
finalized_sweepless_sweep_tx BLOB,
|
||||
|
||||
-- sweep_confirmation_height is the block height at which the sweep transaction is confirmed.
|
||||
sweep_confirmation_height INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS instantout_updates (
|
||||
-- id is auto incremented for each update.
|
||||
id INTEGER PRIMARY KEY,
|
||||
|
||||
-- swap_hash is the hash of the swap that this update is for.
|
||||
swap_hash BLOB NOT NULL REFERENCES instantout_swaps(swap_hash),
|
||||
|
||||
-- update_state is the state of the swap at the time of the update.
|
||||
update_state TEXT NOT NULL,
|
||||
|
||||
-- update_timestamp is the time at which the update was created.
|
||||
update_timestamp TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS instantout_updates_swap_hash_idx ON instantout_updates(swap_hash);
|
@ -0,0 +1,75 @@
|
||||
-- name: InsertInstantOut :exec
|
||||
INSERT INTO instantout_swaps (
|
||||
swap_hash,
|
||||
preimage,
|
||||
sweep_address,
|
||||
outgoing_chan_set,
|
||||
htlc_fee_rate,
|
||||
reservation_ids,
|
||||
swap_invoice
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
$7
|
||||
);
|
||||
|
||||
-- name: UpdateInstantOut :exec
|
||||
UPDATE instantout_swaps
|
||||
SET
|
||||
finalized_htlc_tx = $2,
|
||||
sweep_tx_hash = $3,
|
||||
finalized_sweepless_sweep_tx = $4,
|
||||
sweep_confirmation_height = $5
|
||||
WHERE
|
||||
instantout_swaps.swap_hash = $1;
|
||||
|
||||
-- name: InsertInstantOutUpdate :exec
|
||||
INSERT INTO instantout_updates (
|
||||
swap_hash,
|
||||
update_state,
|
||||
update_timestamp
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3
|
||||
);
|
||||
|
||||
-- name: GetInstantOutSwap :one
|
||||
SELECT
|
||||
swaps.*,
|
||||
instantout_swaps.*,
|
||||
htlc_keys.*
|
||||
FROM
|
||||
swaps
|
||||
JOIN
|
||||
instantout_swaps ON swaps.swap_hash = instantout_swaps.swap_hash
|
||||
JOIN
|
||||
htlc_keys ON swaps.swap_hash = htlc_keys.swap_hash
|
||||
WHERE
|
||||
swaps.swap_hash = $1;
|
||||
|
||||
-- name: GetInstantOutSwaps :many
|
||||
SELECT
|
||||
swaps.*,
|
||||
instantout_swaps.*,
|
||||
htlc_keys.*
|
||||
FROM
|
||||
swaps
|
||||
JOIN
|
||||
instantout_swaps ON swaps.swap_hash = instantout_swaps.swap_hash
|
||||
JOIN
|
||||
htlc_keys ON swaps.swap_hash = htlc_keys.swap_hash
|
||||
ORDER BY
|
||||
swaps.id;
|
||||
|
||||
-- name: GetInstantOutSwapUpdates :many
|
||||
SELECT
|
||||
instantout_updates.*
|
||||
FROM
|
||||
instantout_updates
|
||||
WHERE
|
||||
instantout_updates.swap_hash = $1;
|
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
go run ./fsm/stateparser/stateparser.go --out ./fsm/example_fsm.md --fsm example
|
||||
go run ./fsm/stateparser/stateparser.go --out ./reservation/reservation_fsm.md --fsm reservation
|
||||
go run ./fsm/stateparser/stateparser.go --out ./instantout/fsm.md --fsm instantout
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,136 @@
|
||||
syntax = "proto3";
|
||||
|
||||
// We can't change this to swapserverrpc, it would be a breaking change because
|
||||
// the package name is also contained in the HTTP URIs and old clients would
|
||||
// call the wrong endpoints. Luckily with the go_package option we can have
|
||||
// different golang and RPC package names to fix protobuf namespace conflicts.
|
||||
package looprpc;
|
||||
|
||||
option go_package = "github.com/lightninglabs/loop/swapserverrpc";
|
||||
|
||||
service InstantSwapServer {
|
||||
// RequestInstantLoopOut initiates an instant loop out swap.
|
||||
rpc RequestInstantLoopOut (InstantLoopOutRequest)
|
||||
returns (InstantLoopOutResponse);
|
||||
|
||||
// PollPaymentAccepted polls the server to see if the payment has been
|
||||
// accepted.
|
||||
rpc PollPaymentAccepted (PollPaymentAcceptedRequest)
|
||||
returns (PollPaymentAcceptedResponse);
|
||||
|
||||
// InitHtlcSig is called by the client to initiate the htlc sig exchange.
|
||||
rpc InitHtlcSig (InitHtlcSigRequest) returns (InitHtlcSigResponse);
|
||||
|
||||
// PushHtlcSig is called by the client to push the htlc sigs to the server.
|
||||
rpc PushHtlcSig (PushHtlcSigRequest) returns (PushHtlcSigResponse);
|
||||
|
||||
// PushPreimage is called by the client to push the preimage to the server.
|
||||
// This returns the musig2 signatures that the client needs to sweep the
|
||||
// htlc.
|
||||
rpc PushPreimage (PushPreimageRequest) returns (PushPreimageResponse);
|
||||
|
||||
// CancelInstantSwap tries to cancel the instant swap. This can only be
|
||||
// called if the swap has not been accepted yet.
|
||||
rpc CancelInstantSwap (CancelInstantSwapRequest)
|
||||
returns (CancelInstantSwapResponse);
|
||||
}
|
||||
|
||||
message InstantLoopOutRequest {
|
||||
// Htlc related fields:
|
||||
// The key for the htlc preimage spending path.
|
||||
bytes receiver_key = 1;
|
||||
|
||||
// The hash of the preimage that will be used to settle the htlc.
|
||||
bytes swap_hash = 2;
|
||||
|
||||
// The requested absolute block height of the on-chain htlc.
|
||||
int32 expiry = 3;
|
||||
|
||||
// The fee rate in sat/kw that should be used for the htlc.
|
||||
uint64 htlc_fee_rate = 4;
|
||||
|
||||
// The reservations used as the inputs.
|
||||
repeated bytes reservation_ids = 5;
|
||||
|
||||
// The protocol version to use for the swap.
|
||||
InstantOutProtocolVersion protocol_version = 6;
|
||||
}
|
||||
|
||||
message InstantLoopOutResponse {
|
||||
// The swap invoice that the client should pay.
|
||||
string swap_invoice = 1;
|
||||
|
||||
// the key for the htlc expiry path.
|
||||
bytes sender_key = 2;
|
||||
};
|
||||
|
||||
message PollPaymentAcceptedRequest {
|
||||
// The hash of the swap invoice.
|
||||
bytes swap_hash = 1;
|
||||
}
|
||||
|
||||
message PollPaymentAcceptedResponse {
|
||||
// Whether the payment has been accepted.
|
||||
bool accepted = 1;
|
||||
}
|
||||
|
||||
message InitHtlcSigRequest {
|
||||
// The hash of the swap invoice.
|
||||
bytes swap_hash = 1;
|
||||
|
||||
// The nonces that the client will use to generate the htlc sigs.
|
||||
repeated bytes htlc_client_nonces = 2;
|
||||
}
|
||||
|
||||
message InitHtlcSigResponse {
|
||||
// The nonces that the server will use to generate the htlc sigs.
|
||||
repeated bytes htlc_server_nonces = 2;
|
||||
}
|
||||
|
||||
message PushHtlcSigRequest {
|
||||
// The hash of the swap invoice.
|
||||
bytes swap_hash = 1;
|
||||
|
||||
// The sigs that the client generated for the reservation inputs.
|
||||
repeated bytes client_sigs = 2;
|
||||
}
|
||||
|
||||
message PushHtlcSigResponse {
|
||||
// The sigs that the server generated for the reservation inputs.
|
||||
repeated bytes server_sigs = 1;
|
||||
}
|
||||
|
||||
message PushPreimageRequest {
|
||||
// The preimage that the client generated for the swap.
|
||||
bytes preimage = 1;
|
||||
|
||||
// The nonces that the client used to generate the sweepless sweep sigs.
|
||||
repeated bytes client_nonces = 2;
|
||||
|
||||
// The address that the client wants to sweep the htlc to.
|
||||
string client_sweep_addr = 3;
|
||||
|
||||
// The fee rate in sat/kw that the client wants to use for the sweep.
|
||||
uint64 musig_tx_fee_rate = 4;
|
||||
}
|
||||
|
||||
message PushPreimageResponse {
|
||||
// The sweep sigs that the server generated for the htlc.
|
||||
repeated bytes musig2_sweep_sigs = 1;
|
||||
|
||||
// The nonces that the server used to generate the sweepless sweep sigs.
|
||||
repeated bytes server_nonces = 2;
|
||||
}
|
||||
|
||||
message CancelInstantSwapRequest {
|
||||
// The hash of the swap invoice.
|
||||
bytes swap_hash = 1;
|
||||
}
|
||||
|
||||
message CancelInstantSwapResponse {
|
||||
}
|
||||
|
||||
enum InstantOutProtocolVersion {
|
||||
INSTANTOUT_NONE = 0;
|
||||
INSTANTOUT_FULL_RESERVATION = 1;
|
||||
};
|
@ -0,0 +1,301 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
|
||||
package swapserverrpc
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.32.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion7
|
||||
|
||||
// InstantSwapServerClient is the client API for InstantSwapServer service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type InstantSwapServerClient interface {
|
||||
// RequestInstantLoopOut initiates an instant loop out swap.
|
||||
RequestInstantLoopOut(ctx context.Context, in *InstantLoopOutRequest, opts ...grpc.CallOption) (*InstantLoopOutResponse, error)
|
||||
// PollPaymentAccepted polls the server to see if the payment has been
|
||||
// accepted.
|
||||
PollPaymentAccepted(ctx context.Context, in *PollPaymentAcceptedRequest, opts ...grpc.CallOption) (*PollPaymentAcceptedResponse, error)
|
||||
// InitHtlcSig is called by the client to initiate the htlc sig exchange.
|
||||
InitHtlcSig(ctx context.Context, in *InitHtlcSigRequest, opts ...grpc.CallOption) (*InitHtlcSigResponse, error)
|
||||
// PushHtlcSig is called by the client to push the htlc sigs to the server.
|
||||
PushHtlcSig(ctx context.Context, in *PushHtlcSigRequest, opts ...grpc.CallOption) (*PushHtlcSigResponse, error)
|
||||
// PushPreimage is called by the client to push the preimage to the server.
|
||||
// This returns the musig2 signatures that the client needs to sweep the
|
||||
// htlc.
|
||||
PushPreimage(ctx context.Context, in *PushPreimageRequest, opts ...grpc.CallOption) (*PushPreimageResponse, error)
|
||||
// CancelInstantSwap tries to cancel the instant swap. This can only be
|
||||
// called if the swap has not been accepted yet.
|
||||
CancelInstantSwap(ctx context.Context, in *CancelInstantSwapRequest, opts ...grpc.CallOption) (*CancelInstantSwapResponse, error)
|
||||
}
|
||||
|
||||
type instantSwapServerClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewInstantSwapServerClient(cc grpc.ClientConnInterface) InstantSwapServerClient {
|
||||
return &instantSwapServerClient{cc}
|
||||
}
|
||||
|
||||
func (c *instantSwapServerClient) RequestInstantLoopOut(ctx context.Context, in *InstantLoopOutRequest, opts ...grpc.CallOption) (*InstantLoopOutResponse, error) {
|
||||
out := new(InstantLoopOutResponse)
|
||||
err := c.cc.Invoke(ctx, "/looprpc.InstantSwapServer/RequestInstantLoopOut", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *instantSwapServerClient) PollPaymentAccepted(ctx context.Context, in *PollPaymentAcceptedRequest, opts ...grpc.CallOption) (*PollPaymentAcceptedResponse, error) {
|
||||
out := new(PollPaymentAcceptedResponse)
|
||||
err := c.cc.Invoke(ctx, "/looprpc.InstantSwapServer/PollPaymentAccepted", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *instantSwapServerClient) InitHtlcSig(ctx context.Context, in *InitHtlcSigRequest, opts ...grpc.CallOption) (*InitHtlcSigResponse, error) {
|
||||
out := new(InitHtlcSigResponse)
|
||||
err := c.cc.Invoke(ctx, "/looprpc.InstantSwapServer/InitHtlcSig", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *instantSwapServerClient) PushHtlcSig(ctx context.Context, in *PushHtlcSigRequest, opts ...grpc.CallOption) (*PushHtlcSigResponse, error) {
|
||||
out := new(PushHtlcSigResponse)
|
||||
err := c.cc.Invoke(ctx, "/looprpc.InstantSwapServer/PushHtlcSig", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *instantSwapServerClient) PushPreimage(ctx context.Context, in *PushPreimageRequest, opts ...grpc.CallOption) (*PushPreimageResponse, error) {
|
||||
out := new(PushPreimageResponse)
|
||||
err := c.cc.Invoke(ctx, "/looprpc.InstantSwapServer/PushPreimage", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *instantSwapServerClient) CancelInstantSwap(ctx context.Context, in *CancelInstantSwapRequest, opts ...grpc.CallOption) (*CancelInstantSwapResponse, error) {
|
||||
out := new(CancelInstantSwapResponse)
|
||||
err := c.cc.Invoke(ctx, "/looprpc.InstantSwapServer/CancelInstantSwap", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// InstantSwapServerServer is the server API for InstantSwapServer service.
|
||||
// All implementations must embed UnimplementedInstantSwapServerServer
|
||||
// for forward compatibility
|
||||
type InstantSwapServerServer interface {
|
||||
// RequestInstantLoopOut initiates an instant loop out swap.
|
||||
RequestInstantLoopOut(context.Context, *InstantLoopOutRequest) (*InstantLoopOutResponse, error)
|
||||
// PollPaymentAccepted polls the server to see if the payment has been
|
||||
// accepted.
|
||||
PollPaymentAccepted(context.Context, *PollPaymentAcceptedRequest) (*PollPaymentAcceptedResponse, error)
|
||||
// InitHtlcSig is called by the client to initiate the htlc sig exchange.
|
||||
InitHtlcSig(context.Context, *InitHtlcSigRequest) (*InitHtlcSigResponse, error)
|
||||
// PushHtlcSig is called by the client to push the htlc sigs to the server.
|
||||
PushHtlcSig(context.Context, *PushHtlcSigRequest) (*PushHtlcSigResponse, error)
|
||||
// PushPreimage is called by the client to push the preimage to the server.
|
||||
// This returns the musig2 signatures that the client needs to sweep the
|
||||
// htlc.
|
||||
PushPreimage(context.Context, *PushPreimageRequest) (*PushPreimageResponse, error)
|
||||
// CancelInstantSwap tries to cancel the instant swap. This can only be
|
||||
// called if the swap has not been accepted yet.
|
||||
CancelInstantSwap(context.Context, *CancelInstantSwapRequest) (*CancelInstantSwapResponse, error)
|
||||
mustEmbedUnimplementedInstantSwapServerServer()
|
||||
}
|
||||
|
||||
// UnimplementedInstantSwapServerServer must be embedded to have forward compatible implementations.
|
||||
type UnimplementedInstantSwapServerServer struct {
|
||||
}
|
||||
|
||||
func (UnimplementedInstantSwapServerServer) RequestInstantLoopOut(context.Context, *InstantLoopOutRequest) (*InstantLoopOutResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RequestInstantLoopOut not implemented")
|
||||
}
|
||||
func (UnimplementedInstantSwapServerServer) PollPaymentAccepted(context.Context, *PollPaymentAcceptedRequest) (*PollPaymentAcceptedResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method PollPaymentAccepted not implemented")
|
||||
}
|
||||
func (UnimplementedInstantSwapServerServer) InitHtlcSig(context.Context, *InitHtlcSigRequest) (*InitHtlcSigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method InitHtlcSig not implemented")
|
||||
}
|
||||
func (UnimplementedInstantSwapServerServer) PushHtlcSig(context.Context, *PushHtlcSigRequest) (*PushHtlcSigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method PushHtlcSig not implemented")
|
||||
}
|
||||
func (UnimplementedInstantSwapServerServer) PushPreimage(context.Context, *PushPreimageRequest) (*PushPreimageResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method PushPreimage not implemented")
|
||||
}
|
||||
func (UnimplementedInstantSwapServerServer) CancelInstantSwap(context.Context, *CancelInstantSwapRequest) (*CancelInstantSwapResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CancelInstantSwap not implemented")
|
||||
}
|
||||
func (UnimplementedInstantSwapServerServer) mustEmbedUnimplementedInstantSwapServerServer() {}
|
||||
|
||||
// UnsafeInstantSwapServerServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to InstantSwapServerServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeInstantSwapServerServer interface {
|
||||
mustEmbedUnimplementedInstantSwapServerServer()
|
||||
}
|
||||
|
||||
func RegisterInstantSwapServerServer(s grpc.ServiceRegistrar, srv InstantSwapServerServer) {
|
||||
s.RegisterService(&InstantSwapServer_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _InstantSwapServer_RequestInstantLoopOut_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(InstantLoopOutRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(InstantSwapServerServer).RequestInstantLoopOut(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/looprpc.InstantSwapServer/RequestInstantLoopOut",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(InstantSwapServerServer).RequestInstantLoopOut(ctx, req.(*InstantLoopOutRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _InstantSwapServer_PollPaymentAccepted_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(PollPaymentAcceptedRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(InstantSwapServerServer).PollPaymentAccepted(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/looprpc.InstantSwapServer/PollPaymentAccepted",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(InstantSwapServerServer).PollPaymentAccepted(ctx, req.(*PollPaymentAcceptedRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _InstantSwapServer_InitHtlcSig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(InitHtlcSigRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(InstantSwapServerServer).InitHtlcSig(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/looprpc.InstantSwapServer/InitHtlcSig",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(InstantSwapServerServer).InitHtlcSig(ctx, req.(*InitHtlcSigRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _InstantSwapServer_PushHtlcSig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(PushHtlcSigRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(InstantSwapServerServer).PushHtlcSig(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/looprpc.InstantSwapServer/PushHtlcSig",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(InstantSwapServerServer).PushHtlcSig(ctx, req.(*PushHtlcSigRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _InstantSwapServer_PushPreimage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(PushPreimageRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(InstantSwapServerServer).PushPreimage(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/looprpc.InstantSwapServer/PushPreimage",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(InstantSwapServerServer).PushPreimage(ctx, req.(*PushPreimageRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _InstantSwapServer_CancelInstantSwap_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(CancelInstantSwapRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(InstantSwapServerServer).CancelInstantSwap(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/looprpc.InstantSwapServer/CancelInstantSwap",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(InstantSwapServerServer).CancelInstantSwap(ctx, req.(*CancelInstantSwapRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// InstantSwapServer_ServiceDesc is the grpc.ServiceDesc for InstantSwapServer service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var InstantSwapServer_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "looprpc.InstantSwapServer",
|
||||
HandlerType: (*InstantSwapServerServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "RequestInstantLoopOut",
|
||||
Handler: _InstantSwapServer_RequestInstantLoopOut_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "PollPaymentAccepted",
|
||||
Handler: _InstantSwapServer_PollPaymentAccepted_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "InitHtlcSig",
|
||||
Handler: _InstantSwapServer_InitHtlcSig_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "PushHtlcSig",
|
||||
Handler: _InstantSwapServer_PushHtlcSig_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "PushPreimage",
|
||||
Handler: _InstantSwapServer_PushPreimage_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "CancelInstantSwap",
|
||||
Handler: _InstantSwapServer_CancelInstantSwap_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "instantout.proto",
|
||||
}
|
Loading…
Reference in New Issue