mirror of https://github.com/lightninglabs/loop
instantout: add fsm and actions
parent
25faa48ad1
commit
c6da670a4f
@ -0,0 +1,531 @@
|
||||
package instantout
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightninglabs/loop/fsm"
|
||||
"github.com/lightninglabs/loop/instantout/reservation"
|
||||
"github.com/lightninglabs/loop/loopdb"
|
||||
loop_rpc "github.com/lightninglabs/loop/swapserverrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInsufficientReservationBalance = errors.New(
|
||||
"insufficient balance in reservations",
|
||||
)
|
||||
|
||||
ErrReservationsMustBeSwapAmount = errors.New(
|
||||
"reservations total amount must be equal swap amount",
|
||||
)
|
||||
|
||||
UrgentConfTarget = int32(3)
|
||||
NormalConfTarget = int32(6)
|
||||
)
|
||||
|
||||
// InitInstantOutCtx contains the context for the InitInstantOutAction.
|
||||
type InitInstantOutCtx struct {
|
||||
cltvExpiry int32
|
||||
reservations []reservation.ID
|
||||
initationHeight int32
|
||||
outgoingChanSet loopdb.ChannelSet
|
||||
}
|
||||
|
||||
// InitInstantOutAction is the first action that is executed when the instant
|
||||
// out FSM is started. It will send the instant out request to the server.
|
||||
func (f *FSM) InitInstantOutAction(eventCtx fsm.EventContext) fsm.EventType {
|
||||
initCtx, ok := eventCtx.(*InitInstantOutCtx)
|
||||
if !ok {
|
||||
return f.HandleError(fsm.ErrInvalidContextType)
|
||||
}
|
||||
|
||||
if len(initCtx.reservations) == 0 {
|
||||
return f.HandleError(fmt.Errorf("no reservations provided"))
|
||||
}
|
||||
|
||||
var (
|
||||
reservationAmt uint64
|
||||
reservationIds = make([][]byte, 0, len(initCtx.reservations))
|
||||
reservations = make(
|
||||
[]*reservation.Reservation, 0, len(initCtx.reservations),
|
||||
)
|
||||
)
|
||||
|
||||
// The requested amount needs to be full reservation amounts.
|
||||
for _, reservationId := range initCtx.reservations {
|
||||
res, err := f.cfg.ReservationManager.GetReservation(
|
||||
f.ctx, reservationId,
|
||||
)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
// Check if the reservation is locked.
|
||||
if res.State == reservation.Locked {
|
||||
return f.HandleError(fmt.Errorf("reservation %v is "+
|
||||
"locked", reservationId))
|
||||
}
|
||||
|
||||
reservationAmt += uint64(res.Value)
|
||||
reservationIds = append(reservationIds, reservationId[:])
|
||||
reservations = append(reservations, res)
|
||||
}
|
||||
|
||||
// Create the preimage for the swap.
|
||||
var preimage lntypes.Preimage
|
||||
if _, err := rand.Read(preimage[:]); err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
// Create the keys for the swap.
|
||||
keyRes, err := f.cfg.Wallet.DeriveNextKey(f.ctx, KeyFamily)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
swapHash := preimage.Hash()
|
||||
|
||||
// Create a high fee rate so that the htlc will be confirmed quickly.
|
||||
feeRate, err := f.cfg.Wallet.EstimateFeeRate(f.ctx, UrgentConfTarget)
|
||||
if err != nil {
|
||||
f.Infof("error estimating fee rate: %v", err)
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
// Send the instantout request to the server.
|
||||
instantOutResponse, err := f.cfg.InstantOutClient.RequestInstantLoopOut(
|
||||
f.ctx,
|
||||
&loop_rpc.InstantLoopOutRequest{
|
||||
Amt: reservationAmt,
|
||||
ReceiverKey: keyRes.PubKey.SerializeCompressed(),
|
||||
SwapHash: swapHash[:],
|
||||
Expiry: initCtx.cltvExpiry,
|
||||
HtlcFeeRate: uint64(feeRate),
|
||||
ReservationIds: reservationIds,
|
||||
ProtocolVersion: loop_rpc.
|
||||
InstantOutProtocolVersion_INSTANTOUT_FULL_RESERVATION,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
// Decode the invoice to check if the hash is valid.
|
||||
payReq, err := f.cfg.LndClient.DecodePaymentRequest(
|
||||
f.ctx, instantOutResponse.SwapInvoice,
|
||||
)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
if preimage.Hash() != payReq.Hash {
|
||||
return f.HandleError(fmt.Errorf("invalid swap invoice hash: "+
|
||||
"expected %x got %x", preimage.Hash(), payReq.Hash))
|
||||
}
|
||||
serverPubkey, err := btcec.ParsePubKey(instantOutResponse.SenderKey)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
// Create the address that we'll send the funds to
|
||||
sweepAddress, err := f.cfg.Wallet.NextAddr(
|
||||
f.ctx, "", walletrpc.AddressType_TAPROOT_PUBKEY, false,
|
||||
)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
// Now we can create the instant out.
|
||||
instantOut := &InstantOut{
|
||||
SwapHash: swapHash,
|
||||
SwapPreimage: preimage,
|
||||
ProtocolVersion: ProtocolVersionFullReservation,
|
||||
InitiationHeight: initCtx.initationHeight,
|
||||
OutgoingChanSet: initCtx.outgoingChanSet,
|
||||
CltvExpiry: initCtx.cltvExpiry,
|
||||
ClientPubkey: keyRes.PubKey,
|
||||
ServerPubkey: serverPubkey,
|
||||
Value: btcutil.Amount(reservationAmt),
|
||||
HtlcFeeRate: feeRate,
|
||||
SwapInvoice: instantOutResponse.SwapInvoice,
|
||||
Reservations: reservations,
|
||||
KeyLocator: keyRes.KeyLocator,
|
||||
SweepAddress: sweepAddress,
|
||||
}
|
||||
|
||||
err = f.cfg.Store.CreateInstantLoopOut(f.ctx, instantOut)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
f.InstantOut = instantOut
|
||||
|
||||
return OnInit
|
||||
}
|
||||
|
||||
// PollPaymentAcceptedAction locks the reservations, sends the payment to the
|
||||
// server and polls the server for the payment status.
|
||||
func (f *FSM) PollPaymentAcceptedAction(
|
||||
eventCtx fsm.EventContext) fsm.EventType {
|
||||
|
||||
// Now that we're doing the swap, we first lock the reservations
|
||||
// so that they can't be used for other swaps.
|
||||
for _, reservation := range f.InstantOut.Reservations {
|
||||
err := f.cfg.ReservationManager.LockReservation(
|
||||
f.ctx, reservation.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return f.handleErrorAndUnlockReservations(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Now we send the payment to the server.
|
||||
payChan, paymentErrChan, err := f.cfg.RouterClient.SendPayment(
|
||||
f.ctx,
|
||||
lndclient.SendPaymentRequest{
|
||||
Invoice: f.InstantOut.SwapInvoice,
|
||||
Timeout: time.Minute * 5,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return f.handleErrorAndUnlockReservations(err)
|
||||
}
|
||||
|
||||
// We'll continuously poll the server for the payment status.
|
||||
for {
|
||||
select {
|
||||
case payRes := <-payChan:
|
||||
f.Debugf("payment result: %v", payRes)
|
||||
if payRes.FailureReason !=
|
||||
lnrpc.PaymentFailureReason_FAILURE_REASON_NONE {
|
||||
|
||||
return f.handleErrorAndUnlockReservations(
|
||||
fmt.Errorf("payment failed: %v",
|
||||
payRes.FailureReason),
|
||||
)
|
||||
}
|
||||
|
||||
case err := <-paymentErrChan:
|
||||
return f.handleErrorAndUnlockReservations(err)
|
||||
|
||||
case <-f.ctx.Done():
|
||||
return fsm.NoOp
|
||||
|
||||
default:
|
||||
res, err := f.cfg.InstantOutClient.PollPaymentAccepted(
|
||||
f.ctx, &loop_rpc.PollPaymentAcceptedRequest{
|
||||
SwapHash: f.InstantOut.SwapHash[:],
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return f.handleErrorAndUnlockReservations(err)
|
||||
}
|
||||
if res.Accepted {
|
||||
return OnPaymentAccepted
|
||||
}
|
||||
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BuildHTLCAction creates the htlc transaction, exchanges nonces with
|
||||
// the server and sends the htlc signatures to the server.
|
||||
func (f *FSM) BuildHTLCAction(eventCtx fsm.EventContext) fsm.EventType {
|
||||
htlcSessions, htlcClientNonces, err := f.InstantOut.createMusig2Session(
|
||||
f.ctx, f.cfg.Signer,
|
||||
)
|
||||
if err != nil {
|
||||
return f.handleErrorAndUnlockReservations(err)
|
||||
}
|
||||
|
||||
f.htlcMusig2Sessions = htlcSessions
|
||||
|
||||
// Send the server the client nonces.
|
||||
htlcInitRes, err := f.cfg.InstantOutClient.InitHtlcSig(
|
||||
f.ctx,
|
||||
&loop_rpc.InitHtlcSigRequest{
|
||||
SwapHash: f.InstantOut.SwapHash[:],
|
||||
HtlcClientNonces: htlcClientNonces,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return f.handleErrorAndUnlockReservations(err)
|
||||
}
|
||||
|
||||
if len(htlcInitRes.HtlcServerNonces) != len(f.InstantOut.Reservations) {
|
||||
return f.handleErrorAndUnlockReservations(
|
||||
errors.New("invalid number of server nonces"),
|
||||
)
|
||||
}
|
||||
|
||||
htlcServerNonces, err := toNonces(htlcInitRes.HtlcServerNonces)
|
||||
if err != nil {
|
||||
return f.handleErrorAndUnlockReservations(err)
|
||||
}
|
||||
|
||||
// Now that our nonces are set, we can create and sign the htlc
|
||||
// transaction.
|
||||
htlcTx, err := f.InstantOut.createHtlcTransaction(f.cfg.Network)
|
||||
if err != nil {
|
||||
return f.handleErrorAndUnlockReservations(err)
|
||||
}
|
||||
|
||||
// Next we'll get our sweep tx signatures.
|
||||
htlcSigs, err := f.InstantOut.signMusig2Tx(
|
||||
f.ctx, f.cfg.Signer, htlcTx, f.htlcMusig2Sessions,
|
||||
htlcServerNonces,
|
||||
)
|
||||
if err != nil {
|
||||
return f.handleErrorAndUnlockReservations(err)
|
||||
}
|
||||
|
||||
// Send the server the htlc signatures.
|
||||
htlcRes, err := f.cfg.InstantOutClient.PushHtlcSig(
|
||||
f.ctx,
|
||||
&loop_rpc.PushHtlcSigRequest{
|
||||
SwapHash: f.InstantOut.SwapHash[:],
|
||||
ClientSigs: htlcSigs,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return f.handleErrorAndUnlockReservations(err)
|
||||
}
|
||||
|
||||
// We can now finalize the htlc transaction.
|
||||
htlcTx, err = f.InstantOut.finalizeMusig2Transaction(
|
||||
f.ctx, f.cfg.Signer, f.htlcMusig2Sessions, htlcTx,
|
||||
htlcRes.ServerSigs,
|
||||
)
|
||||
if err != nil {
|
||||
return f.handleErrorAndUnlockReservations(err)
|
||||
}
|
||||
|
||||
f.InstantOut.FinalizedHtlcTx = htlcTx
|
||||
|
||||
return OnHtlcSigReceived
|
||||
}
|
||||
|
||||
// PushPreimageAction pushes the preimage to the server. It also creates the
|
||||
// sweepless sweep transaction and sends the signatures to the server. Finally,
|
||||
// it publishes the sweepless sweep transaction. If any of the steps after
|
||||
// pushing the preimage fail, the htlc timeout transaction will be published.
|
||||
func (f *FSM) PushPreimageAction(eventCtx fsm.EventContext) fsm.EventType {
|
||||
// First we'll create the musig2 context.
|
||||
coopSessions, coopClientNonces, err := f.InstantOut.createMusig2Session(
|
||||
f.ctx, f.cfg.Signer,
|
||||
)
|
||||
if err != nil {
|
||||
return f.handleErrorAndUnlockReservations(err)
|
||||
}
|
||||
|
||||
f.sweeplessSweepSessions = coopSessions
|
||||
|
||||
// Get the feerate for the coop sweep.
|
||||
feeRate, err := f.cfg.Wallet.EstimateFeeRate(f.ctx, NormalConfTarget)
|
||||
if err != nil {
|
||||
return f.handleErrorAndUnlockReservations(err)
|
||||
}
|
||||
|
||||
pushPreImageRes, err := f.cfg.InstantOutClient.PushPreimage(
|
||||
f.ctx,
|
||||
&loop_rpc.PushPreimageRequest{
|
||||
Preimage: f.InstantOut.SwapPreimage[:],
|
||||
ClientNonces: coopClientNonces,
|
||||
ClientSweepAddr: f.InstantOut.SweepAddress.String(),
|
||||
MusigTxFeeRate: uint64(feeRate),
|
||||
},
|
||||
)
|
||||
// Now that we have revealed the preimage, if any following step fail,
|
||||
// we'll need to publish the htlc timeout tx.
|
||||
if err != nil {
|
||||
f.LastActionError = err
|
||||
return OnErrorPublishHtlc
|
||||
}
|
||||
|
||||
// Now that we have the sweepless sweep signatures we can build and
|
||||
// publish the sweepless sweep transaction.
|
||||
sweepTx, err := f.InstantOut.createSweeplessSweepTx(feeRate)
|
||||
if err != nil {
|
||||
f.LastActionError = err
|
||||
return OnErrorPublishHtlc
|
||||
}
|
||||
|
||||
coopServerNonces, err := toNonces(pushPreImageRes.ServerNonces)
|
||||
if err != nil {
|
||||
f.LastActionError = err
|
||||
return OnErrorPublishHtlc
|
||||
}
|
||||
|
||||
// Next we'll get our sweep tx signatures.
|
||||
_, err = f.InstantOut.signMusig2Tx(
|
||||
f.ctx, f.cfg.Signer, sweepTx, f.sweeplessSweepSessions,
|
||||
coopServerNonces,
|
||||
)
|
||||
if err != nil {
|
||||
f.LastActionError = err
|
||||
return OnErrorPublishHtlc
|
||||
}
|
||||
|
||||
// Now we'll finalize the sweepless sweep transaction.
|
||||
sweepTx, err = f.InstantOut.finalizeMusig2Transaction(
|
||||
f.ctx, f.cfg.Signer, f.sweeplessSweepSessions, sweepTx,
|
||||
pushPreImageRes.Musig2SweepSigs,
|
||||
)
|
||||
if err != nil {
|
||||
f.LastActionError = err
|
||||
return OnErrorPublishHtlc
|
||||
}
|
||||
|
||||
txLabel := fmt.Sprintf("sweepless-sweep-%v",
|
||||
f.InstantOut.SwapPreimage.Hash())
|
||||
|
||||
// Publish the sweepless sweep transaction.
|
||||
err = f.cfg.Wallet.PublishTransaction(f.ctx, sweepTx, txLabel)
|
||||
if err != nil {
|
||||
f.LastActionError = err
|
||||
return OnErrorPublishHtlc
|
||||
}
|
||||
|
||||
f.InstantOut.FinalizedSweeplessSweepTx = sweepTx
|
||||
txHash := f.InstantOut.FinalizedSweeplessSweepTx.TxHash()
|
||||
|
||||
f.InstantOut.SweepTxHash = &txHash
|
||||
|
||||
return OnSweeplessSweepPublished
|
||||
}
|
||||
|
||||
// WaitForSweeplessSweepConfirmedAction waits for the sweepless sweep
|
||||
// transaction to be confirmed.
|
||||
func (f *FSM) WaitForSweeplessSweepConfirmedAction(
|
||||
eventCtx fsm.EventContext) fsm.EventType {
|
||||
|
||||
pkscript, err := txscript.PayToAddrScript(f.InstantOut.SweepAddress)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
confChan, confErrChan, err := f.cfg.ChainNotifier.
|
||||
RegisterConfirmationsNtfn(
|
||||
f.ctx, f.InstantOut.SweepTxHash, pkscript,
|
||||
1, f.InstantOut.InitiationHeight,
|
||||
)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case spendErr := <-confErrChan:
|
||||
f.LastActionError = spendErr
|
||||
f.Errorf("error listening for sweepless sweep "+
|
||||
"confirmation: %v", spendErr)
|
||||
|
||||
return OnErrorPublishHtlc
|
||||
|
||||
case conf := <-confChan:
|
||||
f.InstantOut.
|
||||
SweepConfirmationHeight = conf.BlockHeight
|
||||
|
||||
return OnSweeplessSweepConfirmed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PublishHtlcAction publishes the htlc transaction and the htlc sweep
|
||||
// transaction.
|
||||
func (f *FSM) PublishHtlcAction(eventCtx fsm.EventContext) fsm.EventType {
|
||||
// Publish the htlc transaction.
|
||||
err := f.cfg.Wallet.PublishTransaction(
|
||||
f.ctx, f.InstantOut.FinalizedHtlcTx,
|
||||
fmt.Sprintf("htlc-%v", f.InstantOut.SwapPreimage.Hash()),
|
||||
)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
// Create a feerate that will confirm the htlc quickly.
|
||||
feeRate, err := f.cfg.Wallet.EstimateFeeRate(f.ctx, UrgentConfTarget)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
// We can immediately publish the htlc sweep transaction
|
||||
htlcSweepTx, err := f.InstantOut.generateHtlcSweepTx(
|
||||
f.ctx, f.cfg.Signer, feeRate, f.cfg.Network,
|
||||
)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
txLabel := fmt.Sprintf("htlc-sweep-%v",
|
||||
f.InstantOut.SwapPreimage.Hash())
|
||||
|
||||
err = f.cfg.Wallet.PublishTransaction(f.ctx, htlcSweepTx, txLabel)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
sweepTxHash := htlcSweepTx.TxHash()
|
||||
|
||||
f.InstantOut.SweepTxHash = &sweepTxHash
|
||||
|
||||
return OnHtlcSweepPublished
|
||||
}
|
||||
|
||||
// WaitForHtlcSweepConfirmedAction waits for the htlc sweep transaction to be
|
||||
// confirmed.
|
||||
func (f *FSM) WaitForHtlcSweepConfirmedAction(
|
||||
eventCtx fsm.EventContext) fsm.EventType {
|
||||
|
||||
htlc, err := f.InstantOut.getHtlc(f.cfg.Network)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
confChan, confErrChan, err := f.cfg.ChainNotifier.RegisterConfirmationsNtfn(
|
||||
f.ctx, f.InstantOut.SweepTxHash, htlc.PkScript,
|
||||
1, f.InstantOut.InitiationHeight,
|
||||
)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case spendErr := <-confErrChan:
|
||||
f.LastActionError = spendErr
|
||||
return OnErrorPublishHtlc
|
||||
|
||||
case conf := <-confChan:
|
||||
f.InstantOut.
|
||||
SweepConfirmationHeight = conf.BlockHeight
|
||||
|
||||
return OnHtlcSwept
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleErrorAndUnlockReservations handles an error and unlocks the
|
||||
// reservations.
|
||||
func (f *FSM) handleErrorAndUnlockReservations(err error) fsm.EventType {
|
||||
// Unlock the reservations.
|
||||
for _, reservation := range f.InstantOut.Reservations {
|
||||
err := f.cfg.ReservationManager.UnlockReservation(
|
||||
f.ctx, reservation.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
}
|
||||
|
||||
return f.HandleError(err)
|
||||
}
|
@ -0,0 +1,360 @@
|
||||
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 ProtocolVersion = 0
|
||||
ProtocolVersionFullReservation ProtocolVersion = 1
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
// 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("InstantFailedOutFailed")
|
||||
)
|
||||
|
||||
// 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")
|
||||
|
||||
// OnHtlcSweepPublished is the event that is triggered when the htlc
|
||||
// sweep is published.
|
||||
OnHtlcSweepPublished = fsm.EventType("OnHtlcBroadcasted")
|
||||
|
||||
// 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,
|
||||
OnHtlcSweepPublished: WaitForHtlcSweepConfirmed,
|
||||
},
|
||||
Action: f.PublishHtlcAction,
|
||||
},
|
||||
WaitForHtlcSweepConfirmed: fsm.State{
|
||||
Transitions: fsm.Transitions{
|
||||
OnHtlcSwept: FinishedHtlcPreimageSweep,
|
||||
OnRecover: WaitForHtlcSweepConfirmed,
|
||||
fsm.OnError: FailedHtlcSweep,
|
||||
},
|
||||
Action: f.WaitForHtlcSweepConfirmedAction,
|
||||
},
|
||||
FailedHtlcSweep: fsm.State{
|
||||
Action: fsm.NoOpAction,
|
||||
},
|
||||
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("Current: %v, Event: %v", notification.NextState,
|
||||
notification.Event)
|
||||
|
||||
// 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 {
|
||||
return
|
||||
}
|
||||
|
||||
err := f.cfg.Store.UpdateInstantLoopOut(f.ctx, f.InstantOut)
|
||||
if err != nil {
|
||||
log.Errorf("Error updating reservation: %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, FailedHtlcSweep,
|
||||
FinishedSweeplessSweep:
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
@ -0,0 +1,478 @@
|
||||
package instantout
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"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 i, reservation := range i.Reservations {
|
||||
pkScript, err := reservation.GetPkScript()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inputs[i] = 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) {
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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 !equalOutpoints(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
|
||||
}
|
||||
|
||||
// sweeplessSweepOutpoint returns the outpoint of the reservation.
|
||||
func (i *InstantOut) generateHtlcSweepTx(ctx context.Context,
|
||||
signer lndclient.SignerClient,
|
||||
feeRate chainfee.SatPerKWeight, network *chaincfg.Params) (
|
||||
*wire.MsgTx, error) {
|
||||
|
||||
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)
|
||||
|
||||
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,
|
||||
})
|
||||
|
||||
// Add the sweep output.
|
||||
sweepPkScript, err := txscript.PayToAddrScript(i.SweepAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fee := feeRate.FeeForWeight(int64(weightEstimator.Weight()))
|
||||
|
||||
output := &wire.TxOut{
|
||||
Value: int64(i.Value) - int64(fee),
|
||||
PkScript: sweepPkScript,
|
||||
}
|
||||
|
||||
sweepTx.AddTxOut(output)
|
||||
|
||||
signDesc := lndclient.SignDescriptor{
|
||||
WitnessScript: htlc.SuccessScript(),
|
||||
Output: output,
|
||||
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("signing: %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 _, nonce := range nonces {
|
||||
nonce, err := byteSliceTo66ByteSlice(nonce)
|
||||
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
|
||||
}
|
||||
|
||||
// equalOutpoints returns true if the outpoints are equal.
|
||||
func equalOutpoints(txOutpoint, reservationOutpoint wire.OutPoint) bool {
|
||||
if txOutpoint.Hash != reservationOutpoint.Hash &&
|
||||
txOutpoint.Index != reservationOutpoint.Index {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
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 (
|
||||
// TODO(sputn1ck): decide on actual value.
|
||||
KeyFamily = int32(42069)
|
||||
)
|
||||
|
||||
// InstantLoopOutStore is the interface that needs to be implemented by a
|
||||
// store that wants to be used by the instant loop out manager.
|
||||
type InstantLoopOutStore interface {
|
||||
// CreateInstantLoopOut adds a new instant loop out to the store.
|
||||
CreateInstantLoopOut(ctx context.Context, instantOut *InstantOut) error
|
||||
|
||||
// UpdateInstantLoopOut updates an existing instant loop out in the
|
||||
// store.
|
||||
UpdateInstantLoopOut(ctx context.Context, instantOut *InstantOut) error
|
||||
|
||||
// GetInstantLoopOut returns the instant loop out for the given swap
|
||||
// hash.
|
||||
GetInstantLoopOut(ctx context.Context,
|
||||
swapHash []byte) (*InstantOut, error)
|
||||
|
||||
// ListInstantLoopOuts returns all instant loop outs that are in the
|
||||
// store.
|
||||
ListInstantLoopOuts(ctx context.Context) ([]*InstantOut, error)
|
||||
}
|
||||
|
||||
// ReservationManager handles fetching and locking of reservations.
|
||||
type ReservationManager interface {
|
||||
// GetReservation returns the reservation for the given id.
|
||||
GetReservation(ctx context.Context, id reservation.ID) (
|
||||
*reservation.Reservation, error)
|
||||
|
||||
// LockReservation locks the reservation for the given id.
|
||||
LockReservation(ctx context.Context, id reservation.ID) error
|
||||
|
||||
// UnlockReservation unlocks the reservation for the given id.
|
||||
UnlockReservation(ctx context.Context, id reservation.ID) error
|
||||
}
|
||||
|
||||
// InputReservations is a helper struct for the input reservations.
|
||||
type InputReservations []InputReservation
|
||||
|
||||
// InputReservation is a helper struct for the input reservation.
|
||||
type InputReservation struct {
|
||||
Outpoint wire.OutPoint
|
||||
Value btcutil.Amount
|
||||
PkScript []byte
|
||||
}
|
||||
|
||||
// Output returns the output for the input reservation.
|
||||
func (r InputReservation) Output() *wire.TxOut {
|
||||
return wire.NewTxOut(int64(r.Value), r.PkScript)
|
||||
}
|
||||
|
||||
// GetPrevoutFetcher returns a prevout fetcher for the input reservations.
|
||||
func (i InputReservations) GetPrevoutFetcher() txscript.PrevOutputFetcher {
|
||||
prevOuts := make(map[wire.OutPoint]*wire.TxOut)
|
||||
|
||||
// add the reservation inputs
|
||||
for _, reservation := range i {
|
||||
prevOuts[reservation.Outpoint] = reservation.Output()
|
||||
}
|
||||
|
||||
return txscript.NewMultiPrevOutFetcher(prevOuts)
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package instantout
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btclog"
|
||||
"github.com/lightningnetwork/lnd/build"
|
||||
)
|
||||
|
||||
// Subsystem defines the sub system name of this package.
|
||||
const Subsystem = "INST"
|
||||
|
||||
// log is a logger that is initialized with no output filters. This
|
||||
// means the package will not perform any logging by default until the caller
|
||||
// requests it.
|
||||
var log btclog.Logger
|
||||
|
||||
// The default amount of logging is none.
|
||||
func init() {
|
||||
UseLogger(build.NewSubLogger(Subsystem, nil))
|
||||
}
|
||||
|
||||
// UseLogger uses a specified Logger to output package logging info.
|
||||
// This should be used in preference to SetLogWriter if the caller is also
|
||||
// using btclog.
|
||||
func UseLogger(logger btclog.Logger) {
|
||||
log = logger
|
||||
}
|
Loading…
Reference in New Issue