loop/sweepbatcher/sweep_batcher.go
2026-03-09 14:39:45 +01:00

1704 lines
53 KiB
Go

package sweepbatcher
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btclog/v2"
"github.com/btcsuite/btcwallet/chain"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/labels"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/utils"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
)
const (
// defaultMaxTimeoutDistance is the default maximum timeout distance
// of sweeps that can appear in the same batch.
defaultMaxTimeoutDistance = 288
// defaultMainnetPublishDelay is the default publish delay that is used
// for mainnet.
defaultMainnetPublishDelay = 5 * time.Second
// defaultTestnetPublishDelay is the default publish delay that is used
// for testnet.
defaultTestnetPublishDelay = 500 * time.Millisecond
)
type BatcherStore interface {
// FetchUnconfirmedSweepBatches fetches all the batches from the
// database that are not in a confirmed state.
FetchUnconfirmedSweepBatches(ctx context.Context) ([]*dbBatch, error)
// InsertSweepBatch inserts a batch into the database, returning the id
// of the inserted batch.
InsertSweepBatch(ctx context.Context, batch *dbBatch) (int32, error)
// CancelBatch marks a batch as cancelled in the database. Note that we
// only use this call for batches that have no sweeps or all the sweeps
// are in skipped transaction and so we'd not be able to resume.
CancelBatch(ctx context.Context, id int32) error
// UpdateSweepBatch updates a batch in the database.
UpdateSweepBatch(ctx context.Context, batch *dbBatch) error
// ConfirmBatchWithSweeps atomically marks the batch as confirmed and
// updates the provided sweeps in the database.
ConfirmBatchWithSweeps(ctx context.Context, batch *dbBatch,
sweeps []*dbSweep) error
// FetchBatchSweeps fetches all the sweeps that belong to a batch.
FetchBatchSweeps(ctx context.Context, id int32) ([]*dbSweep, error)
// UpsertSweep inserts a sweep into the database, or updates an existing
// sweep if it already exists.
UpsertSweep(ctx context.Context, sweep *dbSweep) error
// GetSweepStatus returns the completed status of the sweep.
GetSweepStatus(ctx context.Context,
outpoint wire.OutPoint) (bool, error)
// GetParentBatch returns the parent batch of a (completed) sweep.
GetParentBatch(ctx context.Context,
outpoint wire.OutPoint) (*dbBatch, error)
// TotalSweptAmount returns the total amount swept by a (confirmed)
// batch.
TotalSweptAmount(ctx context.Context, id int32) (btcutil.Amount, error)
}
// SweepInfo stores any data related to sweeping a specific outpoint.
type SweepInfo struct {
// ConfTarget is the confirmation target of the sweep.
ConfTarget int32
// Timeout is the timeout of the swap that the sweep belongs to.
Timeout int32
// InitiationHeight is the height at which the swap was initiated.
InitiationHeight int32
// HTLC is the HTLC that is being swept.
HTLC swap.Htlc
// Preimage is the preimage of the HTLC that is being swept.
Preimage lntypes.Preimage
// SwapInvoicePaymentAddr is the payment address of the swap invoice.
SwapInvoicePaymentAddr [32]byte
// HTLCKeys is the set of keys used to sign the HTLC.
HTLCKeys loopdb.HtlcKeys
// HTLCSuccessEstimator is a function that estimates the weight of the
// HTLC success script.
HTLCSuccessEstimator func(*input.TxWeightEstimator) error
// ProtocolVersion is the protocol version of the swap that the sweep
// belongs to.
ProtocolVersion loopdb.ProtocolVersion
// IsExternalAddr is true if the sweep spends to a non-wallet address.
IsExternalAddr bool
// DestAddr is the destination address of the sweep.
DestAddr btcutil.Address
// NonCoopHint is set, if the sweep can not be spent cooperatively and
// has to be spent using preimage. This is only used in fee estimations
// when selecting a batch for the sweep to minimize fees.
NonCoopHint bool
// IsPresigned stores if presigned mode is enabled for the sweep. This
// value should be stable for a sweep. Currently presigned and
// non-presigned sweeps never appear in the same batch.
IsPresigned bool
// Change is an optional change output of the sweep.
Change *wire.TxOut
}
// SweepFetcher is used to get details of a sweep.
type SweepFetcher interface {
// FetchSweep returns details of the sweep with the given hash or
// outpoint. The outpoint is used if hash is not unique.
FetchSweep(ctx context.Context, hash lntypes.Hash,
outpoint wire.OutPoint) (*SweepInfo, error)
}
// MuSig2SignSweep is a function that can be used to sign a sweep transaction
// cooperatively with the swap server.
type MuSig2SignSweep func(ctx context.Context,
protocolVersion loopdb.ProtocolVersion, swapHash lntypes.Hash,
paymentAddr [32]byte, nonce []byte, sweepTxPsbt []byte,
prevoutMap map[wire.OutPoint]*wire.TxOut) (
[]byte, []byte, error)
// SignMuSig2 is a function that can be used to sign a sweep transaction in a
// custom way.
type SignMuSig2 func(ctx context.Context, muSig2Version input.MuSig2Version,
swapHash lntypes.Hash, rootHash chainhash.Hash, sigHash [32]byte,
) ([]byte, error)
// PresignedHelper provides methods used when batches are presigned in advance.
// In this mode sweepbatcher uses transactions provided by PresignedHelper,
// which are pre-signed. The helper also memorizes transactions it previously
// produced. It also affects batch selection: presigned inputs and regular
// (non-presigned) inputs never appear in the same batch. Also if presigning
// fails (e.g. because one of the inputs is offline), an input can't be added to
// a batch.
type PresignedHelper interface {
// DestPkScript returns destination pkScript used by the sweep batch
// with the primary outpoint specified. Returns an error, if such tx
// doesn't exist. If there are many such transactions, returns any of
// pkScript's; all of them should have the same destination pkScript.
// TODO: embed this data into SweepInfo.
DestPkScript(ctx context.Context,
primarySweepID wire.OutPoint) ([]byte, error)
// SignTx signs an unsigned transaction or returns a pre-signed tx.
// It must satisfy the following invariants:
// - the set of inputs is the same, though the order may change;
// - the main output is the same, but its amount may be different;
// - the main output is the first output in the transaction;
// - an optional set of change outputs may be added, the values and
// pkscripts must be preserved.
// - feerate is higher or equal to minRelayFee;
// - LockTime may be decreased;
// - transaction version must be the same;
// - witness must not be empty;
// - Sequence numbers in the inputs must be preserved.
// When choosing a presigned transaction, a transaction with fee rate
// closer to the fee rate passed is selected. If loadOnly is set, it
// doesn't try to sign the transaction and only loads a presigned tx.
// These rules are enforced by CheckSignedTx function.
SignTx(ctx context.Context, primarySweepID wire.OutPoint,
tx *wire.MsgTx, inputAmt btcutil.Amount,
minRelayFee, feeRate chainfee.SatPerKWeight,
loadOnly bool) (*wire.MsgTx, error)
// CleanupTransactions removes all transactions related to any of the
// outpoints. Should be called after sweep batch tx is reorg-safely
// confirmed.
CleanupTransactions(ctx context.Context, inputs []wire.OutPoint) error
}
// VerifySchnorrSig is a function that can be used to verify a schnorr
// signature.
type VerifySchnorrSig func(pubKey *btcec.PublicKey, hash, sig []byte) error
// FeeRateProvider is a function that returns min fee rate of a batch sweeping
// the UTXO of the swap.
type FeeRateProvider func(ctx context.Context, swapHash lntypes.Hash,
utxo wire.OutPoint) (chainfee.SatPerKWeight, error)
// InitialDelayProvider returns the duration after which a newly created batch
// is first published. It allows to customize the duration based on total value
// of the batch. There is a trade-off between better grouping and getting funds
// faster. If the function returns an error, no delay is used and the error is
// logged as a warning.
type InitialDelayProvider func(ctx context.Context, numSweeps int,
value btcutil.Amount, fast bool) (time.Duration, error)
// zeroInitialDelay returns no delay for any sweeps.
func zeroInitialDelay(_ context.Context, _ int,
_ btcutil.Amount, _ bool) (time.Duration, error) {
return 0, nil
}
// PublishErrorHandler is a function that handles transaction publishing error.
type PublishErrorHandler func(err error, errMsg string, log btclog.Logger)
// defaultPublishErrorLogger is an instance of PublishErrorHandler which logs
// all errors as warnings, but "insufficient fee" as info (since they are
// expected, if RBF fails).
func defaultPublishErrorLogger(err error, errMsg string, log btclog.Logger) {
// Check if the error is "insufficient fee" error.
if strings.Contains(err.Error(), chain.ErrInsufficientFee.Error()) {
// Log "insufficient fee" with level Info.
log.Infof("%s: %v", errMsg, err)
return
}
// Log any other error as a warning.
log.Warnf("%s: %v", errMsg, err)
}
// Input specifies an UTXO with amount that is added to the batcher.
type Input struct {
// Outpoint is the outpoint that is being swept.
Outpoint wire.OutPoint
// Value is the value of the outpoint that is being swept.
Value btcutil.Amount
}
// SweepRequest is a request to sweep an outpoint or a group of outpoints.
type SweepRequest struct {
// SwapHash is the hash of the swap that is being swept.
SwapHash lntypes.Hash
// Inputs specifies the inputs in the same request. All the inputs
// belong to the same swap and are added to the same batch.
Inputs []Input
// Notifier is a notifier that is used to notify the requester of this
// sweep that the sweep was successful.
Notifier *SpendNotifier
// Fast is set by the client if the sweep should be published
// immediately.
Fast bool
}
// addSweepsRequest is a request to sweep an outpoint or a group of outpoints
// that is used internally by the batcher (between AddSweep and handleSweeps).
type addSweepsRequest struct {
// sweeps is the list of sweeps already loaded from DB and fee rate
// source.
sweeps []*sweep
// notifier is a notifier that is used to notify the requester of this
// sweep that the sweep was successful.
notifier *SpendNotifier
// fast indicates sweeps that are part of a fast swap.
fast bool
}
// SpendDetail is a notification that is send to the user of sweepbatcher when
// a batch gets the first confirmation.
type SpendDetail struct {
// Tx is the transaction that spent the outpoint.
Tx *wire.MsgTx
// OnChainFeePortion is the fee portion that was paid to get this sweep
// confirmed on chain. This is the difference between the value of the
// outpoint and the value of all sweeps that were included in the batch
// divided by the number of sweeps.
OnChainFeePortion btcutil.Amount
}
// ConfDetail is a notification that is send to the user of sweepbatcher when
// a batch is reorg-safely confirmed, i.e. gets batchConfHeight confirmations.
type ConfDetail struct {
// TxConfirmation has data about the confirmation of the transaction.
*chainntnfs.TxConfirmation
// OnChainFeePortion is the fee portion that was paid to get this sweep
// confirmed on chain. This is the difference between the value of the
// outpoint and the value of all sweeps that were included in the batch
// divided by the number of sweeps.
OnChainFeePortion btcutil.Amount
}
// SpendNotifier is a notifier that is used to notify the requester of a sweep
// that the sweep was successful.
type SpendNotifier struct {
// SpendChan is a channel where the spend details are received.
SpendChan chan<- *SpendDetail
// SpendErrChan is a channel where spend errors are received.
SpendErrChan chan<- error
// ConfChan is a channel where the confirmation details are received.
// This channel is optional.
ConfChan chan<- *ConfDetail
// ConfErrChan is a channel where confirmation errors are received.
// This channel is optional.
ConfErrChan chan<- error
// QuitChan is a channel that can be closed to stop the notifier.
QuitChan <-chan bool
}
var (
ErrBatcherShuttingDown = errors.New("batcher shutting down")
)
// testRequest is a function passed to an event loop and a channel used to
// wait until the function is executed. This is used in unit tests only!
type testRequest struct {
// handler is the function to an event loop.
handler func()
// quit is closed when the handler completes.
quit chan struct{}
}
// Batcher is a system that is responsible for accepting sweep requests and
// placing them in appropriate batches. It will spin up new batches as needed.
type Batcher struct {
// batches is a map of batch IDs to the currently active batches.
batches map[int32]*batch
// addSweepsChan is a channel where sweep requests are received.
addSweepsChan chan *addSweepsRequest
// testReqs is a channel where test requests are received.
// This is used only in unit tests! The reason to have this is to
// avoid data races in require.Eventually calls running in parallel
// to the event loop. See method testRunInEventLoop().
testReqs chan *testRequest
// errChan is a channel where errors are received.
errChan chan error
// quit signals that the batch must stop.
quit chan struct{}
// initDone is a channel that is closed when the batcher has been
// initialized.
initDone chan struct{}
// wallet is the wallet kit client that is used by batches.
wallet lndclient.WalletKitClient
// chainNotifier is the chain notifier client that is used by batches.
chainNotifier lndclient.ChainNotifierClient
// signerClient is the signer client that is used by batches.
signerClient lndclient.SignerClient
// musig2ServerSign includes all the required functionality to collect
// and verify signatures by the swap server in order to cooperatively
// sweep funds.
musig2ServerSign MuSig2SignSweep
// VerifySchnorrSig is a function that can be used to verify a schnorr
// signature.
VerifySchnorrSig VerifySchnorrSig
// chainParams are the chain parameters of the chain that is used by
// batches.
chainParams *chaincfg.Params
// store includes all the database interactions that are needed by the
// batcher and the batches.
store BatcherStore
// sweepStore is used to load sweeps from the database.
sweepStore SweepFetcher
// wg is a waitgroup that is used to wait for all the goroutines to
// exit.
wg sync.WaitGroup
// clock provides methods to work with time and timers.
clock clock.Clock
// initialDelayProvider provides the delay of first batch publishing
// after creation. It only affects newly created batches, not batches
// loaded from DB, so publishing does happen in case of a daemon restart
// (especially important in case of a crashloop). If a sweep is about to
// expire (time until timeout is less that 2x initialDelay), then
// waiting is skipped.
initialDelayProvider InitialDelayProvider
// publishDelay is the delay of batch publishing that is applied in the
// beginning, after the appearance of a new block in the network or
// after the end of initial delay. For batches recovered from DB this
// value is always 0s, regardless of this setting.
publishDelay time.Duration
// customFeeRate provides custom min fee rate per swap. The batch uses
// max of the fee rates of its swaps. In this mode confTarget is
// ignored and fee bumping by sweepbatcher is disabled.
customFeeRate FeeRateProvider
// txLabeler is a function generating a transaction label. It is called
// before publishing a batch transaction. Batch ID is passed to it.
txLabeler func(batchID int32) string
// customMuSig2Signer is a custom signer. If it is set, it is used to
// create musig2 signatures instead of musig2SignSweep and signerClient.
// Note that musig2SignSweep must be nil in this case, however signer
// client must still be provided, as it is used for non-coop spendings.
customMuSig2Signer SignMuSig2
// publishErrorHandler is a function that handles transaction publishing
// error. By default, it logs all errors as warnings, but "insufficient
// fee" as Info.
publishErrorHandler PublishErrorHandler
// presignedHelper provides methods used when presigned batches are
// enabled.
presignedHelper PresignedHelper
// skippedTxns is the list of previous transactions to ignore when
// loading the sweeps from DB. This is needed to fix a historical bug.
skippedTxns map[chainhash.Hash]struct{}
}
// BatcherConfig holds batcher configuration.
type BatcherConfig struct {
// clock provides methods to work with time and timers.
clock clock.Clock
// initialDelayProvider provides the delay of first batch publishing
// after creation. It only affects newly created batches, not batches
// loaded from DB, so publishing does happen in case of a daemon restart
// (especially important in case of a crashloop). If a sweep is about to
// expire (time until timeout is less that 2x initialDelay), then
// waiting is skipped.
initialDelayProvider InitialDelayProvider
// publishDelay is the delay of batch publishing that is applied in the
// beginning, after the appearance of a new block in the network or
// after the end of initial delay. For batches recovered from DB this
// value is always 0s, regardless of this setting.
publishDelay time.Duration
// customFeeRate provides custom min fee rate per swap. The batch uses
// max of the fee rates of its swaps. In this mode confTarget is
// ignored and fee bumping by sweepbatcher is disabled.
customFeeRate FeeRateProvider
// txLabeler is a function generating a transaction label. It is called
// before publishing a batch transaction. Batch ID is passed to it.
txLabeler func(batchID int32) string
// customMuSig2Signer is a custom signer. If it is set, it is used to
// create musig2 signatures instead of musig2SignSweep and signerClient.
// Note that musig2SignSweep must be nil in this case, however signer
// client must still be provided, as it is used for non-coop spendings.
customMuSig2Signer SignMuSig2
// publishErrorHandler is a function that handles transaction publishing
// error. By default, it logs all errors as warnings, but "insufficient
// fee" as Info.
publishErrorHandler PublishErrorHandler
// presignedHelper provides methods used when presigned batches are
// enabled.
presignedHelper PresignedHelper
// skippedTxns is the list of previous transactions to ignore when
// loading the sweeps from DB. This is needed to fix a historical bug.
skippedTxns map[chainhash.Hash]struct{}
}
// BatcherOption configures batcher behaviour.
type BatcherOption func(*BatcherConfig)
// WithClock sets the clock used by sweepbatcher and its batches. It is needed
// to manipulate time in tests.
func WithClock(clock clock.Clock) BatcherOption {
return func(cfg *BatcherConfig) {
cfg.clock = clock
}
}
// WithInitialDelay instructs sweepbatcher to wait for the duration provided
// after new batch creation before it is first published. This facilitates
// better grouping. Defaults to 0s (no initial delay). If a sweep is about
// to expire (time until timeout is less that 2x initialDelay), then waiting
// is skipped.
func WithInitialDelay(provider InitialDelayProvider) BatcherOption {
return func(cfg *BatcherConfig) {
cfg.initialDelayProvider = provider
}
}
// WithPublishDelay sets the delay of batch publishing that is applied in the
// beginning, after the appearance of a new block in the network or after the
// end of initial delay (see WithInitialDelay). It is needed to prevent
// unnecessary transaction publishments when a spend is detected on that block.
// Default value depends on the network: 5 seconds in mainnet, 0.5s in testnet.
// For batches recovered from DB this value is always 0s.
func WithPublishDelay(publishDelay time.Duration) BatcherOption {
return func(cfg *BatcherConfig) {
cfg.publishDelay = publishDelay
}
}
// WithCustomFeeRate instructs sweepbatcher not to fee bump itself and rely on
// external source of fee rates (FeeRateProvider). To apply a fee rate change,
// the caller should re-add the sweep by calling AddSweep.
func WithCustomFeeRate(customFeeRate FeeRateProvider) BatcherOption {
return func(cfg *BatcherConfig) {
cfg.customFeeRate = customFeeRate
}
}
// WithTxLabeler sets a function generating a transaction label. It is called
// before publishing a batch transaction. Batch ID is passed to the function.
// By default, loop/labels.LoopOutBatchSweepSuccess is used.
func WithTxLabeler(txLabeler func(batchID int32) string) BatcherOption {
return func(cfg *BatcherConfig) {
cfg.txLabeler = txLabeler
}
}
// WithCustomSignMuSig2 instructs sweepbatcher to use a custom function to
// produce MuSig2 signatures. If it is set, it is used to create
// musig2 signatures instead of musig2SignSweep and signerClient. Note
// that musig2SignSweep must be nil in this case, however signerClient
// must still be provided, as it is used for non-coop spendings.
func WithCustomSignMuSig2(customMuSig2Signer SignMuSig2) BatcherOption {
return func(cfg *BatcherConfig) {
cfg.customMuSig2Signer = customMuSig2Signer
}
}
// WithPublishErrorHandler sets the callback used to handle publish errors.
// It can be used to filter out noisy messages.
func WithPublishErrorHandler(handler PublishErrorHandler) BatcherOption {
return func(cfg *BatcherConfig) {
cfg.publishErrorHandler = handler
}
}
// WithPresignedHelper enables presigned batches in the batcher. When a sweep
// intended for presigning is added, it must be first passed to the
// PresignSweepsGroup method, before first call of the AddSweep method.
func WithPresignedHelper(presignedHelper PresignedHelper) BatcherOption {
return func(cfg *BatcherConfig) {
cfg.presignedHelper = presignedHelper
}
}
// WithSkippedTxns is the list of previous transactions to ignore when
// loading the sweeps from DB. This is needed to fix a historical bug.
func WithSkippedTxns(skippedTxns map[chainhash.Hash]struct{}) BatcherOption {
return func(cfg *BatcherConfig) {
cfg.skippedTxns = skippedTxns
}
}
// NewBatcher creates a new Batcher instance.
func NewBatcher(wallet lndclient.WalletKitClient,
chainNotifier lndclient.ChainNotifierClient,
signerClient lndclient.SignerClient, musig2ServerSigner MuSig2SignSweep,
verifySchnorrSig VerifySchnorrSig, chainparams *chaincfg.Params,
store BatcherStore, sweepStore SweepFetcher,
opts ...BatcherOption) *Batcher {
badTx1, err := chainhash.NewHashFromStr(
"7028bdac753a254785d29506f311abcda323706b531345105f38999" +
"aecd6f3d1",
)
if err != nil {
panic(err)
}
cfg := BatcherConfig{
// By default, loop/labels.LoopOutBatchSweepSuccess is used
// to label sweep transactions.
txLabeler: labels.LoopOutBatchSweepSuccess,
// publishErrorHandler is a function that handles transaction
// publishing error. By default, it logs all errors as warnings,
// but "insufficient fee" as Info.
publishErrorHandler: defaultPublishErrorLogger,
skippedTxns: map[chainhash.Hash]struct{}{
*badTx1: {},
},
}
for _, opt := range opts {
opt(&cfg)
}
// If WithClock was not provided, use default clock.
if cfg.clock == nil {
cfg.clock = clock.NewDefaultClock()
}
if cfg.customMuSig2Signer != nil && musig2ServerSigner != nil {
panic("customMuSig2Signer must not be used with " +
"musig2ServerSigner")
}
return &Batcher{
batches: make(map[int32]*batch),
addSweepsChan: make(chan *addSweepsRequest),
testReqs: make(chan *testRequest),
errChan: make(chan error, 1),
quit: make(chan struct{}),
initDone: make(chan struct{}),
wallet: wallet,
chainNotifier: chainNotifier,
signerClient: signerClient,
musig2ServerSign: musig2ServerSigner,
VerifySchnorrSig: verifySchnorrSig,
chainParams: chainparams,
store: store,
sweepStore: sweepStore,
clock: cfg.clock,
initialDelayProvider: cfg.initialDelayProvider,
publishDelay: cfg.publishDelay,
customFeeRate: cfg.customFeeRate,
txLabeler: cfg.txLabeler,
customMuSig2Signer: cfg.customMuSig2Signer,
publishErrorHandler: cfg.publishErrorHandler,
presignedHelper: cfg.presignedHelper,
skippedTxns: cfg.skippedTxns,
}
}
// Run starts the batcher and processes incoming sweep requests.
func (b *Batcher) Run(ctx context.Context) error {
runCtx, cancel := context.WithCancel(ctx)
defer func() {
cancel()
close(b.quit)
for _, batch := range b.batches {
batch.Wait()
}
b.wg.Wait()
}()
// First we fetch all the batches that are not in a confirmed state from
// the database. We will then resume the execution of these batches.
batches, err := b.FetchUnconfirmedBatches(runCtx)
if err != nil {
return err
}
for _, batch := range batches {
err := b.spinUpBatchFromDB(runCtx, batch)
if err != nil {
return err
}
}
// Signal that the batcher has been initialized.
close(b.initDone)
for {
select {
case req := <-b.addSweepsChan:
err = b.handleSweeps(
runCtx, req.sweeps, req.notifier, req.fast,
)
if err != nil {
warnf("handleSweeps failed: %v.", err)
return err
}
case testReq := <-b.testReqs:
testReq.handler()
close(testReq.quit)
case err := <-b.errChan:
warnf("Batcher received an error: %v.", err)
return err
case <-runCtx.Done():
infof("Stopping Batcher: run context cancelled.")
return runCtx.Err()
}
}
}
// PresignSweepsGroup creates and stores presigned transactions for the sweeps
// group. This method must be called prior to AddSweep if presigned mode is
// enabled, otherwise AddSweep will fail. All the sweeps must belong to the same
// swap. The order of sweeps is important. The first sweep serves as
// primarySweepID if the group starts a new batch. The change output may be nil
// to indicate that the sweep group does not create a change output.
func (b *Batcher) PresignSweepsGroup(ctx context.Context, inputs []Input,
sweepTimeout int32, destAddress btcutil.Address,
changeOutput *wire.TxOut) error {
if len(inputs) == 0 {
return fmt.Errorf("no inputs passed to PresignSweepsGroup")
}
if b.presignedHelper == nil {
return fmt.Errorf("presignedHelper is not installed")
}
// Find the feerate needed to get into next block. Use conf_target=2,
nextBlockFeeRate, err := b.wallet.EstimateFeeRate(ctx, 2)
if err != nil {
return fmt.Errorf("failed to get nextBlockFeeRate: %w", err)
}
minRelayFeeRate, err := b.wallet.MinRelayFee(ctx)
if err != nil {
return fmt.Errorf("failed to get minRelayFeeRate: %w", err)
}
destPkscript, err := txscript.PayToAddrScript(destAddress)
if err != nil {
return fmt.Errorf("txscript.PayToAddrScript failed: %w", err)
}
infof("PresignSweepsGroup: nextBlockFeeRate is %v, inputs: %v, "+
"destAddress: %v, destPkscript: %x sweepTimeout: %d",
nextBlockFeeRate, inputs, destAddress, destPkscript,
sweepTimeout)
sweeps := make([]sweep, len(inputs))
for i, input := range inputs {
sweeps[i] = sweep{
outpoint: input.Outpoint,
value: input.Value,
timeout: sweepTimeout,
}
}
// Set the change output on the primary group sweep.
sweeps[0].change = changeOutput
// The sweeps are ordered inside the group, the first one is the primary
// outpoint in the batch.
primarySweepID := sweeps[0].outpoint
return presign(
ctx, b.presignedHelper, destAddress, primarySweepID, sweeps,
nextBlockFeeRate, minRelayFeeRate,
)
}
// AddSweep loads information about sweeps from the store and fee rate source,
// and adds them to the batcher for handling. This will either place the sweep
// in an existing batch or create a new one. The method can be called multiple
// times, but the sweeps (including the order of them) must be the same. If
// notifier is provided, the batcher sends back sweeping results through it.
func (b *Batcher) AddSweep(ctx context.Context, sweepReq *SweepRequest) error {
// If the batcher is shutting down, quit now.
select {
case <-b.quit:
return ErrBatcherShuttingDown
default:
}
sweeps, err := b.fetchSweeps(ctx, *sweepReq)
if err != nil {
return fmt.Errorf("fetchSweeps failed: %w", err)
}
if len(sweeps) == 0 {
return fmt.Errorf("trying to add an empty group of sweeps")
}
// Since the whole group is added to the same batch and belongs to
// the same transaction, we use sweeps[0] below where we need any sweep.
sweep := sweeps[0]
completed, err := b.store.GetSweepStatus(ctx, sweep.outpoint)
if err != nil {
return fmt.Errorf("failed to get the status of sweep %v: %w",
sweep.outpoint, err)
}
var (
parentBatch *dbBatch
fullyConfirmed bool
)
if completed {
// Verify that the parent batch is confirmed. Note that a batch
// is only considered confirmed after it has received three
// on-chain confirmations to prevent issues caused by reorgs.
parentBatch, err = b.store.GetParentBatch(ctx, sweep.outpoint)
if err != nil {
return fmt.Errorf("unable to get parent batch for "+
"sweep %x: %w", sweep.swapHash[:6], err)
}
if parentBatch.Confirmed {
fullyConfirmed = true
}
}
minRelayFeeRate, err := b.wallet.MinRelayFee(ctx)
if err != nil {
return fmt.Errorf("failed to get min relay fee: %w", err)
}
// If this is a presigned mode, make sure PresignSweepsGroup was called.
// We skip the check for reorg-safely confirmed sweeps, because their
// presigned transactions were already cleaned up from the store.
if sweep.presigned && !fullyConfirmed {
err := ensurePresigned(
ctx, sweeps, b.presignedHelper, minRelayFeeRate,
b.chainParams,
)
if err != nil {
return fmt.Errorf("inputs with primarySweep %v were "+
"not presigned (call PresignSweepsGroup "+
"first): %w", sweep.outpoint, err)
}
}
infof("Batcher adding sweep group of %d sweeps with primarySweep %x, "+
"presigned=%v, fully_confirmed=%v", len(sweeps),
sweep.swapHash[:6], sweep.presigned, completed)
req := &addSweepsRequest{
sweeps: sweeps,
notifier: sweepReq.Notifier,
fast: sweepReq.Fast,
}
select {
case b.addSweepsChan <- req:
return nil
case <-b.quit:
return ErrBatcherShuttingDown
}
}
// testRunInEventLoop runs a function in the event loop blocking until
// the function returns. For unit tests only!
func (b *Batcher) testRunInEventLoop(ctx context.Context, handler func()) {
// If the event loop is finished, run the function.
select {
case <-b.quit:
handler()
return
default:
}
quit := make(chan struct{})
req := &testRequest{
handler: handler,
quit: quit,
}
select {
case b.testReqs <- req:
case <-ctx.Done():
return
}
select {
case <-quit:
case <-ctx.Done():
}
}
// handleSweeps handles a sweep request by either placing the group of sweeps in
// an existing batch, or by spinning up a new batch for it.
func (b *Batcher) handleSweeps(ctx context.Context, sweeps []*sweep,
notifier *SpendNotifier, fast bool) error {
// Since the whole group is added to the same batch and belongs to
// the same transaction, we use sweeps[0] below where we need any sweep.
sweep := sweeps[0]
sweep.notifier = notifier
// Check if the sweep is already in a batch. If that is the case, we
// provide the sweep to that batch and return.
readded := false
for _, batch := range b.batches {
if batch.sweepExists(sweep.outpoint) {
accepted, err := batch.addSweeps(ctx, sweeps)
if errors.Is(err, ErrBatchShuttingDown) {
// The batch finished while we were trying to
// re-add the sweep. Leave it in the map for the
// lazy cleanup below and fall back to the
// monitorSpendAndNotify path below.
break
}
if err != nil {
return err
}
if !accepted {
return fmt.Errorf("existing sweep %x was not "+
"accepted by batch %d",
sweep.swapHash[:6], batch.id)
}
// The sweep was updated in the batch, our job is done.
readded = true
break
}
}
// Check whether any batch is already completed and lazily delete it. Do
// this after attempting the re-add so we do not remove the batch that
// would have accepted the sweep again; otherwise we could miss the
// re-addition and spin up a new batch for an already finished sweep.
for _, batch := range b.batches {
if batch.isComplete() {
delete(b.batches, batch.id)
}
}
// If the batch was re-added to an existing batch, our job is done.
if readded {
return nil
}
// If the sweep has already been completed in a confirmed batch then we
// can't attach its notifier to the batch as that is no longer running.
// Instead we directly detect and return the spend here. We cannot reuse
// the values gathered in AddSweep because the sweep status may change in
// the meantime. If the status flips while handleSweeps is running, the
// re-add path above will handle it. A batch is removed from b.batches
// only after the code below finds the sweep fully confirmed and switches
// to the monitorSpendAndNotify path.
completed, err := b.store.GetSweepStatus(ctx, sweep.outpoint)
if err != nil {
return fmt.Errorf("failed to get the status of sweep %v: %w",
sweep.outpoint, err)
}
debugf("Status of the sweep group of %d sweeps with primarySweep %x: "+
"presigned=%v, fully_confirmed=%v", len(sweeps),
sweep.swapHash[:6], sweep.presigned, completed)
if completed {
// Verify that the parent batch is confirmed. Note that a batch
// is only considered confirmed after it has received three
// on-chain confirmations to prevent issues caused by reorgs.
parentBatch, err := b.store.GetParentBatch(ctx, sweep.outpoint)
if err != nil {
return fmt.Errorf("unable to get parent batch for "+
"sweep %x: %w", sweep.swapHash[:6], err)
}
debugf("Status of the parent batch of the sweep group of %d "+
"sweeps with primarySweep %x: confirmed=%v",
len(sweeps), sweep.swapHash[:6], parentBatch.Confirmed)
// Batch + sweeps are persisted atomically, so if the sweep
// shows as completed its parent batch must be confirmed.
if parentBatch.Confirmed {
debugf("Sweep group of %d sweeps with primarySweep %x "+
"is fully confirmed, switching directly to "+
"monitoring", len(sweeps), sweep.swapHash[:6])
return b.monitorSpendAndNotify(
ctx, sweeps, parentBatch.ID, notifier,
)
}
}
// If fast is set, we spin up a new batch which is published
// immediately.
if fast {
return b.spinUpNewBatch(ctx, sweeps, true)
}
// Try to run the greedy algorithm of batch selection to minimize costs.
err = b.greedyAddSweeps(ctx, sweeps)
if err == nil {
// The greedy algorithm succeeded.
return nil
}
warnf("Greedy batch selection algorithm failed for sweep %x: %v."+
" Falling back to old approach.", sweep.swapHash[:6], err)
// If one of the batches accepts the sweep, we provide it to that batch.
for _, batch := range b.batches {
accepted, err := batch.addSweeps(ctx, sweeps)
if err != nil && !errors.Is(err, ErrBatchShuttingDown) {
return err
}
// If the sweep was accepted by this batch, we return, our job
// is done.
if accepted {
return nil
}
}
// If no batch is capable of accepting the sweep, we spin up a fresh
// batch and hand the sweep over to it.
return b.spinUpNewBatch(ctx, sweeps, false)
}
// spinUpNewBatch creates new batch, starts it and adds the sweeps to it. If
// presigned mode is enabled, the result also depends on outcome of
// presignedHelper.SignTx.
func (b *Batcher) spinUpNewBatch(ctx context.Context, sweeps []*sweep,
fast bool) error {
// Spin up a fresh batch.
newBatch, err := b.spinUpBatch(ctx, fast)
if err != nil {
return err
}
// Add the sweeps to the fresh batch.
accepted, err := newBatch.addSweeps(ctx, sweeps)
if err != nil {
return err
}
// If the sweeps weren't accepted by the fresh batch something is wrong,
// we should return the error.
if !accepted {
return fmt.Errorf("sweep %x was not accepted by new batch %d",
sweeps[0].swapHash[:6], newBatch.id)
}
return nil
}
// spinUpBatch spins up a new batch and returns it.
func (b *Batcher) spinUpBatch(ctx context.Context, fast bool) (*batch, error) {
cfg := b.newBatchConfig(defaultMaxTimeoutDistance)
switch b.chainParams {
case &chaincfg.MainNetParams:
cfg.batchPublishDelay = defaultMainnetPublishDelay
default:
cfg.batchPublishDelay = defaultTestnetPublishDelay
}
if b.publishDelay != 0 {
if b.publishDelay < 0 {
return nil, fmt.Errorf("negative publishDelay: %v",
b.publishDelay)
}
cfg.batchPublishDelay = b.publishDelay
}
cfg.initialDelayProvider = b.initialDelayProvider
if cfg.initialDelayProvider == nil || fast {
cfg.initialDelayProvider = zeroInitialDelay
}
batchKit := b.newBatchKit()
batch := NewBatch(cfg, batchKit)
id, err := batch.insertAndAcquireID(ctx)
if err != nil {
return nil, err
}
// We add the batch to our map of batches and start it.
b.batches[id] = batch
b.wg.Go(func() {
err := batch.Run(ctx)
if err != nil {
b.writeToErrChan(
ctx, fmt.Errorf("new batch failed: %w", err),
)
}
})
return batch, nil
}
// filterDbSweeps copies dbSweeps, skipping the sweeps from skipped txs.
func filterDbSweeps(skippedTxns map[chainhash.Hash]struct{},
dbSweeps []*dbSweep) []*dbSweep {
result := make([]*dbSweep, 0, len(dbSweeps))
for _, dbSweep := range dbSweeps {
if _, has := skippedTxns[dbSweep.Outpoint.Hash]; has {
continue
}
result = append(result, dbSweep)
}
return result
}
// spinUpBatchFromDB spins up a batch that already existed in storage, then
// returns it.
func (b *Batcher) spinUpBatchFromDB(ctx context.Context, batch *batch) error {
dbSweeps, err := b.store.FetchBatchSweeps(ctx, batch.id)
if err != nil {
return err
}
dbSweeps = filterDbSweeps(b.skippedTxns, dbSweeps)
if len(dbSweeps) == 0 {
infof("skipping restored batch %d as it has no sweeps",
batch.id)
// It is safe to cancel this empty batch as it has no sweeps
// that are not skipped.
err := b.store.CancelBatch(ctx, batch.id)
if err != nil {
warnf("unable to drop empty batch %d: %v",
batch.id, err)
}
return nil
}
primarySweep := dbSweeps[0]
sweeps := make(map[wire.OutPoint]sweep)
// Collect feeRate from sweeps and stored batch.
feeRate := batch.rbfCache.FeeRate
for _, dbSweep := range dbSweeps {
sweep, err := b.convertSweep(ctx, dbSweep)
if err != nil {
return err
}
sweeps[sweep.outpoint] = *sweep
// Set minFeeRate to max(sweep.minFeeRate) for all sweeps.
if feeRate < sweep.minFeeRate {
feeRate = sweep.minFeeRate
}
}
rbfCache := rbfCache{
LastHeight: batch.rbfCache.LastHeight,
FeeRate: feeRate,
}
logger := batchPrefixLogger(fmt.Sprintf("%d", batch.id))
batchKit := b.newBatchKit()
batchKit.id = batch.id
batchKit.batchTxid = batch.batchTxid
batchKit.batchPkScript = batch.batchPkScript
batchKit.state = batch.state
batchKit.primaryID = primarySweep.Outpoint
batchKit.sweeps = sweeps
batchKit.rbfCache = rbfCache
batchKit.log = logger
cfg := b.newBatchConfig(batch.cfg.maxTimeoutDistance)
// Note that initialDelay and batchPublishDelay are 0 for batches
// recovered from DB so publishing happen in case of a daemon restart
// (especially important in case of a crashloop).
cfg.initialDelayProvider = zeroInitialDelay
newBatch, err := NewBatchFromDB(cfg, batchKit)
if err != nil {
return fmt.Errorf("failed in NewBatchFromDB: %w", err)
}
// We add the batch to our map of batches and start it.
b.batches[batch.id] = newBatch
b.wg.Go(func() {
err := newBatch.Run(ctx)
if err != nil {
b.writeToErrChan(
ctx, fmt.Errorf("db batch failed: %w", err),
)
}
})
return nil
}
// FetchUnconfirmedBatches fetches all the batches from the database that are
// not in a confirmed state.
func (b *Batcher) FetchUnconfirmedBatches(ctx context.Context) ([]*batch,
error) {
dbBatches, err := b.store.FetchUnconfirmedSweepBatches(ctx)
if err != nil {
return nil, err
}
batches := make([]*batch, 0, len(dbBatches))
for _, bch := range dbBatches {
batch := batch{}
batch.id = bch.ID
if bch.Confirmed {
batch.state = Confirmed
} else {
// We don't store Closed state separately in DB.
// If the batch is closed (included into a block, but
// not reorg-safely confirmed), it is now considered
// Open again. It will receive a spending notification
// as soon as it starts, so it is not an issue. If a
// sweep manages to be added during this time, it will
// be detected as missing when analyzing the spend
// notification and will be added to new batch.
batch.state = Open
}
batch.batchTxid = &bch.BatchTxid
batch.batchPkScript = bch.BatchPkScript
rbfCache := rbfCache{
LastHeight: bch.LastRbfHeight,
FeeRate: chainfee.SatPerKWeight(bch.LastRbfSatPerKw),
}
batch.rbfCache = rbfCache
bchCfg := b.newBatchConfig(bch.MaxTimeoutDistance)
batch.cfg = &bchCfg
batches = append(batches, &batch)
}
return batches, nil
}
// monitorSpendAndNotify monitors the spend of a specific outpoint and writes
// the response back to the response channel. It is called if the batch is fully
// confirmed and we just need to deliver the data back to the caller though
// SpendNotifier.
func (b *Batcher) monitorSpendAndNotify(ctx context.Context, sweeps []*sweep,
parentBatchID int32, notifier *SpendNotifier) error {
// If the caller has not provided a notifier, stop.
if notifier == nil || *notifier == (SpendNotifier{}) {
return nil
}
spendCtx, cancel := context.WithCancel(ctx)
// Then we get the total amount that was swept by the batch.
totalSwept, err := b.store.TotalSweptAmount(ctx, parentBatchID)
if err != nil {
cancel()
return err
}
// Find the primarySweepID.
dbSweeps, err := b.store.FetchBatchSweeps(ctx, parentBatchID)
if err != nil {
cancel()
return err
}
primarySweepID := dbSweeps[0].Outpoint
sweep := sweeps[0]
spendChan, spendErr, err := b.chainNotifier.RegisterSpendNtfn(
spendCtx, &sweep.outpoint, sweep.htlc.PkScript,
sweep.initiationHeight,
)
if err != nil {
cancel()
return err
}
b.wg.Go(func() {
defer cancel()
infof("Batcher monitoring spend for swap %x",
sweep.swapHash[:6])
select {
case spend := <-spendChan:
spendTx := spend.SpendingTx
// Calculate the fee portion that each sweep should pay
// for the batch.
feePortionPerSweep, roundingDifference :=
getFeePortionForSweep(
spendTx, len(spendTx.TxIn),
totalSwept,
)
// Sum onchain fee across all the sweeps of the swap.
var fee btcutil.Amount
for _, s := range sweeps {
isFirst := s.outpoint == primarySweepID
fee += getFeePortionPaidBySweep(
feePortionPerSweep, roundingDifference,
isFirst,
)
}
// Notify the requester of the spend with the spend
// details, including the fee portion for this
// particular sweep.
spendDetail := &SpendDetail{
Tx: spendTx,
OnChainFeePortion: fee,
}
select {
// Try to write the update to the notification channel.
case notifier.SpendChan <- spendDetail:
err := b.monitorConfAndNotify(
ctx, sweep, notifier, spendTx,
fee,
)
if err != nil {
b.writeToErrChan(
ctx, fmt.Errorf("monitor conf "+
"failed: %w", err),
)
}
// If a quit signal was provided by the swap, continue.
case <-notifier.QuitChan:
// If the context was canceled, stop.
case <-ctx.Done():
}
return
case err := <-spendErr:
select {
// Try to write the error to the notification
// channel.
case notifier.SpendErrChan <- err:
// If a quit signal was provided by the swap,
// continue.
case <-notifier.QuitChan:
// If the context was canceled, stop.
case <-ctx.Done():
}
b.writeToErrChan(
ctx, fmt.Errorf("spend error: %w", err),
)
return
// If a quit signal was provided by the swap, continue.
case <-notifier.QuitChan:
return
// If the context was canceled, stop.
case <-ctx.Done():
return
}
})
return nil
}
// monitorConfAndNotify monitors the confirmation of a specific transaction and
// writes the response back to the response channel. It is called if the batch
// is reorg-safely confirmed, and we just need to deliver the data back to the
// caller though SpendNotifier.
func (b *Batcher) monitorConfAndNotify(ctx context.Context, sweep *sweep,
notifier *SpendNotifier, spendTx *wire.MsgTx,
onChainFeePortion btcutil.Amount) error {
// If confirmation notifications were not requested, stop.
if notifier.ConfChan == nil && notifier.ConfErrChan == nil {
return nil
}
batchTxid := spendTx.TxHash()
if len(spendTx.TxOut) < 1 {
return fmt.Errorf("expected at least one output in batch")
}
batchPkScript := spendTx.TxOut[0].PkScript
reorgChan := make(chan struct{})
confCtx, cancel := context.WithCancel(ctx)
confChan, errChan, err := b.chainNotifier.RegisterConfirmationsNtfn(
confCtx, &batchTxid, batchPkScript, batchConfHeight,
sweep.initiationHeight, lndclient.WithReOrgChan(reorgChan),
)
if err != nil {
cancel()
return err
}
b.wg.Go(func() {
defer cancel()
select {
case conf := <-confChan:
if notifier.ConfChan != nil {
confDetail := &ConfDetail{
TxConfirmation: conf,
OnChainFeePortion: onChainFeePortion,
}
select {
case notifier.ConfChan <- confDetail:
case <-notifier.QuitChan:
case <-ctx.Done():
}
}
case err := <-errChan:
if notifier.ConfErrChan != nil {
select {
case notifier.ConfErrChan <- err:
case <-notifier.QuitChan:
case <-ctx.Done():
}
}
b.writeToErrChan(ctx, fmt.Errorf("confirmations "+
"monitoring error: %w", err))
case <-reorgChan:
// A re-org has been detected, but the batch is fully
// confirmed and this is unexpected. Crash the batcher.
b.writeToErrChan(ctx, fmt.Errorf("unexpected reorg"))
case <-ctx.Done():
}
})
return nil
}
func (b *Batcher) writeToErrChan(ctx context.Context, err error) {
select {
case b.errChan <- err:
case <-ctx.Done():
}
}
// convertSweep converts a fetched sweep from the database to a sweep that is
// ready to be processed by the batcher. It loads swap from loopdb by calling
// the method FetchLoopOutSwap.
func (b *Batcher) convertSweep(ctx context.Context, dbSweep *dbSweep) (
*sweep, error) {
return b.loadSweep(ctx, dbSweep.SwapHash, dbSweep.Outpoint,
dbSweep.Amount)
}
// LoopOutFetcher is used to load LoopOut swaps from the database.
// It is implemented by loopdb.SwapStore.
type LoopOutFetcher interface {
// FetchLoopOutSwap returns the loop out swap with the given hash.
FetchLoopOutSwap(ctx context.Context,
hash lntypes.Hash) (*loopdb.LoopOut, error)
}
// SwapStoreWrapper is LoopOutFetcher wrapper providing SweepFetcher interface.
type SwapStoreWrapper struct {
// swapStore is used to load LoopOut swaps from the database.
swapStore LoopOutFetcher
// chainParams are the chain parameters of the chain that is used by
// batches.
chainParams *chaincfg.Params
}
// FetchSweep returns details of the sweep with the given hash.
// In LoopOut case, swap hashes are unique.
// Implements SweepFetcher interface.
func (f *SwapStoreWrapper) FetchSweep(ctx context.Context,
swapHash lntypes.Hash, _ wire.OutPoint) (*SweepInfo, error) {
swap, err := f.swapStore.FetchLoopOutSwap(ctx, swapHash)
if err != nil {
return nil, fmt.Errorf("failed to fetch loop out for %x: %w",
swapHash[:6], err)
}
htlc, err := utils.GetHtlc(
swapHash, &swap.Contract.SwapContract, f.chainParams,
)
if err != nil {
return nil, fmt.Errorf("failed to get htlc: %w", err)
}
swapPaymentAddr, err := utils.ObtainSwapPaymentAddr(
swap.Contract.SwapInvoice, f.chainParams,
)
if err != nil {
return nil, fmt.Errorf("failed to get payment addr: %w", err)
}
return &SweepInfo{
ConfTarget: swap.Contract.SweepConfTarget,
Timeout: swap.Contract.CltvExpiry,
InitiationHeight: swap.Contract.InitiationHeight,
HTLC: *htlc,
Preimage: swap.Contract.Preimage,
SwapInvoicePaymentAddr: *swapPaymentAddr,
HTLCKeys: swap.Contract.HtlcKeys,
HTLCSuccessEstimator: htlc.AddSuccessToEstimator,
ProtocolVersion: swap.Contract.ProtocolVersion,
IsExternalAddr: swap.Contract.IsExternalAddr,
DestAddr: swap.Contract.DestAddr,
}, nil
}
// NewSweepFetcherFromSwapStore accepts swapStore (e.g. loopdb) and returns
// a wrapper implementing SweepFetcher interface (suitable for NewBatcher).
func NewSweepFetcherFromSwapStore(swapStore LoopOutFetcher,
chainParams *chaincfg.Params) (*SwapStoreWrapper, error) {
return &SwapStoreWrapper{
swapStore: swapStore,
chainParams: chainParams,
}, nil
}
// fetchSweeps fetches the sweep related information from the database.
func (b *Batcher) fetchSweeps(ctx context.Context,
sweepReq SweepRequest) ([]*sweep, error) {
sweeps := make([]*sweep, len(sweepReq.Inputs))
for i, utxo := range sweepReq.Inputs {
s, err := b.loadSweep(
ctx, sweepReq.SwapHash, utxo.Outpoint,
utxo.Value,
)
if err != nil {
return nil, fmt.Errorf("failed to load "+
"sweep %v: %w", utxo.Outpoint, err)
}
sweeps[i] = s
}
return sweeps, nil
}
// loadSweep loads inputs of sweep from the database and from FeeRateProvider
// if needed and returns an assembled sweep object.
func (b *Batcher) loadSweep(ctx context.Context, swapHash lntypes.Hash,
outpoint wire.OutPoint, value btcutil.Amount) (*sweep, error) {
s, err := b.sweepStore.FetchSweep(ctx, swapHash, outpoint)
if err != nil {
return nil, fmt.Errorf("failed to fetch sweep data for %x: %w",
swapHash[:6], err)
}
// Make sure that PkScript of the coin is filled. Otherwise
// RegisterSpendNtfn fails.
if len(s.HTLC.PkScript) == 0 {
return nil, fmt.Errorf("sweep data for %x doesn't have "+
"HTLC.PkScript set", swapHash[:6])
}
// Find minimum fee rate for the sweep. Use customFeeRate if it is
// provided, otherwise use wallet's EstimateFeeRate.
minFeeRate, err := minimumSweepFeeRate(
ctx, b.customFeeRate, b.wallet,
swapHash, outpoint, s.ConfTarget,
)
if err != nil {
return nil, err
}
return &sweep{
swapHash: swapHash,
outpoint: outpoint,
value: value,
confTarget: s.ConfTarget,
timeout: s.Timeout,
initiationHeight: s.InitiationHeight,
htlc: s.HTLC,
preimage: s.Preimage,
swapInvoicePaymentAddr: s.SwapInvoicePaymentAddr,
htlcKeys: s.HTLCKeys,
htlcSuccessEstimator: s.HTLCSuccessEstimator,
protocolVersion: s.ProtocolVersion,
isExternalAddr: s.IsExternalAddr,
destAddr: s.DestAddr,
minFeeRate: minFeeRate,
nonCoopHint: s.NonCoopHint,
presigned: s.IsPresigned,
change: s.Change,
}, nil
}
// feeRateEstimator determines feerate by confTarget.
type feeRateEstimator interface {
// EstimateFeeRate returns feerate corresponding to the confTarget.
EstimateFeeRate(ctx context.Context,
confTarget int32) (chainfee.SatPerKWeight, error)
}
// minimumSweepFeeRate determines minimum feerate for a sweep.
func minimumSweepFeeRate(ctx context.Context, customFeeRate FeeRateProvider,
wallet feeRateEstimator, swapHash lntypes.Hash, outpoint wire.OutPoint,
sweepConfTarget int32) (chainfee.SatPerKWeight, error) {
// Find minimum fee rate for the sweep. Use customFeeRate if it is
// provided, otherwise use wallet's EstimateFeeRate.
if customFeeRate != nil {
minFeeRate, err := customFeeRate(ctx, swapHash, outpoint)
if err != nil {
return 0, fmt.Errorf("failed to fetch min fee rate "+
"for %x: %w", swapHash[:6], err)
}
if minFeeRate < chainfee.AbsoluteFeePerKwFloor {
return 0, fmt.Errorf("min fee rate too low (%v) for "+
"%x", minFeeRate, swapHash[:6])
}
return minFeeRate, nil
}
// Make sure sweepConfTarget is at least 2. LND's walletkit fails with
// conftarget of 0 or 1.
// TODO: when https://github.com/lightningnetwork/lnd/pull/10087 is
// merged and that LND version becomes a requirement, we can decrease
// this from 2 to 1.
if sweepConfTarget < 2 {
warnf("Fee estimation was requested for confTarget=%d for "+
"sweep %x; changing confTarget to 2", sweepConfTarget,
swapHash[:6])
sweepConfTarget = 2
}
minFeeRate, err := wallet.EstimateFeeRate(ctx, sweepConfTarget)
if err != nil {
return 0, fmt.Errorf("failed to estimate fee rate "+
"for %x, confTarget=%d: %w", swapHash[:6],
sweepConfTarget, err)
}
return minFeeRate, nil
}
// newBatchConfig creates new batch config.
func (b *Batcher) newBatchConfig(maxTimeoutDistance int32) batchConfig {
return batchConfig{
maxTimeoutDistance: maxTimeoutDistance,
customFeeRate: b.customFeeRate,
txLabeler: b.txLabeler,
customMuSig2Signer: b.customMuSig2Signer,
presignedHelper: b.presignedHelper,
skippedTxns: b.skippedTxns,
clock: b.clock,
chainParams: b.chainParams,
}
}
// newBatchKit creates new batch kit.
func (b *Batcher) newBatchKit() batchKit {
return batchKit{
wallet: b.wallet,
chainNotifier: b.chainNotifier,
signerClient: b.signerClient,
musig2SignSweep: b.musig2ServerSign,
verifySchnorrSig: b.VerifySchnorrSig,
publishErrorHandler: b.publishErrorHandler,
purger: b.AddSweep,
store: b.store,
quit: b.quit,
}
}