2
0
mirror of https://github.com/lightninglabs/loop synced 2024-11-13 13:10:30 +00:00
loop/loopin.go

917 lines
24 KiB
Go
Raw Normal View History

2019-03-12 15:10:37 +00:00
package loop
import (
"context"
"crypto/rand"
"crypto/sha256"
2020-05-29 09:27:47 +00:00
"errors"
2019-03-12 15:10:37 +00:00
"fmt"
"sync"
2019-03-12 15:10:37 +00:00
"time"
"github.com/btcsuite/btcutil"
2020-06-25 10:35:22 +00:00
"github.com/btcsuite/btcd/chaincfg/chainhash"
2019-03-12 15:10:37 +00:00
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/labels"
2019-03-12 15:10:37 +00:00
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap"
2020-06-25 10:35:22 +00:00
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
2019-03-12 15:10:37 +00:00
"github.com/lightningnetwork/lnd/lntypes"
2020-06-25 10:35:22 +00:00
"github.com/lightningnetwork/lnd/lnwire"
2019-03-12 15:10:37 +00:00
)
var (
// MaxLoopInAcceptDelta configures the maximum acceptable number of
// remaining blocks until the on-chain htlc expires. This value is used
// to decide whether we want to continue with the swap parameters as
// proposed by the server. It is a protection to prevent the server from
// getting us to lock up our funds to an arbitrary point in the future.
MaxLoopInAcceptDelta = int32(1500)
// MinLoopInPublishDelta defines the minimum number of remaining blocks
// until on-chain htlc expiry required to proceed to publishing the htlc
// tx. This value isn't critical, as we could even safely publish the
// htlc after expiry. The reason we do implement this check is to
// prevent us from publishing an htlc that the server surely wouldn't
// follow up to.
MinLoopInPublishDelta = int32(10)
// TimeoutTxConfTarget defines the confirmation target for the loop in
// timeout tx.
TimeoutTxConfTarget = int32(2)
)
// loopInSwap contains all the in-memory state related to a pending loop in
// swap.
type loopInSwap struct {
swapKit
executeConfig
2019-03-12 15:10:37 +00:00
loopdb.LoopInContract
htlc *swap.Htlc
htlcP2WSH *swap.Htlc
htlcNP2WSH *swap.Htlc
2020-06-25 10:35:22 +00:00
// htlcTxHash is the confirmed htlc tx id.
htlcTxHash *chainhash.Hash
2019-03-12 15:10:37 +00:00
timeoutAddr btcutil.Address
wg sync.WaitGroup
2019-03-12 15:10:37 +00:00
}
2020-06-30 12:06:36 +00:00
// loopInInitResult contains information about a just-initiated loop in swap.
type loopInInitResult struct {
swap *loopInSwap
serverMessage string
2020-06-30 12:06:36 +00:00
}
2019-03-12 15:10:37 +00:00
// newLoopInSwap initiates a new loop in swap.
func newLoopInSwap(globalCtx context.Context, cfg *swapConfig,
2020-06-30 12:06:36 +00:00
currentHeight int32, request *LoopInRequest) (*loopInInitResult,
error) {
2019-03-12 15:10:37 +00:00
// Request current server loop in terms and use these to calculate the
// swap fee that we should subtract from the swap amount in the payment
// request that we send to the server.
quote, err := cfg.server.GetLoopInQuote(globalCtx, request.Amount)
2019-03-12 15:10:37 +00:00
if err != nil {
return nil, fmt.Errorf("loop in terms: %v", err)
}
swapFee := quote.SwapFee
2019-03-12 15:10:37 +00:00
if swapFee > request.MaxSwapFee {
2019-10-28 16:06:07 +00:00
log.Warnf("Swap fee %v exceeding maximum of %v",
2019-03-12 15:10:37 +00:00
swapFee, request.MaxSwapFee)
return nil, ErrSwapFeeTooHigh
}
// Calculate the swap invoice amount. The prepay is added which
// effectively forces the server to pay us back our prepayment on a
// successful swap.
swapInvoiceAmt := request.Amount - swapFee
// Generate random preimage.
var swapPreimage lntypes.Preimage
if _, err := rand.Read(swapPreimage[:]); err != nil {
2019-10-28 16:06:07 +00:00
log.Error("Cannot generate preimage")
2019-03-12 15:10:37 +00:00
}
swapHash := lntypes.Hash(sha256.Sum256(swapPreimage[:]))
// Derive a sender key for this swap.
keyDesc, err := cfg.lnd.WalletKit.DeriveNextKey(
globalCtx, swap.KeyFamily,
)
if err != nil {
return nil, err
}
var senderKey [33]byte
copy(senderKey[:], keyDesc.PubKey.SerializeCompressed())
// Create the swap invoice in lnd.
_, swapInvoice, err := cfg.lnd.Client.AddInvoice(
globalCtx, &invoicesrpc.AddInvoiceData{
Preimage: &swapPreimage,
Value: lnwire.NewMSatFromSatoshis(swapInvoiceAmt),
2019-03-12 15:10:37 +00:00
Memo: "swap",
Expiry: 3600 * 24 * 365,
},
)
if err != nil {
return nil, err
}
2020-05-29 09:27:47 +00:00
// Create the probe invoice in lnd. Derive the payment hash
// deterministically from the swap hash in such a way that the server
// can be sure that we don't know the preimage.
probeHash := lntypes.Hash(sha256.Sum256(swapHash[:]))
probeHash[0] ^= 1
log.Infof("Creating probe invoice %v", probeHash)
probeInvoice, err := cfg.lnd.Invoices.AddHoldInvoice(
globalCtx, &invoicesrpc.AddInvoiceData{
Hash: &probeHash,
Value: lnwire.NewMSatFromSatoshis(swapInvoiceAmt),
Memo: "loop in probe",
Expiry: 3600,
},
)
if err != nil {
return nil, err
}
// Create a cancellable context that is used for monitoring the probe.
probeWaitCtx, probeWaitCancel := context.WithCancel(globalCtx)
// Launch a goroutine to monitor the probe.
probeResult, err := awaitProbe(probeWaitCtx, *cfg.lnd, probeHash)
if err != nil {
probeWaitCancel()
return nil, fmt.Errorf("probe failed: %v", err)
}
2019-03-12 15:10:37 +00:00
// Post the swap parameters to the swap server. The response contains
// the server success key and the expiry height of the on-chain swap
// htlc.
2019-10-28 16:06:07 +00:00
log.Infof("Initiating swap request at height %v", currentHeight)
2019-03-12 15:10:37 +00:00
swapResp, err := cfg.server.NewLoopInSwap(globalCtx, swapHash,
2020-05-29 09:27:47 +00:00
request.Amount, senderKey, swapInvoice, probeInvoice,
request.LastHop, request.Initiator,
2019-03-12 15:10:37 +00:00
)
2020-05-29 09:27:47 +00:00
probeWaitCancel()
2019-03-12 15:10:37 +00:00
if err != nil {
return nil, fmt.Errorf("cannot initiate swap: %v", err)
}
2020-05-29 09:27:47 +00:00
// Because the context is cancelled, it is guaranteed that we will be
// able to read from the probeResult channel.
err = <-probeResult
if err != nil {
return nil, fmt.Errorf("probe error: %v", err)
}
2019-03-12 15:10:37 +00:00
// Validate the response parameters the prevent us continuing with a
// swap that is based on parameters outside our allowed range.
err = validateLoopInContract(cfg.lnd, currentHeight, request, swapResp)
if err != nil {
return nil, err
}
// Instantiate a struct that contains all required data to start the
// swap.
initiationTime := time.Now()
contract := loopdb.LoopInContract{
HtlcConfTarget: request.HtlcConfTarget,
LastHop: request.LastHop,
ExternalHtlc: request.ExternalHtlc,
2019-03-12 15:10:37 +00:00
SwapContract: loopdb.SwapContract{
InitiationHeight: currentHeight,
InitiationTime: initiationTime,
ReceiverKey: swapResp.receiverKey,
SenderKey: senderKey,
Preimage: swapPreimage,
AmountRequested: request.Amount,
CltvExpiry: swapResp.expiry,
MaxMinerFee: request.MaxMinerFee,
MaxSwapFee: request.MaxSwapFee,
Label: request.Label,
ProtocolVersion: loopdb.CurrentInternalProtocolVersion,
2019-03-12 15:10:37 +00:00
},
}
swapKit := newSwapKit(
swapHash, swap.TypeIn,
cfg, &contract.SwapContract,
2019-03-12 15:10:37 +00:00
)
swapKit.lastUpdateTime = initiationTime
2019-03-12 15:10:37 +00:00
swap := &loopInSwap{
LoopInContract: contract,
swapKit: *swapKit,
}
if err := swap.initHtlcs(); err != nil {
return nil, err
2019-03-12 15:10:37 +00:00
}
// Persist the data before exiting this function, so that the caller can
// trust that this swap will be resumed on restart.
err = cfg.store.CreateLoopIn(swapHash, &swap.LoopInContract)
if err != nil {
return nil, fmt.Errorf("cannot store swap: %v", err)
}
2020-06-30 10:55:50 +00:00
if swapResp.serverMessage != "" {
swap.log.Infof("Server message: %v", swapResp.serverMessage)
}
2020-06-30 12:06:36 +00:00
return &loopInInitResult{
swap: swap,
serverMessage: swapResp.serverMessage,
2020-06-30 12:06:36 +00:00
}, nil
2019-03-12 15:10:37 +00:00
}
2020-05-29 09:27:47 +00:00
// awaitProbe waits for a probe payment to arrive and cancels it. This is a
// workaround for the current lack of multi-path probing.
func awaitProbe(ctx context.Context, lnd lndclient.LndServices,
probeHash lntypes.Hash) (chan error, error) {
// Subscribe to the probe invoice.
updateChan, errChan, err := lnd.Invoices.SubscribeSingleInvoice(
ctx, probeHash,
)
if err != nil {
return nil, err
}
// Wait in the background for the probe to arrive.
probeResult := make(chan error, 1)
go func() {
for {
select {
case update := <-updateChan:
switch update.State {
case channeldb.ContractAccepted:
log.Infof("Server probe successful")
probeResult <- nil
// Cancel probe invoice so that the
// server will know that its probe was
// successful.
err := lnd.Invoices.CancelInvoice(
ctx, probeHash,
)
if err != nil {
log.Errorf("Cancel probe "+
"invoice: %v", err)
}
return
case channeldb.ContractCanceled:
probeResult <- errors.New(
"probe invoice expired")
return
case channeldb.ContractSettled:
probeResult <- errors.New(
"impossible that probe " +
"invoice was settled")
return
}
case err := <-errChan:
probeResult <- err
return
case <-ctx.Done():
probeResult <- ctx.Err()
return
}
}
}()
return probeResult, nil
}
2019-03-12 15:10:37 +00:00
// resumeLoopInSwap returns a swap object representing a pending swap that has
// been restored from the database.
func resumeLoopInSwap(reqContext context.Context, cfg *swapConfig,
pend *loopdb.LoopIn) (*loopInSwap, error) {
hash := lntypes.Hash(sha256.Sum256(pend.Contract.Preimage[:]))
2019-10-28 16:06:07 +00:00
log.Infof("Resuming loop in swap %v", hash)
2019-03-12 15:10:37 +00:00
swapKit := newSwapKit(
hash, swap.TypeIn, cfg,
&pend.Contract.SwapContract,
2019-03-12 15:10:37 +00:00
)
2019-03-12 15:10:37 +00:00
swap := &loopInSwap{
LoopInContract: *pend.Contract,
swapKit: *swapKit,
}
if err := swap.initHtlcs(); err != nil {
return nil, err
2019-03-12 15:10:37 +00:00
}
lastUpdate := pend.LastUpdate()
if lastUpdate == nil {
swap.lastUpdateTime = pend.Contract.InitiationTime
} else {
swap.state = lastUpdate.State
swap.lastUpdateTime = lastUpdate.Time
2020-06-25 10:35:22 +00:00
swap.htlcTxHash = lastUpdate.HtlcTxHash
2019-03-12 15:10:37 +00:00
}
return swap, nil
}
// validateLoopInContract validates the contract parameters against our
// request.
func validateLoopInContract(lnd *lndclient.LndServices,
height int32,
request *LoopInRequest,
response *newLoopInResponse) error {
// Verify that we are not forced to publish an htlc that locks up our
// funds for too long in case the server doesn't follow through.
if response.expiry-height > MaxLoopInAcceptDelta {
return ErrExpiryTooFar
}
return nil
}
// initHtlcs creates and updates the native and nested segwit htlcs
// of the loopInSwap.
func (s *loopInSwap) initHtlcs() error {
htlcP2WSH, err := s.swapKit.getHtlc(swap.HtlcP2WSH)
if err != nil {
return err
}
htlcNP2WSH, err := s.swapKit.getHtlc(swap.HtlcNP2WSH)
if err != nil {
return err
}
// Log htlc addresses for debugging.
s.swapKit.log.Infof("Htlc address (P2WSH): %v", htlcP2WSH.Address)
s.swapKit.log.Infof("Htlc address (NP2WSH): %v", htlcNP2WSH.Address)
s.htlcP2WSH = htlcP2WSH
s.htlcNP2WSH = htlcNP2WSH
return nil
}
// sendUpdate reports an update to the swap state.
func (s *loopInSwap) sendUpdate(ctx context.Context) error {
info := s.swapInfo()
s.log.Infof("Loop in swap state: %v", info.State)
info.HtlcAddressP2WSH = s.htlcP2WSH.Address
info.HtlcAddressNP2WSH = s.htlcNP2WSH.Address
info.ExternalHtlc = s.ExternalHtlc
select {
case s.statusChan <- *info:
case <-ctx.Done():
return ctx.Err()
}
return nil
}
2019-03-12 15:10:37 +00:00
// execute starts/resumes the swap. It is a thin wrapper around executeSwap to
// conveniently handle the error case.
func (s *loopInSwap) execute(mainCtx context.Context,
cfg *executeConfig, height int32) error {
defer s.wg.Wait()
2019-03-12 15:10:37 +00:00
s.executeConfig = *cfg
s.height = height
// Create context for our state subscription which we will cancel once
// swap execution has completed, ensuring that we kill the subscribe
// goroutine.
subCtx, cancel := context.WithCancel(mainCtx)
defer cancel()
s.wg.Add(1)
go func() {
defer s.wg.Done()
subscribeAndLogUpdates(
subCtx, s.hash, s.log, s.server.SubscribeLoopInUpdates,
)
}()
2019-03-12 15:10:37 +00:00
// Announce swap by sending out an initial update.
err := s.sendUpdate(mainCtx)
if err != nil {
return err
}
// Execute the swap until it either reaches a final state or a temporary
// error occurs.
err = s.executeSwap(mainCtx)
// Sanity check. If there is no error, the swap must be in a final
// state.
if err == nil && s.state.Type() == loopdb.StateTypePending {
err = fmt.Errorf("swap in non-final state %v", s.state)
}
// If an unexpected error happened, report a temporary failure
// but don't persist the error. Otherwise for example a
// connection error could lead to abandoning the swap
// permanently and losing funds.
if err != nil {
s.log.Errorf("Swap error: %v", err)
s.setState(loopdb.StateFailTemporary)
2019-03-12 15:10:37 +00:00
// If we cannot send out this update, there is nothing we can do.
_ = s.sendUpdate(mainCtx)
return err
}
s.log.Infof("Loop in swap completed: %v "+
2019-05-15 12:01:27 +00:00
"(final cost: server %v, onchain %v, offchain %v)",
2019-03-12 15:10:37 +00:00
s.state,
s.cost.Server,
s.cost.Onchain,
2019-05-15 12:01:27 +00:00
s.cost.Offchain,
2019-03-12 15:10:37 +00:00
)
return nil
}
// executeSwap executes the swap.
func (s *loopInSwap) executeSwap(globalCtx context.Context) error {
var err error
// For loop in, the client takes the first step by publishing the
// on-chain htlc. Only do this is we haven't already done so in a
// previous run.
if s.state == loopdb.StateInitiated {
if s.ExternalHtlc {
// If an external htlc was indicated, we can move to the
// HtlcPublished state directly and wait for
// confirmation.
s.setState(loopdb.StateHtlcPublished)
err = s.persistAndAnnounceState(globalCtx)
if err != nil {
return err
}
} else {
published, err := s.publishOnChainHtlc(globalCtx)
if err != nil {
return err
}
if !published {
return nil
}
2019-03-12 15:10:37 +00:00
}
}
// Wait for the htlc to confirm. After a restart this will pick up a
// previously published tx.
conf, err := s.waitForHtlcConf(globalCtx)
if err != nil {
return err
}
// Determine the htlc outpoint by inspecting the htlc tx.
htlcOutpoint, htlcValue, err := swap.GetScriptOutput(
2019-04-04 10:20:45 +00:00
conf.Tx, s.htlc.PkScript,
2019-03-12 15:10:37 +00:00
)
if err != nil {
return err
}
// Verify that the confirmed (external) htlc value matches the swap
// amount. Otherwise fail the swap immediately.
if htlcValue != s.LoopInContract.AmountRequested {
s.setState(loopdb.StateFailIncorrectHtlcAmt)
return s.persistAndAnnounceState(globalCtx)
}
2019-03-12 15:10:37 +00:00
// TODO: Add miner fee of htlc tx to swap cost balance.
// The server is expected to see the htlc on-chain and knowing that it
// can sweep that htlc with the preimage, it should pay our swap
// invoice, receive the preimage and sweep the htlc. We are waiting for
// this to happen and simultaneously watch the htlc expiry height. When
// the htlc expires, we will publish a timeout tx to reclaim the funds.
err = s.waitForSwapComplete(globalCtx, htlcOutpoint, htlcValue)
2019-03-12 15:10:37 +00:00
if err != nil {
return err
}
// Persist swap outcome.
if err := s.persistAndAnnounceState(globalCtx); err != nil {
2019-03-12 15:10:37 +00:00
return err
}
return nil
}
// waitForHtlcConf watches the chain until the htlc confirms.
func (s *loopInSwap) waitForHtlcConf(globalCtx context.Context) (
*chainntnfs.TxConfirmation, error) {
2020-06-25 10:35:22 +00:00
// Register for confirmation of the htlc. It is essential to specify not
// just the pk script, because an attacker may publish the same htlc
// with a lower value and we don't want to follow through with that tx.
// In the unlikely event that our call to SendOutputs crashes and we
// restart, htlcTxHash will be nil at this point. Then only register
// with PkScript and accept the risk that the call triggers on a
// different htlc outpoint.
s.log.Infof("Register for htlc conf (hh=%v, txid=%v)",
s.InitiationHeight, s.htlcTxHash)
if s.htlcTxHash == nil {
s.log.Warnf("No htlc tx hash available, registering with " +
"just the pkscript")
}
2019-03-12 15:10:37 +00:00
ctx, cancel := context.WithCancel(globalCtx)
defer cancel()
notifier := s.lnd.ChainNotifier
confChanP2WSH, confErrP2WSH, err := notifier.RegisterConfirmationsNtfn(
2020-06-25 10:35:22 +00:00
ctx, s.htlcTxHash, s.htlcP2WSH.PkScript, 1, s.InitiationHeight,
)
if err != nil {
return nil, err
}
confChanNP2WSH, confErrNP2WSH, err := notifier.RegisterConfirmationsNtfn(
2020-06-25 10:35:22 +00:00
ctx, s.htlcTxHash, s.htlcNP2WSH.PkScript, 1, s.InitiationHeight,
2019-03-12 15:10:37 +00:00
)
if err != nil {
return nil, err
}
2020-06-25 10:11:17 +00:00
var conf *chainntnfs.TxConfirmation
for conf == nil {
2019-03-12 15:10:37 +00:00
select {
// P2WSH htlc confirmed.
2020-06-25 10:11:17 +00:00
case conf = <-confChanP2WSH:
s.htlc = s.htlcP2WSH
s.log.Infof("P2WSH htlc confirmed")
// NP2WSH htlc confirmed.
2020-06-25 10:11:17 +00:00
case conf = <-confChanNP2WSH:
s.htlc = s.htlcNP2WSH
s.log.Infof("NP2WSH htlc confirmed")
2019-03-12 15:10:37 +00:00
// Conf ntfn error.
case err := <-confErrP2WSH:
return nil, err
// Conf ntfn error.
case err := <-confErrNP2WSH:
2019-03-12 15:10:37 +00:00
return nil, err
// Keep up with block height.
case notification := <-s.blockEpochChan:
s.height = notification.(int32)
// Cancel.
case <-globalCtx.Done():
return nil, globalCtx.Err()
}
}
2020-06-25 10:11:17 +00:00
2020-06-25 10:35:22 +00:00
// Store htlc tx hash for accounting purposes. Usually this call is a
// no-op because the htlc tx hash was already known. Exceptions are:
//
// - Old pending swaps that were initiated before we persisted the htlc
// tx hash directly after publish.
//
// - Swaps that experienced a crash during their call to SendOutputs. In
// that case, we weren't able to record the tx hash.
txHash := conf.Tx.TxHash()
s.htlcTxHash = &txHash
2020-06-25 10:11:17 +00:00
return conf, nil
2019-03-12 15:10:37 +00:00
}
// publishOnChainHtlc checks whether there are still enough blocks left and if
// so, it publishes the htlc and advances the swap state.
func (s *loopInSwap) publishOnChainHtlc(ctx context.Context) (bool, error) {
var err error
blocksRemaining := s.CltvExpiry - s.height
s.log.Infof("Blocks left until on-chain expiry: %v", blocksRemaining)
// Verify whether it still makes sense to publish the htlc.
if blocksRemaining < MinLoopInPublishDelta {
s.setState(loopdb.StateFailTimeout)
return false, s.persistAndAnnounceState(ctx)
2019-03-12 15:10:37 +00:00
}
// Get fee estimate from lnd.
feeRate, err := s.lnd.WalletKit.EstimateFee(
ctx, s.LoopInContract.HtlcConfTarget,
)
if err != nil {
return false, fmt.Errorf("estimate fee: %v", err)
}
// Transition to state HtlcPublished before calling SendOutputs to
// prevent us from ever paying multiple times after a crash.
s.setState(loopdb.StateHtlcPublished)
err = s.persistAndAnnounceState(ctx)
2019-03-12 15:10:37 +00:00
if err != nil {
return false, err
}
s.log.Infof("Publishing on chain HTLC with fee rate %v", feeRate)
// Internal loop-in is always P2WSH.
tx, err := s.lnd.WalletKit.SendOutputs(
ctx, []*wire.TxOut{{
PkScript: s.htlcP2WSH.PkScript,
2019-03-12 15:10:37 +00:00
Value: int64(s.LoopInContract.AmountRequested),
}}, feeRate, labels.LoopInHtlcLabel(swap.ShortHash(&s.hash)),
2019-03-12 15:10:37 +00:00
)
if err != nil {
return false, fmt.Errorf("send outputs: %v", err)
}
2020-06-25 10:35:22 +00:00
txHash := tx.TxHash()
s.log.Infof("Published on chain HTLC tx %v", txHash)
// Persist the htlc hash so that after a restart we are still waiting
// for our own htlc. We don't need to announce to clients, because the
// state remains unchanged.
//
// TODO(joostjager): Store tx hash before calling SendOutputs. This is
// not yet possible with the current lnd api.
s.htlcTxHash = &txHash
s.lastUpdateTime = time.Now()
if err := s.persistState(); err != nil {
return false, fmt.Errorf("persist htlc tx: %v", err)
}
2019-03-12 15:10:37 +00:00
return true, nil
}
// waitForSwapComplete waits until a spending tx of the htlc gets confirmed and
// the swap invoice is either settled or canceled. If the htlc times out, the
// timeout tx will be published.
func (s *loopInSwap) waitForSwapComplete(ctx context.Context,
htlcOutpoint *wire.OutPoint, htlcValue btcutil.Amount) error {
2019-03-12 15:10:37 +00:00
// Register the htlc spend notification.
rpcCtx, cancel := context.WithCancel(ctx)
defer cancel()
spendChan, spendErr, err := s.lnd.ChainNotifier.RegisterSpendNtfn(
2020-06-25 10:35:22 +00:00
rpcCtx, htlcOutpoint, s.htlc.PkScript, s.InitiationHeight,
2019-03-12 15:10:37 +00:00
)
if err != nil {
return fmt.Errorf("register spend ntfn: %v", err)
2019-03-12 15:10:37 +00:00
}
// Register for swap invoice updates.
rpcCtx, cancel = context.WithCancel(ctx)
defer cancel()
s.log.Infof("Subscribing to swap invoice %v", s.hash)
swapInvoiceChan, swapInvoiceErr, err := s.lnd.Invoices.SubscribeSingleInvoice(
rpcCtx, s.hash,
)
if err != nil {
return fmt.Errorf("subscribe to swap invoice: %v", err)
}
// checkTimeout publishes the timeout tx if the contract has expired.
checkTimeout := func() error {
if s.height >= s.LoopInContract.CltvExpiry {
return s.publishTimeoutTx(ctx, htlcOutpoint, htlcValue)
}
return nil
}
// Check timeout at current height. After a restart we may want to
// publish the tx immediately.
err = checkTimeout()
if err != nil {
return err
}
htlcSpend := false
invoiceFinalized := false
for !htlcSpend || !invoiceFinalized {
2019-03-12 15:10:37 +00:00
select {
// Spend notification error.
case err := <-spendErr:
return err
2019-03-12 15:10:37 +00:00
// Receive block epochs and start publishing the timeout tx
// whenever possible.
2019-03-12 15:10:37 +00:00
case notification := <-s.blockEpochChan:
s.height = notification.(int32)
err := checkTimeout()
if err != nil {
return err
2019-03-12 15:10:37 +00:00
}
// The htlc spend is confirmed. Inspect the spending tx to
// determine the final swap state.
2019-03-12 15:10:37 +00:00
case spendDetails := <-spendChan:
s.log.Infof("Htlc spend by tx: %v",
spendDetails.SpenderTxHash)
err := s.processHtlcSpend(
ctx, spendDetails, htlcValue,
)
if err != nil {
return err
}
2019-03-12 15:10:37 +00:00
htlcSpend = true
2019-03-12 15:10:37 +00:00
// Swap invoice ntfn error.
case err := <-swapInvoiceErr:
return err
2019-10-07 15:29:24 +00:00
// An update to the swap invoice occurred. Check the new state
// and update the swap state accordingly.
2019-03-12 15:10:37 +00:00
case update := <-swapInvoiceChan:
s.log.Infof("Received swap invoice update: %v",
update.State)
switch update.State {
// Swap invoice was paid, so update server cost balance.
case channeldb.ContractSettled:
s.cost.Server -= update.AmtPaid
// If invoice settlement and htlc spend happen
// in the expected order, move the swap to an
// intermediate state that indicates that the
// swap is complete from the user point of view,
// but still incomplete with regards to
// accounting data.
if s.state == loopdb.StateHtlcPublished {
s.setState(loopdb.StateInvoiceSettled)
err := s.persistAndAnnounceState(ctx)
if err != nil {
return err
}
}
invoiceFinalized = true
2019-03-12 15:10:37 +00:00
// Canceled invoice has no effect on server cost
// balance.
case channeldb.ContractCanceled:
invoiceFinalized = true
2019-03-12 15:10:37 +00:00
}
case <-ctx.Done():
return ctx.Err()
}
}
return nil
}
func (s *loopInSwap) processHtlcSpend(ctx context.Context,
spend *chainntnfs.SpendDetail, htlcValue btcutil.Amount) error {
// Determine the htlc input of the spending tx and inspect the witness
// to findout whether a success or a timeout tx spend the htlc.
htlcInput := spend.SpendingTx.TxIn[spend.SpenderInputIndex]
if s.htlc.IsSuccessWitness(htlcInput.Witness) {
s.setState(loopdb.StateSuccess)
// Server swept the htlc. The htlc value can be added to the
// server cost balance.
s.cost.Server += htlcValue
} else {
s.setState(loopdb.StateFailTimeout)
// Now that the timeout tx confirmed, we can safely cancel the
// swap invoice. We still need to query the final invoice state.
// This is not a hodl invoice, so it may be that the invoice was
// already settled. This means that the server didn't succeed in
// sweeping the htlc after paying the invoice.
err := s.lnd.Invoices.CancelInvoice(ctx, s.hash)
if err != nil && err != channeldb.ErrInvoiceAlreadySettled {
return err
}
// TODO: Add miner fee of timeout tx to swap cost balance.
}
return nil
2019-03-12 15:10:37 +00:00
}
// publishTimeoutTx publishes a timeout tx after the on-chain htlc has expired.
// The swap failed and we are reclaiming our funds.
func (s *loopInSwap) publishTimeoutTx(ctx context.Context,
htlcOutpoint *wire.OutPoint, htlcValue btcutil.Amount) error {
2019-03-12 15:10:37 +00:00
if s.timeoutAddr == nil {
var err error
s.timeoutAddr, err = s.lnd.WalletKit.NextAddr(ctx)
if err != nil {
return err
}
}
// Calculate sweep tx fee
fee, err := s.sweeper.GetSweepFee(
ctx, s.htlc.AddTimeoutToEstimator, s.timeoutAddr,
TimeoutTxConfTarget,
2019-03-12 15:10:37 +00:00
)
if err != nil {
return err
}
witnessFunc := func(sig []byte) (wire.TxWitness, error) {
return s.htlc.GenTimeoutWitness(sig), nil
2019-03-12 15:10:37 +00:00
}
sequence := uint32(0)
2019-03-12 15:10:37 +00:00
timeoutTx, err := s.sweeper.CreateSweepTx(
ctx, s.height, sequence, s.htlc, *htlcOutpoint, s.SenderKey,
witnessFunc, htlcValue, fee, s.timeoutAddr,
2019-03-12 15:10:37 +00:00
)
if err != nil {
return err
}
timeoutTxHash := timeoutTx.TxHash()
s.log.Infof("Publishing timeout tx %v with fee %v to addr %v",
timeoutTxHash, fee, s.timeoutAddr)
err = s.lnd.WalletKit.PublishTransaction(
ctx, timeoutTx,
labels.LoopInSweepTimeout(swap.ShortHash(&s.hash)),
)
2019-03-12 15:10:37 +00:00
if err != nil {
s.log.Warnf("publish timeout: %v", err)
}
return nil
}
// persistAndAnnounceState updates the swap state on disk and sends out an
// update notification.
func (s *loopInSwap) persistAndAnnounceState(ctx context.Context) error {
2019-03-12 15:10:37 +00:00
// Update state in store.
2020-06-25 11:15:58 +00:00
if err := s.persistState(); err != nil {
return err
}
// Send out swap update
return s.sendUpdate(ctx)
}
// persistState updates the swap state on disk.
func (s *loopInSwap) persistState() error {
return s.store.UpdateLoopIn(
s.hash, s.lastUpdateTime,
loopdb.SwapStateData{
2020-06-25 10:35:22 +00:00
State: s.state,
Cost: s.cost,
HtlcTxHash: s.htlcTxHash,
},
)
2019-03-12 15:10:37 +00:00
}
// setState updates the swap state and last update timestamp.
func (s *loopInSwap) setState(state loopdb.SwapState) {
s.lastUpdateTime = time.Now()
s.state = state
}