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
|
#!/usr/bin/env bash
|
||||||
go run ./fsm/stateparser/stateparser.go --out ./fsm/example_fsm.md --fsm example
|
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 ./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