From 56ed6f7ccbbfe7ff1b1e4ac5c061eddb7bfb4b65 Mon Sep 17 00:00:00 2001 From: sputn1ck Date: Mon, 5 Feb 2024 16:13:46 +0100 Subject: [PATCH] instantout: add fsm and actions --- instantout/actions.go | 615 ++++++++++++++++++++++++++++++ instantout/fsm.go | 401 +++++++++++++++++++ instantout/instantout.go | 488 ++++++++++++++++++++++++ instantout/interfaces.go | 73 ++++ instantout/log.go | 26 ++ instantout/reservation/actions.go | 2 +- 6 files changed, 1604 insertions(+), 1 deletion(-) create mode 100644 instantout/actions.go create mode 100644 instantout/fsm.go create mode 100644 instantout/instantout.go create mode 100644 instantout/interfaces.go create mode 100644 instantout/log.go diff --git a/instantout/actions.go b/instantout/actions.go new file mode 100644 index 0000000..46e53af --- /dev/null +++ b/instantout/actions.go @@ -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) +} diff --git a/instantout/fsm.go b/instantout/fsm.go new file mode 100644 index 0000000..21559df --- /dev/null +++ b/instantout/fsm.go @@ -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 +} diff --git a/instantout/instantout.go b/instantout/instantout.go new file mode 100644 index 0000000..162fef2 --- /dev/null +++ b/instantout/instantout.go @@ -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 +} diff --git a/instantout/interfaces.go b/instantout/interfaces.go new file mode 100644 index 0000000..2f9018e --- /dev/null +++ b/instantout/interfaces.go @@ -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) +} diff --git a/instantout/log.go b/instantout/log.go new file mode 100644 index 0000000..75764f5 --- /dev/null +++ b/instantout/log.go @@ -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 +} diff --git a/instantout/reservation/actions.go b/instantout/reservation/actions.go index 6acbfc9..8ea1e98 100644 --- a/instantout/reservation/actions.go +++ b/instantout/reservation/actions.go @@ -120,7 +120,7 @@ func (f *FSM) SubscribeToConfirmationAction(_ fsm.EventContext) fsm.EventType { return f.HandleError(err) case confInfo := <-confChan: - f.Debugf("confirmed in block %v", confInfo.Block) + f.Debugf("confirmed in tx: %v", confInfo.Tx) outpoint, err := f.reservation.findReservationOutput( confInfo.Tx, )