2023-08-23 17:44:14 +03:00
|
|
|
package sweepbatcher
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2024-05-24 10:36:42 +02:00
|
|
|
"errors"
|
2023-08-23 17:44:14 +03:00
|
|
|
"fmt"
|
2024-08-27 11:44:28 -03:00
|
|
|
"strings"
|
2023-08-23 17:44:14 +03:00
|
|
|
"sync"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
|
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
|
|
|
"github.com/btcsuite/btcd/chaincfg"
|
2024-06-24 23:18:16 -03:00
|
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
2025-05-14 23:36:15 -03:00
|
|
|
"github.com/btcsuite/btcd/txscript"
|
2023-08-23 17:44:14 +03:00
|
|
|
"github.com/btcsuite/btcd/wire"
|
2025-03-06 14:03:27 +01:00
|
|
|
"github.com/btcsuite/btclog/v2"
|
2024-08-27 11:44:28 -03:00
|
|
|
"github.com/btcsuite/btcwallet/chain"
|
2023-08-23 17:44:14 +03:00
|
|
|
"github.com/lightninglabs/lndclient"
|
2024-08-13 00:34:16 -03:00
|
|
|
"github.com/lightninglabs/loop/labels"
|
2023-08-23 17:44:14 +03:00
|
|
|
"github.com/lightninglabs/loop/loopdb"
|
2024-05-14 11:36:47 -03:00
|
|
|
"github.com/lightninglabs/loop/swap"
|
2023-08-23 17:44:14 +03:00
|
|
|
"github.com/lightninglabs/loop/utils"
|
2025-04-08 23:15:23 -03:00
|
|
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
2024-08-03 23:18:55 -03:00
|
|
|
"github.com/lightningnetwork/lnd/clock"
|
2024-05-14 11:36:47 -03:00
|
|
|
"github.com/lightningnetwork/lnd/input"
|
2023-08-23 17:44:14 +03:00
|
|
|
"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.
|
2024-07-30 23:39:06 -03:00
|
|
|
defaultTestnetPublishDelay = 500 * time.Millisecond
|
2023-08-23 17:44:14 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type BatcherStore interface {
|
|
|
|
|
// FetchUnconfirmedSweepBatches fetches all the batches from the
|
|
|
|
|
// database that are not in a confirmed state.
|
2024-05-24 10:36:42 +02:00
|
|
|
FetchUnconfirmedSweepBatches(ctx context.Context) ([]*dbBatch, error)
|
2023-08-23 17:44:14 +03:00
|
|
|
|
|
|
|
|
// InsertSweepBatch inserts a batch into the database, returning the id
|
|
|
|
|
// of the inserted batch.
|
2024-05-24 10:36:42 +02:00
|
|
|
InsertSweepBatch(ctx context.Context, batch *dbBatch) (int32, error)
|
|
|
|
|
|
2025-06-17 14:01:59 -03:00
|
|
|
// 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
|
2023-08-23 17:44:14 +03:00
|
|
|
|
|
|
|
|
// UpdateSweepBatch updates a batch in the database.
|
2024-05-24 10:36:42 +02:00
|
|
|
UpdateSweepBatch(ctx context.Context, batch *dbBatch) error
|
2023-08-23 17:44:14 +03:00
|
|
|
|
2025-11-12 02:33:43 -03:00
|
|
|
// ConfirmBatchWithSweeps atomically marks the batch as confirmed and
|
|
|
|
|
// updates the provided sweeps in the database.
|
|
|
|
|
ConfirmBatchWithSweeps(ctx context.Context, batch *dbBatch,
|
|
|
|
|
sweeps []*dbSweep) error
|
|
|
|
|
|
2023-08-23 17:44:14 +03:00
|
|
|
// FetchBatchSweeps fetches all the sweeps that belong to a batch.
|
2024-05-24 10:36:42 +02:00
|
|
|
FetchBatchSweeps(ctx context.Context, id int32) ([]*dbSweep, error)
|
2023-08-23 17:44:14 +03:00
|
|
|
|
|
|
|
|
// 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.
|
2025-03-25 14:30:52 -03:00
|
|
|
GetSweepStatus(ctx context.Context,
|
|
|
|
|
outpoint wire.OutPoint) (bool, error)
|
2024-02-02 22:07:40 +01:00
|
|
|
|
|
|
|
|
// GetParentBatch returns the parent batch of a (completed) sweep.
|
2025-03-25 14:30:52 -03:00
|
|
|
GetParentBatch(ctx context.Context,
|
|
|
|
|
outpoint wire.OutPoint) (*dbBatch, error)
|
2024-02-02 22:07:40 +01:00
|
|
|
|
|
|
|
|
// TotalSweptAmount returns the total amount swept by a (confirmed)
|
|
|
|
|
// batch.
|
|
|
|
|
TotalSweptAmount(ctx context.Context, id int32) (btcutil.Amount, error)
|
2023-08-23 17:44:14 +03:00
|
|
|
}
|
|
|
|
|
|
2024-05-14 11:36:47 -03:00
|
|
|
// 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
|
2024-06-28 21:34:55 -03:00
|
|
|
|
|
|
|
|
// 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
|
2025-04-01 12:52:57 -03:00
|
|
|
|
|
|
|
|
// 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
|
2025-07-18 11:07:40 +02:00
|
|
|
|
|
|
|
|
// Change is an optional change output of the sweep.
|
|
|
|
|
Change *wire.TxOut
|
2024-05-14 11:36:47 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SweepFetcher is used to get details of a sweep.
|
|
|
|
|
type SweepFetcher interface {
|
2025-03-25 14:30:52 -03:00
|
|
|
// 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)
|
2024-05-14 10:44:08 -03:00
|
|
|
}
|
|
|
|
|
|
2023-08-23 17:44:14 +03:00
|
|
|
// 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)
|
|
|
|
|
|
2024-06-24 23:18:16 -03:00
|
|
|
// 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)
|
|
|
|
|
|
2025-04-01 12:52:57 -03:00
|
|
|
// 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.
|
2025-05-14 21:40:42 -03:00
|
|
|
// TODO: embed this data into SweepInfo.
|
2025-04-01 12:52:57 -03:00
|
|
|
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;
|
2025-07-18 11:07:40 +02:00
|
|
|
// - 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.
|
2025-04-01 12:52:57 -03:00
|
|
|
// - 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.
|
2025-07-18 11:07:40 +02:00
|
|
|
// These rules are enforced by CheckSignedTx function.
|
2025-04-01 12:52:57 -03:00
|
|
|
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
|
2025-04-26 01:01:39 -03:00
|
|
|
// outpoints. Should be called after sweep batch tx is reorg-safely
|
|
|
|
|
// confirmed.
|
2025-04-01 12:52:57 -03:00
|
|
|
CleanupTransactions(ctx context.Context, inputs []wire.OutPoint) error
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-23 17:44:14 +03:00
|
|
|
// VerifySchnorrSig is a function that can be used to verify a schnorr
|
|
|
|
|
// signature.
|
|
|
|
|
type VerifySchnorrSig func(pubKey *btcec.PublicKey, hash, sig []byte) error
|
|
|
|
|
|
2024-06-27 14:10:24 -03:00
|
|
|
// FeeRateProvider is a function that returns min fee rate of a batch sweeping
|
|
|
|
|
// the UTXO of the swap.
|
2025-05-05 23:28:23 -03:00
|
|
|
type FeeRateProvider func(ctx context.Context, swapHash lntypes.Hash,
|
|
|
|
|
utxo wire.OutPoint) (chainfee.SatPerKWeight, error)
|
2024-06-27 14:10:24 -03:00
|
|
|
|
2025-03-28 00:21:20 -03:00
|
|
|
// 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,
|
2025-10-01 08:44:36 +02:00
|
|
|
value btcutil.Amount, fast bool) (time.Duration, error)
|
2025-03-28 00:21:20 -03:00
|
|
|
|
|
|
|
|
// zeroInitialDelay returns no delay for any sweeps.
|
|
|
|
|
func zeroInitialDelay(_ context.Context, _ int,
|
2025-10-01 08:44:36 +02:00
|
|
|
_ btcutil.Amount, _ bool) (time.Duration, error) {
|
2025-03-28 00:21:20 -03:00
|
|
|
|
|
|
|
|
return 0, nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-27 11:44:28 -03:00
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-01 00:26:01 -03:00
|
|
|
// Input specifies an UTXO with amount that is added to the batcher.
|
|
|
|
|
type Input struct {
|
2023-08-23 17:44:14 +03:00
|
|
|
// 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
|
2025-04-01 00:26:01 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
2023-08-23 17:44:14 +03:00
|
|
|
|
|
|
|
|
// Notifier is a notifier that is used to notify the requester of this
|
|
|
|
|
// sweep that the sweep was successful.
|
|
|
|
|
Notifier *SpendNotifier
|
2025-10-01 08:44:36 +02:00
|
|
|
|
|
|
|
|
// Fast is set by the client if the sweep should be published
|
|
|
|
|
// immediately.
|
|
|
|
|
Fast bool
|
2023-08-23 17:44:14 +03:00
|
|
|
}
|
|
|
|
|
|
2025-04-30 13:53:56 -03:00
|
|
|
// 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
|
|
|
|
|
|
2026-02-28 10:29:25 +01:00
|
|
|
// notifier is a notifier that is used to notify the requester of this
|
2025-04-30 13:53:56 -03:00
|
|
|
// sweep that the sweep was successful.
|
|
|
|
|
notifier *SpendNotifier
|
2025-10-01 08:44:36 +02:00
|
|
|
|
|
|
|
|
// fast indicates sweeps that are part of a fast swap.
|
|
|
|
|
fast bool
|
2025-04-30 13:53:56 -03:00
|
|
|
}
|
|
|
|
|
|
2025-04-08 23:15:23 -03:00
|
|
|
// SpendDetail is a notification that is send to the user of sweepbatcher when
|
|
|
|
|
// a batch gets the first confirmation.
|
2024-02-02 22:37:54 +01:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-08 23:15:23 -03:00
|
|
|
// ConfDetail is a notification that is send to the user of sweepbatcher when
|
2025-04-26 01:01:39 -03:00
|
|
|
// a batch is reorg-safely confirmed, i.e. gets batchConfHeight confirmations.
|
2025-04-08 23:15:23 -03:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-23 17:44:14 +03:00
|
|
|
// 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.
|
2025-04-08 12:02:23 -03:00
|
|
|
SpendChan chan<- *SpendDetail
|
2023-08-23 17:44:14 +03:00
|
|
|
|
|
|
|
|
// SpendErrChan is a channel where spend errors are received.
|
2025-04-08 12:02:23 -03:00
|
|
|
SpendErrChan chan<- error
|
2023-08-23 17:44:14 +03:00
|
|
|
|
2025-04-08 23:15:23 -03:00
|
|
|
// 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
|
|
|
|
|
|
2023-08-23 17:44:14 +03:00
|
|
|
// QuitChan is a channel that can be closed to stop the notifier.
|
2025-04-08 12:02:23 -03:00
|
|
|
QuitChan <-chan bool
|
2023-08-23 17:44:14 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (
|
2024-05-24 10:36:42 +02:00
|
|
|
ErrBatcherShuttingDown = errors.New("batcher shutting down")
|
2023-08-23 17:44:14 +03:00
|
|
|
)
|
|
|
|
|
|
2025-02-25 23:42:27 -03:00
|
|
|
// 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{}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-23 17:44:14 +03:00
|
|
|
// 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
|
|
|
|
|
|
2025-04-30 13:53:56 -03:00
|
|
|
// addSweepsChan is a channel where sweep requests are received.
|
|
|
|
|
addSweepsChan chan *addSweepsRequest
|
2023-08-23 17:44:14 +03:00
|
|
|
|
2025-02-25 23:42:27 -03:00
|
|
|
// 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
|
|
|
|
|
|
2023-08-23 17:44:14 +03:00
|
|
|
// errChan is a channel where errors are received.
|
|
|
|
|
errChan chan error
|
|
|
|
|
|
|
|
|
|
// quit signals that the batch must stop.
|
|
|
|
|
quit chan struct{}
|
|
|
|
|
|
2024-05-24 10:52:54 +02:00
|
|
|
// initDone is a channel that is closed when the batcher has been
|
|
|
|
|
// initialized.
|
|
|
|
|
initDone chan struct{}
|
|
|
|
|
|
2023-08-23 17:44:14 +03:00
|
|
|
// 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
|
|
|
|
|
|
2024-07-19 14:20:07 +08:00
|
|
|
// musig2ServerSign includes all the required functionality to collect
|
2023-08-23 17:44:14 +03:00
|
|
|
// and verify signatures by the swap server in order to cooperatively
|
|
|
|
|
// sweep funds.
|
|
|
|
|
musig2ServerSign MuSig2SignSweep
|
|
|
|
|
|
2024-08-27 10:57:41 -03:00
|
|
|
// VerifySchnorrSig is a function that can be used to verify a schnorr
|
2023-08-23 17:44:14 +03:00
|
|
|
// 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
|
|
|
|
|
|
2024-05-14 11:36:47 -03:00
|
|
|
// sweepStore is used to load sweeps from the database.
|
|
|
|
|
sweepStore SweepFetcher
|
2023-08-23 17:44:14 +03:00
|
|
|
|
|
|
|
|
// wg is a waitgroup that is used to wait for all the goroutines to
|
|
|
|
|
// exit.
|
|
|
|
|
wg sync.WaitGroup
|
2024-06-21 23:19:51 -03:00
|
|
|
|
2024-08-03 23:18:55 -03:00
|
|
|
// clock provides methods to work with time and timers.
|
|
|
|
|
clock clock.Clock
|
|
|
|
|
|
2025-03-28 00:21:20 -03:00
|
|
|
// 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
|
2024-07-31 12:07:18 -03:00
|
|
|
|
2024-07-31 14:08:49 -03:00
|
|
|
// 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
|
|
|
|
|
|
2024-06-27 14:10:24 -03:00
|
|
|
// 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
|
2024-06-24 23:18:16 -03:00
|
|
|
|
2024-08-13 00:34:16 -03:00
|
|
|
// 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
|
|
|
|
|
|
2024-06-24 23:18:16 -03:00
|
|
|
// 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
|
2024-07-11 23:53:12 -03:00
|
|
|
|
2024-08-27 11:44:28 -03:00
|
|
|
// publishErrorHandler is a function that handles transaction publishing
|
|
|
|
|
// error. By default, it logs all errors as warnings, but "insufficient
|
|
|
|
|
// fee" as Info.
|
|
|
|
|
publishErrorHandler PublishErrorHandler
|
2025-04-01 12:52:57 -03:00
|
|
|
|
|
|
|
|
// presignedHelper provides methods used when presigned batches are
|
|
|
|
|
// enabled.
|
|
|
|
|
presignedHelper PresignedHelper
|
2025-06-17 14:01:59 -03:00
|
|
|
|
|
|
|
|
// 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{}
|
2024-06-21 23:19:51 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BatcherConfig holds batcher configuration.
|
|
|
|
|
type BatcherConfig struct {
|
2024-08-03 23:18:55 -03:00
|
|
|
// clock provides methods to work with time and timers.
|
|
|
|
|
clock clock.Clock
|
|
|
|
|
|
2025-03-28 00:21:20 -03:00
|
|
|
// 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
|
2024-07-31 12:07:18 -03:00
|
|
|
|
2024-07-31 14:08:49 -03:00
|
|
|
// 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
|
|
|
|
|
|
2024-06-27 14:10:24 -03:00
|
|
|
// 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
|
2024-06-24 23:18:16 -03:00
|
|
|
|
2024-08-13 00:34:16 -03:00
|
|
|
// 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
|
|
|
|
|
|
2024-06-24 23:18:16 -03:00
|
|
|
// 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
|
2024-07-11 23:53:12 -03:00
|
|
|
|
2024-08-27 11:44:28 -03:00
|
|
|
// publishErrorHandler is a function that handles transaction publishing
|
|
|
|
|
// error. By default, it logs all errors as warnings, but "insufficient
|
|
|
|
|
// fee" as Info.
|
|
|
|
|
publishErrorHandler PublishErrorHandler
|
2025-04-01 12:52:57 -03:00
|
|
|
|
|
|
|
|
// presignedHelper provides methods used when presigned batches are
|
|
|
|
|
// enabled.
|
|
|
|
|
presignedHelper PresignedHelper
|
2025-06-17 14:01:59 -03:00
|
|
|
|
|
|
|
|
// 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{}
|
2024-06-21 23:19:51 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BatcherOption configures batcher behaviour.
|
|
|
|
|
type BatcherOption func(*BatcherConfig)
|
|
|
|
|
|
2024-08-03 23:18:55 -03:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-31 12:07:18 -03:00
|
|
|
// 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.
|
2025-03-28 00:21:20 -03:00
|
|
|
func WithInitialDelay(provider InitialDelayProvider) BatcherOption {
|
2024-07-31 12:07:18 -03:00
|
|
|
return func(cfg *BatcherConfig) {
|
2025-03-28 00:21:20 -03:00
|
|
|
cfg.initialDelayProvider = provider
|
2024-07-31 12:07:18 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-31 14:08:49 -03:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-27 14:10:24 -03:00
|
|
|
// 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 {
|
2024-06-21 23:19:51 -03:00
|
|
|
return func(cfg *BatcherConfig) {
|
2024-06-27 14:10:24 -03:00
|
|
|
cfg.customFeeRate = customFeeRate
|
2024-06-21 23:19:51 -03:00
|
|
|
}
|
2023-08-23 17:44:14 +03:00
|
|
|
}
|
|
|
|
|
|
2024-08-13 00:34:16 -03:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-24 23:18:16 -03:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-27 11:44:28 -03:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-01 12:52:57 -03:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-17 14:01:59 -03:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-23 17:44:14 +03:00
|
|
|
// NewBatcher creates a new Batcher instance.
|
|
|
|
|
func NewBatcher(wallet lndclient.WalletKitClient,
|
|
|
|
|
chainNotifier lndclient.ChainNotifierClient,
|
|
|
|
|
signerClient lndclient.SignerClient, musig2ServerSigner MuSig2SignSweep,
|
|
|
|
|
verifySchnorrSig VerifySchnorrSig, chainparams *chaincfg.Params,
|
2024-06-21 23:19:51 -03:00
|
|
|
store BatcherStore, sweepStore SweepFetcher,
|
|
|
|
|
opts ...BatcherOption) *Batcher {
|
|
|
|
|
|
2025-06-17 14:01:59 -03:00
|
|
|
badTx1, err := chainhash.NewHashFromStr(
|
|
|
|
|
"7028bdac753a254785d29506f311abcda323706b531345105f38999" +
|
|
|
|
|
"aecd6f3d1",
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-13 00:34:16 -03:00
|
|
|
cfg := BatcherConfig{
|
|
|
|
|
// By default, loop/labels.LoopOutBatchSweepSuccess is used
|
|
|
|
|
// to label sweep transactions.
|
|
|
|
|
txLabeler: labels.LoopOutBatchSweepSuccess,
|
2024-08-27 11:44:28 -03:00
|
|
|
|
|
|
|
|
// publishErrorHandler is a function that handles transaction
|
|
|
|
|
// publishing error. By default, it logs all errors as warnings,
|
|
|
|
|
// but "insufficient fee" as Info.
|
|
|
|
|
publishErrorHandler: defaultPublishErrorLogger,
|
2025-06-17 14:01:59 -03:00
|
|
|
|
|
|
|
|
skippedTxns: map[chainhash.Hash]struct{}{
|
|
|
|
|
*badTx1: {},
|
|
|
|
|
},
|
2024-08-13 00:34:16 -03:00
|
|
|
}
|
2024-06-21 23:19:51 -03:00
|
|
|
for _, opt := range opts {
|
|
|
|
|
opt(&cfg)
|
|
|
|
|
}
|
2023-08-23 17:44:14 +03:00
|
|
|
|
2024-08-03 23:18:55 -03:00
|
|
|
// If WithClock was not provided, use default clock.
|
|
|
|
|
if cfg.clock == nil {
|
|
|
|
|
cfg.clock = clock.NewDefaultClock()
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-24 23:18:16 -03:00
|
|
|
if cfg.customMuSig2Signer != nil && musig2ServerSigner != nil {
|
|
|
|
|
panic("customMuSig2Signer must not be used with " +
|
|
|
|
|
"musig2ServerSigner")
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-23 17:44:14 +03:00
|
|
|
return &Batcher{
|
2025-03-28 00:21:20 -03:00
|
|
|
batches: make(map[int32]*batch),
|
2025-04-30 13:53:56 -03:00
|
|
|
addSweepsChan: make(chan *addSweepsRequest),
|
2025-03-28 00:21:20 -03:00
|
|
|
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,
|
2025-04-01 12:52:57 -03:00
|
|
|
presignedHelper: cfg.presignedHelper,
|
2025-06-17 14:01:59 -03:00
|
|
|
skippedTxns: cfg.skippedTxns,
|
2023-08-23 17:44:14 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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()
|
2024-05-24 10:52:26 +02:00
|
|
|
close(b.quit)
|
2023-08-23 17:44:14 +03:00
|
|
|
|
|
|
|
|
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 {
|
2026-05-15 18:24:55 -05:00
|
|
|
if ctxErr := runCtx.Err(); ctxErr != nil {
|
|
|
|
|
infof("FetchUnconfirmedBatches failed during shutdown, "+
|
|
|
|
|
"returning %v instead of %v.", ctxErr, err)
|
|
|
|
|
|
|
|
|
|
return ctxErr
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-23 17:44:14 +03:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, batch := range batches {
|
|
|
|
|
err := b.spinUpBatchFromDB(runCtx, batch)
|
|
|
|
|
if err != nil {
|
2026-05-15 18:24:55 -05:00
|
|
|
if ctxErr := runCtx.Err(); ctxErr != nil {
|
|
|
|
|
infof("spinUpBatchFromDB failed during shutdown, "+
|
|
|
|
|
"returning %v instead of %v.", ctxErr, err)
|
|
|
|
|
|
|
|
|
|
return ctxErr
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-23 17:44:14 +03:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-24 10:52:54 +02:00
|
|
|
// Signal that the batcher has been initialized.
|
|
|
|
|
close(b.initDone)
|
|
|
|
|
|
2023-08-23 17:44:14 +03:00
|
|
|
for {
|
|
|
|
|
select {
|
2025-04-30 13:53:56 -03:00
|
|
|
case req := <-b.addSweepsChan:
|
2025-10-01 08:44:36 +02:00
|
|
|
err = b.handleSweeps(
|
|
|
|
|
runCtx, req.sweeps, req.notifier, req.fast,
|
|
|
|
|
)
|
2023-08-23 17:44:14 +03:00
|
|
|
if err != nil {
|
2026-05-15 18:24:55 -05:00
|
|
|
if ctxErr := runCtx.Err(); ctxErr != nil {
|
|
|
|
|
infof("handleSweeps failed during shutdown, "+
|
|
|
|
|
"returning %v instead of %v.",
|
|
|
|
|
ctxErr, err)
|
|
|
|
|
|
|
|
|
|
return ctxErr
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-01 00:26:01 -03:00
|
|
|
warnf("handleSweeps failed: %v.", err)
|
2025-02-26 00:13:22 -03:00
|
|
|
|
2023-08-23 17:44:14 +03:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-25 23:42:27 -03:00
|
|
|
case testReq := <-b.testReqs:
|
|
|
|
|
testReq.handler()
|
|
|
|
|
close(testReq.quit)
|
|
|
|
|
|
2023-08-23 17:44:14 +03:00
|
|
|
case err := <-b.errChan:
|
2026-05-15 18:24:55 -05:00
|
|
|
if ctxErr := runCtx.Err(); ctxErr != nil {
|
|
|
|
|
infof("Batcher received an error during shutdown, "+
|
|
|
|
|
"returning %v instead of %v.",
|
|
|
|
|
ctxErr, err)
|
|
|
|
|
|
|
|
|
|
return ctxErr
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-26 00:13:22 -03:00
|
|
|
warnf("Batcher received an error: %v.", err)
|
|
|
|
|
|
2023-08-23 17:44:14 +03:00
|
|
|
return err
|
|
|
|
|
|
|
|
|
|
case <-runCtx.Done():
|
2025-02-26 00:13:22 -03:00
|
|
|
infof("Stopping Batcher: run context cancelled.")
|
|
|
|
|
|
2023-08-23 17:44:14 +03:00
|
|
|
return runCtx.Err()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-01 12:52:57 -03:00
|
|
|
// 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
|
2025-07-18 11:07:40 +02:00
|
|
|
// 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.
|
2025-04-01 12:52:57 -03:00
|
|
|
func (b *Batcher) PresignSweepsGroup(ctx context.Context, inputs []Input,
|
2025-07-18 11:07:40 +02:00
|
|
|
sweepTimeout int32, destAddress btcutil.Address,
|
|
|
|
|
changeOutput *wire.TxOut) error {
|
2025-04-01 12:52:57 -03:00
|
|
|
|
|
|
|
|
if len(inputs) == 0 {
|
|
|
|
|
return fmt.Errorf("no inputs passed to PresignSweepsGroup")
|
|
|
|
|
}
|
|
|
|
|
if b.presignedHelper == nil {
|
|
|
|
|
return fmt.Errorf("presignedHelper is not installed")
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 19:15:59 -05:00
|
|
|
if err := b.shutdownOrCancelErrIfAny(ctx); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-01 12:52:57 -03:00
|
|
|
// Find the feerate needed to get into next block. Use conf_target=2,
|
|
|
|
|
nextBlockFeeRate, err := b.wallet.EstimateFeeRate(ctx, 2)
|
|
|
|
|
if err != nil {
|
2026-05-16 19:15:59 -05:00
|
|
|
if exitErr := b.shutdownOrCancelErrIfAny(ctx); exitErr != nil {
|
|
|
|
|
infof("PresignSweepsGroup EstimateFeeRate failed "+
|
|
|
|
|
"during shutdown, returning %v instead of %v.",
|
|
|
|
|
exitErr, err)
|
|
|
|
|
|
|
|
|
|
return exitErr
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-01 12:52:57 -03:00
|
|
|
return fmt.Errorf("failed to get nextBlockFeeRate: %w", err)
|
|
|
|
|
}
|
2025-07-22 11:23:58 +02:00
|
|
|
minRelayFeeRate, err := b.wallet.MinRelayFee(ctx)
|
|
|
|
|
if err != nil {
|
2026-05-16 19:15:59 -05:00
|
|
|
if exitErr := b.shutdownOrCancelErrIfAny(ctx); exitErr != nil {
|
|
|
|
|
infof("PresignSweepsGroup MinRelayFee failed during "+
|
|
|
|
|
"shutdown, returning %v instead of %v.",
|
|
|
|
|
exitErr, err)
|
|
|
|
|
|
|
|
|
|
return exitErr
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-22 11:23:58 +02:00
|
|
|
return fmt.Errorf("failed to get minRelayFeeRate: %w", err)
|
|
|
|
|
}
|
2025-05-14 23:36:15 -03:00
|
|
|
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)
|
2025-04-01 12:52:57 -03:00
|
|
|
|
|
|
|
|
sweeps := make([]sweep, len(inputs))
|
|
|
|
|
for i, input := range inputs {
|
|
|
|
|
sweeps[i] = sweep{
|
|
|
|
|
outpoint: input.Outpoint,
|
|
|
|
|
value: input.Value,
|
|
|
|
|
timeout: sweepTimeout,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-18 11:07:40 +02:00
|
|
|
// Set the change output on the primary group sweep.
|
|
|
|
|
sweeps[0].change = changeOutput
|
|
|
|
|
|
2025-04-01 12:52:57 -03:00
|
|
|
// The sweeps are ordered inside the group, the first one is the primary
|
|
|
|
|
// outpoint in the batch.
|
|
|
|
|
primarySweepID := sweeps[0].outpoint
|
|
|
|
|
|
2026-05-16 19:15:59 -05:00
|
|
|
err = presign(
|
2025-04-01 12:52:57 -03:00
|
|
|
ctx, b.presignedHelper, destAddress, primarySweepID, sweeps,
|
2025-07-22 11:23:58 +02:00
|
|
|
nextBlockFeeRate, minRelayFeeRate,
|
2025-04-01 12:52:57 -03:00
|
|
|
)
|
2026-05-16 19:15:59 -05:00
|
|
|
if err != nil {
|
|
|
|
|
if exitErr := b.shutdownOrCancelErrIfAny(ctx); exitErr != nil {
|
|
|
|
|
infof("PresignSweepsGroup presign failed during "+
|
|
|
|
|
"shutdown, returning %v instead of %v.",
|
|
|
|
|
exitErr, err)
|
|
|
|
|
|
|
|
|
|
return exitErr
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
2025-04-01 12:52:57 -03:00
|
|
|
}
|
|
|
|
|
|
2025-04-30 13:53:56 -03:00
|
|
|
// 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 {
|
2026-04-28 12:28:29 +02:00
|
|
|
// If the batcher or the caller is shutting down, quit now.
|
|
|
|
|
err := b.shutdownOrCancelErrIfAny(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
2025-04-30 13:53:56 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sweeps, err := b.fetchSweeps(ctx, *sweepReq)
|
|
|
|
|
if err != nil {
|
2026-04-28 12:28:29 +02:00
|
|
|
if exitErr := b.shutdownOrCancelErrIfAny(ctx); exitErr != nil {
|
|
|
|
|
infof("fetchSweeps failed during shutdown, returning "+
|
|
|
|
|
"%v instead of %v.", exitErr, err)
|
|
|
|
|
|
|
|
|
|
return exitErr
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-30 13:53:56 -03:00
|
|
|
return fmt.Errorf("fetchSweeps failed: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-01 12:52:57 -03:00
|
|
|
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 {
|
2026-04-28 12:28:29 +02:00
|
|
|
if exitErr := b.shutdownOrCancelErrIfAny(ctx); exitErr != nil {
|
|
|
|
|
infof("GetSweepStatus failed for sweep %v during "+
|
|
|
|
|
"shutdown, returning %v instead of %v.",
|
|
|
|
|
sweep.outpoint, exitErr, err)
|
|
|
|
|
|
|
|
|
|
return exitErr
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-01 12:52:57 -03:00
|
|
|
return fmt.Errorf("failed to get the status of sweep %v: %w",
|
|
|
|
|
sweep.outpoint, err)
|
|
|
|
|
}
|
2025-10-01 08:44:36 +02:00
|
|
|
var (
|
|
|
|
|
parentBatch *dbBatch
|
|
|
|
|
fullyConfirmed bool
|
|
|
|
|
)
|
2025-04-01 12:52:57 -03:00
|
|
|
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.
|
2025-10-01 08:44:36 +02:00
|
|
|
parentBatch, err = b.store.GetParentBatch(ctx, sweep.outpoint)
|
2025-04-01 12:52:57 -03:00
|
|
|
if err != nil {
|
2026-04-28 12:28:29 +02:00
|
|
|
if exitErr := b.shutdownOrCancelErrIfAny(ctx); exitErr != nil {
|
|
|
|
|
infof("GetParentBatch failed for sweep %v "+
|
|
|
|
|
"during shutdown, returning %v instead "+
|
|
|
|
|
"of %v.", sweep.outpoint, exitErr, err)
|
|
|
|
|
|
|
|
|
|
return exitErr
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-01 12:52:57 -03:00
|
|
|
return fmt.Errorf("unable to get parent batch for "+
|
|
|
|
|
"sweep %x: %w", sweep.swapHash[:6], err)
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-08 20:44:30 -03:00
|
|
|
if parentBatch.Confirmed {
|
2025-04-01 12:52:57 -03:00
|
|
|
fullyConfirmed = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-22 11:23:58 +02:00
|
|
|
minRelayFeeRate, err := b.wallet.MinRelayFee(ctx)
|
|
|
|
|
if err != nil {
|
2026-04-28 12:28:29 +02:00
|
|
|
if exitErr := b.shutdownOrCancelErrIfAny(ctx); exitErr != nil {
|
|
|
|
|
infof("MinRelayFee failed during shutdown, returning "+
|
|
|
|
|
"%v instead of %v.", exitErr, err)
|
|
|
|
|
|
|
|
|
|
return exitErr
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-22 11:23:58 +02:00
|
|
|
return fmt.Errorf("failed to get min relay fee: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-01 12:52:57 -03:00
|
|
|
// If this is a presigned mode, make sure PresignSweepsGroup was called.
|
2025-04-26 01:01:39 -03:00
|
|
|
// We skip the check for reorg-safely confirmed sweeps, because their
|
|
|
|
|
// presigned transactions were already cleaned up from the store.
|
2025-04-01 12:52:57 -03:00
|
|
|
if sweep.presigned && !fullyConfirmed {
|
|
|
|
|
err := ensurePresigned(
|
2025-07-22 11:23:58 +02:00
|
|
|
ctx, sweeps, b.presignedHelper, minRelayFeeRate,
|
|
|
|
|
b.chainParams,
|
2025-04-01 12:52:57 -03:00
|
|
|
)
|
|
|
|
|
if err != nil {
|
2026-04-28 12:28:29 +02:00
|
|
|
if exitErr := b.shutdownOrCancelErrIfAny(ctx); exitErr != nil {
|
|
|
|
|
infof("ensurePresigned failed for primary "+
|
|
|
|
|
"sweep %v during shutdown, returning %v "+
|
|
|
|
|
"instead of %v.", sweep.outpoint,
|
|
|
|
|
exitErr, err)
|
|
|
|
|
|
|
|
|
|
return exitErr
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-01 12:52:57 -03:00
|
|
|
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, "+
|
2025-04-26 01:01:39 -03:00
|
|
|
"presigned=%v, fully_confirmed=%v", len(sweeps),
|
|
|
|
|
sweep.swapHash[:6], sweep.presigned, completed)
|
2025-04-01 12:52:57 -03:00
|
|
|
|
2025-04-30 13:53:56 -03:00
|
|
|
req := &addSweepsRequest{
|
2025-10-03 00:04:59 -03:00
|
|
|
sweeps: sweeps,
|
|
|
|
|
notifier: sweepReq.Notifier,
|
2025-10-01 08:44:36 +02:00
|
|
|
fast: sweepReq.Fast,
|
2025-04-30 13:53:56 -03:00
|
|
|
}
|
|
|
|
|
|
2023-08-23 17:44:14 +03:00
|
|
|
select {
|
2025-04-30 13:53:56 -03:00
|
|
|
case b.addSweepsChan <- req:
|
2023-08-23 17:44:14 +03:00
|
|
|
return nil
|
|
|
|
|
|
|
|
|
|
case <-b.quit:
|
|
|
|
|
return ErrBatcherShuttingDown
|
2026-04-28 12:28:29 +02:00
|
|
|
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return b.shutdownOrCancelErrIfAny(ctx)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// shutdownOrCancelErrIfAny returns the terminal error to use when caller-facing
|
|
|
|
|
// batcher methods race with shutdown or caller cancellation. It returns nil if
|
|
|
|
|
// the operation should continue.
|
|
|
|
|
func (b *Batcher) shutdownOrCancelErrIfAny(ctx context.Context) error {
|
|
|
|
|
select {
|
|
|
|
|
case <-b.quit:
|
|
|
|
|
return ErrBatcherShuttingDown
|
|
|
|
|
|
|
|
|
|
default:
|
2023-08-23 17:44:14 +03:00
|
|
|
}
|
2026-04-28 12:28:29 +02:00
|
|
|
|
|
|
|
|
return ctx.Err()
|
2023-08-23 17:44:14 +03:00
|
|
|
}
|
|
|
|
|
|
2025-02-25 23:42:27 -03:00
|
|
|
// 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():
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-01 00:26:01 -03:00
|
|
|
// 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,
|
2025-10-01 08:44:36 +02:00
|
|
|
notifier *SpendNotifier, fast bool) error {
|
2025-04-01 00:26:01 -03:00
|
|
|
|
|
|
|
|
// 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]
|
|
|
|
|
|
2023-08-23 17:44:14 +03:00
|
|
|
sweep.notifier = notifier
|
|
|
|
|
|
2025-02-25 23:14:36 -03:00
|
|
|
// Check if the sweep is already in a batch. If that is the case, we
|
|
|
|
|
// provide the sweep to that batch and return.
|
2025-10-03 00:04:59 -03:00
|
|
|
readded := false
|
2025-02-25 23:14:36 -03:00
|
|
|
for _, batch := range b.batches {
|
2025-03-25 14:30:52 -03:00
|
|
|
if batch.sweepExists(sweep.outpoint) {
|
2025-04-01 00:26:01 -03:00
|
|
|
accepted, err := batch.addSweeps(ctx, sweeps)
|
2025-11-12 00:54:00 -03:00
|
|
|
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 {
|
2023-08-23 17:44:14 +03:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !accepted {
|
|
|
|
|
return fmt.Errorf("existing sweep %x was not "+
|
2024-05-28 22:35:08 -03:00
|
|
|
"accepted by batch %d",
|
|
|
|
|
sweep.swapHash[:6], batch.id)
|
2023-08-23 17:44:14 +03:00
|
|
|
}
|
2024-05-20 22:37:29 -03:00
|
|
|
|
|
|
|
|
// The sweep was updated in the batch, our job is done.
|
2025-10-03 00:04:59 -03:00
|
|
|
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)
|
|
|
|
|
|
2025-11-12 02:33:43 -03:00
|
|
|
// Batch + sweeps are persisted atomically, so if the sweep
|
|
|
|
|
// shows as completed its parent batch must be confirmed.
|
2025-10-03 00:04:59 -03:00
|
|
|
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,
|
|
|
|
|
)
|
2023-08-23 17:44:14 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-01 08:44:36 +02:00
|
|
|
// If fast is set, we spin up a new batch which is published
|
|
|
|
|
// immediately.
|
|
|
|
|
if fast {
|
|
|
|
|
return b.spinUpNewBatch(ctx, sweeps, true)
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-15 12:26:43 -03:00
|
|
|
// Try to run the greedy algorithm of batch selection to minimize costs.
|
2025-10-03 00:04:59 -03:00
|
|
|
err = b.greedyAddSweeps(ctx, sweeps)
|
2024-07-15 12:26:43 -03:00
|
|
|
if err == nil {
|
|
|
|
|
// The greedy algorithm succeeded.
|
|
|
|
|
return nil
|
2024-06-28 21:34:55 -03:00
|
|
|
}
|
|
|
|
|
|
2025-02-26 00:13:22 -03:00
|
|
|
warnf("Greedy batch selection algorithm failed for sweep %x: %v."+
|
|
|
|
|
" Falling back to old approach.", sweep.swapHash[:6], err)
|
2024-07-15 12:26:43 -03:00
|
|
|
|
2023-08-23 17:44:14 +03:00
|
|
|
// If one of the batches accepts the sweep, we provide it to that batch.
|
|
|
|
|
for _, batch := range b.batches {
|
2025-04-01 00:26:01 -03:00
|
|
|
accepted, err := batch.addSweeps(ctx, sweeps)
|
2024-05-24 10:36:42 +02:00
|
|
|
if err != nil && !errors.Is(err, ErrBatchShuttingDown) {
|
2023-08-23 17:44:14 +03:00
|
|
|
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.
|
2025-10-01 08:44:36 +02:00
|
|
|
return b.spinUpNewBatch(ctx, sweeps, false)
|
2024-06-28 21:34:55 -03:00
|
|
|
}
|
|
|
|
|
|
2025-04-01 12:52:57 -03:00
|
|
|
// spinUpNewBatch creates new batch, starts it and adds the sweeps to it. If
|
|
|
|
|
// presigned mode is enabled, the result also depends on outcome of
|
2025-05-14 21:40:42 -03:00
|
|
|
// presignedHelper.SignTx.
|
2025-10-01 08:44:36 +02:00
|
|
|
func (b *Batcher) spinUpNewBatch(ctx context.Context, sweeps []*sweep,
|
|
|
|
|
fast bool) error {
|
|
|
|
|
|
2024-06-28 21:34:55 -03:00
|
|
|
// Spin up a fresh batch.
|
2025-10-01 08:44:36 +02:00
|
|
|
newBatch, err := b.spinUpBatch(ctx, fast)
|
2023-08-23 17:44:14 +03:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-01 00:26:01 -03:00
|
|
|
// Add the sweeps to the fresh batch.
|
|
|
|
|
accepted, err := newBatch.addSweeps(ctx, sweeps)
|
2023-08-23 17:44:14 +03:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-01 00:26:01 -03:00
|
|
|
// If the sweeps weren't accepted by the fresh batch something is wrong,
|
2023-08-23 17:44:14 +03:00
|
|
|
// we should return the error.
|
|
|
|
|
if !accepted {
|
|
|
|
|
return fmt.Errorf("sweep %x was not accepted by new batch %d",
|
2025-04-01 00:26:01 -03:00
|
|
|
sweeps[0].swapHash[:6], newBatch.id)
|
2023-08-23 17:44:14 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// spinUpBatch spins up a new batch and returns it.
|
2025-10-01 08:44:36 +02:00
|
|
|
func (b *Batcher) spinUpBatch(ctx context.Context, fast bool) (*batch, error) {
|
2024-06-27 13:04:06 -03:00
|
|
|
cfg := b.newBatchConfig(defaultMaxTimeoutDistance)
|
2023-08-23 17:44:14 +03:00
|
|
|
|
|
|
|
|
switch b.chainParams {
|
|
|
|
|
case &chaincfg.MainNetParams:
|
|
|
|
|
cfg.batchPublishDelay = defaultMainnetPublishDelay
|
|
|
|
|
|
|
|
|
|
default:
|
2024-07-30 23:39:06 -03:00
|
|
|
cfg.batchPublishDelay = defaultTestnetPublishDelay
|
2023-08-23 17:44:14 +03:00
|
|
|
}
|
|
|
|
|
|
2024-07-31 14:08:49 -03:00
|
|
|
if b.publishDelay != 0 {
|
|
|
|
|
if b.publishDelay < 0 {
|
|
|
|
|
return nil, fmt.Errorf("negative publishDelay: %v",
|
|
|
|
|
b.publishDelay)
|
|
|
|
|
}
|
|
|
|
|
cfg.batchPublishDelay = b.publishDelay
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-28 00:21:20 -03:00
|
|
|
cfg.initialDelayProvider = b.initialDelayProvider
|
2025-10-01 08:44:36 +02:00
|
|
|
if cfg.initialDelayProvider == nil || fast {
|
2025-03-28 00:21:20 -03:00
|
|
|
cfg.initialDelayProvider = zeroInitialDelay
|
2024-07-31 12:07:18 -03:00
|
|
|
}
|
|
|
|
|
|
2024-06-27 13:19:37 -03:00
|
|
|
batchKit := b.newBatchKit()
|
2023-08-23 17:44:14 +03:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2026-03-05 12:18:05 +01:00
|
|
|
b.wg.Go(func() {
|
2023-08-23 17:44:14 +03:00
|
|
|
err := batch.Run(ctx)
|
|
|
|
|
if err != nil {
|
2025-03-21 00:32:02 -03:00
|
|
|
b.writeToErrChan(
|
|
|
|
|
ctx, fmt.Errorf("new batch failed: %w", err),
|
|
|
|
|
)
|
2023-08-23 17:44:14 +03:00
|
|
|
}
|
2026-03-05 12:18:05 +01:00
|
|
|
})
|
2023-08-23 17:44:14 +03:00
|
|
|
|
|
|
|
|
return batch, nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-17 14:01:59 -03:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-22 16:11:16 +08:00
|
|
|
// spinUpBatchFromDB spins up a batch that already existed in storage, then
|
2023-08-23 17:44:14 +03:00
|
|
|
// 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
|
|
|
|
|
}
|
2025-06-17 14:01:59 -03:00
|
|
|
dbSweeps = filterDbSweeps(b.skippedTxns, dbSweeps)
|
2023-08-23 17:44:14 +03:00
|
|
|
|
|
|
|
|
if len(dbSweeps) == 0 {
|
2025-02-26 00:13:22 -03:00
|
|
|
infof("skipping restored batch %d as it has no sweeps",
|
2024-05-24 10:36:42 +02:00
|
|
|
batch.id)
|
|
|
|
|
|
2025-06-17 14:01:59 -03:00
|
|
|
// It is safe to cancel this empty batch as it has no sweeps
|
|
|
|
|
// that are not skipped.
|
|
|
|
|
err := b.store.CancelBatch(ctx, batch.id)
|
2024-05-24 10:36:42 +02:00
|
|
|
if err != nil {
|
2025-02-26 00:13:22 -03:00
|
|
|
warnf("unable to drop empty batch %d: %v",
|
2024-05-24 10:36:42 +02:00
|
|
|
batch.id, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
2023-08-23 17:44:14 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
primarySweep := dbSweeps[0]
|
|
|
|
|
|
2025-03-25 14:30:52 -03:00
|
|
|
sweeps := make(map[wire.OutPoint]sweep)
|
2023-08-23 17:44:14 +03:00
|
|
|
|
2024-06-21 23:20:52 -03:00
|
|
|
// Collect feeRate from sweeps and stored batch.
|
|
|
|
|
feeRate := batch.rbfCache.FeeRate
|
|
|
|
|
|
2023-08-23 17:44:14 +03:00
|
|
|
for _, dbSweep := range dbSweeps {
|
2024-05-30 13:05:47 -03:00
|
|
|
sweep, err := b.convertSweep(ctx, dbSweep)
|
2023-08-23 17:44:14 +03:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-25 14:30:52 -03:00
|
|
|
sweeps[sweep.outpoint] = *sweep
|
2024-06-21 23:20:52 -03:00
|
|
|
|
|
|
|
|
// Set minFeeRate to max(sweep.minFeeRate) for all sweeps.
|
|
|
|
|
if feeRate < sweep.minFeeRate {
|
|
|
|
|
feeRate = sweep.minFeeRate
|
|
|
|
|
}
|
2023-08-23 17:44:14 +03:00
|
|
|
}
|
|
|
|
|
|
2024-05-24 10:36:42 +02:00
|
|
|
rbfCache := rbfCache{
|
|
|
|
|
LastHeight: batch.rbfCache.LastHeight,
|
2024-06-21 23:20:52 -03:00
|
|
|
FeeRate: feeRate,
|
2024-05-24 10:36:42 +02:00
|
|
|
}
|
|
|
|
|
|
2024-05-28 22:35:08 -03:00
|
|
|
logger := batchPrefixLogger(fmt.Sprintf("%d", batch.id))
|
|
|
|
|
|
2024-06-27 13:19:37 -03:00
|
|
|
batchKit := b.newBatchKit()
|
|
|
|
|
batchKit.id = batch.id
|
|
|
|
|
batchKit.batchTxid = batch.batchTxid
|
|
|
|
|
batchKit.batchPkScript = batch.batchPkScript
|
|
|
|
|
batchKit.state = batch.state
|
2025-03-25 14:30:52 -03:00
|
|
|
batchKit.primaryID = primarySweep.Outpoint
|
2024-06-27 13:19:37 -03:00
|
|
|
batchKit.sweeps = sweeps
|
|
|
|
|
batchKit.rbfCache = rbfCache
|
|
|
|
|
batchKit.log = logger
|
2023-08-23 17:44:14 +03:00
|
|
|
|
2024-06-27 13:04:06 -03:00
|
|
|
cfg := b.newBatchConfig(batch.cfg.maxTimeoutDistance)
|
2024-05-24 10:36:42 +02:00
|
|
|
|
2024-07-31 12:07:18 -03:00
|
|
|
// 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).
|
2025-03-28 00:21:20 -03:00
|
|
|
cfg.initialDelayProvider = zeroInitialDelay
|
|
|
|
|
|
2024-05-29 15:07:32 -03:00
|
|
|
newBatch, err := NewBatchFromDB(cfg, batchKit)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed in NewBatchFromDB: %w", err)
|
|
|
|
|
}
|
2023-08-23 17:44:14 +03:00
|
|
|
|
|
|
|
|
// We add the batch to our map of batches and start it.
|
|
|
|
|
b.batches[batch.id] = newBatch
|
|
|
|
|
|
2026-03-05 12:18:05 +01:00
|
|
|
b.wg.Go(func() {
|
2023-08-23 17:44:14 +03:00
|
|
|
err := newBatch.Run(ctx)
|
|
|
|
|
if err != nil {
|
2025-03-21 00:32:02 -03:00
|
|
|
b.writeToErrChan(
|
|
|
|
|
ctx, fmt.Errorf("db batch failed: %w", err),
|
|
|
|
|
)
|
2023-08-23 17:44:14 +03:00
|
|
|
}
|
2026-03-05 12:18:05 +01:00
|
|
|
})
|
2023-08-23 17:44:14 +03:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2025-04-08 20:44:30 -03:00
|
|
|
if bch.Confirmed {
|
2023-08-23 17:44:14 +03:00
|
|
|
batch.state = Confirmed
|
2025-04-08 20:44:30 -03:00
|
|
|
} else {
|
|
|
|
|
// We don't store Closed state separately in DB.
|
|
|
|
|
// If the batch is closed (included into a block, but
|
2025-04-26 01:01:39 -03:00
|
|
|
// 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
|
2025-04-08 20:44:30 -03:00
|
|
|
// notification and will be added to new batch.
|
|
|
|
|
batch.state = Open
|
2023-08-23 17:44:14 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
batch.batchTxid = &bch.BatchTxid
|
|
|
|
|
batch.batchPkScript = bch.BatchPkScript
|
|
|
|
|
|
|
|
|
|
rbfCache := rbfCache{
|
|
|
|
|
LastHeight: bch.LastRbfHeight,
|
|
|
|
|
FeeRate: chainfee.SatPerKWeight(bch.LastRbfSatPerKw),
|
|
|
|
|
}
|
|
|
|
|
batch.rbfCache = rbfCache
|
|
|
|
|
|
2024-06-27 13:04:06 -03:00
|
|
|
bchCfg := b.newBatchConfig(bch.MaxTimeoutDistance)
|
2023-08-23 17:44:14 +03:00
|
|
|
batch.cfg = &bchCfg
|
|
|
|
|
|
|
|
|
|
batches = append(batches, &batch)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return batches, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// monitorSpendAndNotify monitors the spend of a specific outpoint and writes
|
2025-04-08 23:15:23 -03:00
|
|
|
// 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.
|
2025-04-27 01:08:41 -03:00
|
|
|
func (b *Batcher) monitorSpendAndNotify(ctx context.Context, sweeps []*sweep,
|
2024-07-11 11:23:02 +02:00
|
|
|
parentBatchID int32, notifier *SpendNotifier) error {
|
2023-08-23 17:44:14 +03:00
|
|
|
|
2025-04-26 01:01:39 -03:00
|
|
|
// If the caller has not provided a notifier, stop.
|
|
|
|
|
if notifier == nil || *notifier == (SpendNotifier{}) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-23 17:44:14 +03:00
|
|
|
spendCtx, cancel := context.WithCancel(ctx)
|
|
|
|
|
|
2024-02-02 22:37:54 +01:00
|
|
|
// Then we get the total amount that was swept by the batch.
|
2024-07-11 11:23:02 +02:00
|
|
|
totalSwept, err := b.store.TotalSweptAmount(ctx, parentBatchID)
|
2024-02-02 22:37:54 +01:00
|
|
|
if err != nil {
|
2025-04-08 18:43:21 -03:00
|
|
|
cancel()
|
|
|
|
|
|
2024-02-02 22:37:54 +01:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-27 01:08:41 -03:00
|
|
|
// Find the primarySweepID.
|
|
|
|
|
dbSweeps, err := b.store.FetchBatchSweeps(ctx, parentBatchID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
cancel()
|
|
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
primarySweepID := dbSweeps[0].Outpoint
|
|
|
|
|
|
|
|
|
|
sweep := sweeps[0]
|
|
|
|
|
|
2023-08-23 17:44:14 +03:00
|
|
|
spendChan, spendErr, err := b.chainNotifier.RegisterSpendNtfn(
|
|
|
|
|
spendCtx, &sweep.outpoint, sweep.htlc.PkScript,
|
|
|
|
|
sweep.initiationHeight,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
2025-04-08 18:43:21 -03:00
|
|
|
cancel()
|
|
|
|
|
|
2024-02-02 22:04:09 +01:00
|
|
|
return err
|
2023-08-23 17:44:14 +03:00
|
|
|
}
|
|
|
|
|
|
2026-03-05 12:18:05 +01:00
|
|
|
b.wg.Go(func() {
|
2025-04-08 18:43:21 -03:00
|
|
|
defer cancel()
|
2025-02-26 00:13:22 -03:00
|
|
|
infof("Batcher monitoring spend for swap %x",
|
2024-02-02 22:04:09 +01:00
|
|
|
sweep.swapHash[:6])
|
2023-08-23 17:44:14 +03:00
|
|
|
|
2025-04-08 18:48:34 -03:00
|
|
|
select {
|
|
|
|
|
case spend := <-spendChan:
|
|
|
|
|
spendTx := spend.SpendingTx
|
2025-04-27 01:08:41 -03:00
|
|
|
|
2025-04-08 18:48:34 -03:00
|
|
|
// Calculate the fee portion that each sweep should pay
|
|
|
|
|
// for the batch.
|
|
|
|
|
feePortionPerSweep, roundingDifference :=
|
|
|
|
|
getFeePortionForSweep(
|
|
|
|
|
spendTx, len(spendTx.TxIn),
|
|
|
|
|
totalSwept,
|
2024-05-28 22:35:08 -03:00
|
|
|
)
|
|
|
|
|
|
2025-04-27 01:08:41 -03:00
|
|
|
// 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,
|
|
|
|
|
)
|
|
|
|
|
}
|
2025-03-21 00:32:02 -03:00
|
|
|
|
2025-04-08 18:48:34 -03:00
|
|
|
// Notify the requester of the spend with the spend
|
|
|
|
|
// details, including the fee portion for this
|
|
|
|
|
// particular sweep.
|
|
|
|
|
spendDetail := &SpendDetail{
|
|
|
|
|
Tx: spendTx,
|
2025-04-27 01:08:41 -03:00
|
|
|
OnChainFeePortion: fee,
|
2025-04-08 18:48:34 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
// Try to write the update to the notification channel.
|
|
|
|
|
case notifier.SpendChan <- spendDetail:
|
2025-04-08 23:15:23 -03:00
|
|
|
err := b.monitorConfAndNotify(
|
|
|
|
|
ctx, sweep, notifier, spendTx,
|
2025-04-27 01:08:41 -03:00
|
|
|
fee,
|
2025-04-08 23:15:23 -03:00
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
b.writeToErrChan(
|
|
|
|
|
ctx, fmt.Errorf("monitor conf "+
|
|
|
|
|
"failed: %w", err),
|
|
|
|
|
)
|
|
|
|
|
}
|
2023-08-23 17:44:14 +03:00
|
|
|
|
2025-04-08 12:34:42 -03:00
|
|
|
// If a quit signal was provided by the swap, continue.
|
2024-02-02 22:04:09 +01:00
|
|
|
case <-notifier.QuitChan:
|
2023-08-23 17:44:14 +03:00
|
|
|
|
2025-04-08 12:34:42 -03:00
|
|
|
// If the context was canceled, stop.
|
2024-02-02 22:04:09 +01:00
|
|
|
case <-ctx.Done():
|
|
|
|
|
}
|
2025-04-08 18:48:34 -03:00
|
|
|
|
|
|
|
|
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
|
2023-08-23 17:44:14 +03:00
|
|
|
}
|
2026-03-05 12:18:05 +01:00
|
|
|
})
|
2024-02-02 22:04:09 +01:00
|
|
|
|
|
|
|
|
return nil
|
2023-08-23 17:44:14 +03:00
|
|
|
}
|
|
|
|
|
|
2025-04-08 23:15:23 -03:00
|
|
|
// monitorConfAndNotify monitors the confirmation of a specific transaction and
|
|
|
|
|
// writes the response back to the response channel. It is called if the batch
|
2025-09-19 12:50:25 +02:00
|
|
|
// is reorg-safely confirmed, and we just need to deliver the data back to the
|
2025-04-26 01:01:39 -03:00
|
|
|
// caller though SpendNotifier.
|
2025-04-08 23:15:23 -03:00
|
|
|
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()
|
|
|
|
|
|
2025-09-19 12:50:25 +02:00
|
|
|
if len(spendTx.TxOut) < 1 {
|
|
|
|
|
return fmt.Errorf("expected at least one output in batch")
|
2025-04-08 23:15:23 -03:00
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 12:18:05 +01:00
|
|
|
b.wg.Go(func() {
|
2025-04-08 23:15:23 -03:00
|
|
|
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():
|
|
|
|
|
}
|
2026-03-05 12:18:05 +01:00
|
|
|
})
|
2025-04-08 23:15:23 -03:00
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-21 00:32:02 -03:00
|
|
|
func (b *Batcher) writeToErrChan(ctx context.Context, err error) {
|
2023-08-23 17:44:14 +03:00
|
|
|
select {
|
|
|
|
|
case b.errChan <- err:
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// convertSweep converts a fetched sweep from the database to a sweep that is
|
2024-05-30 13:05:47 -03:00
|
|
|
// ready to be processed by the batcher. It loads swap from loopdb by calling
|
2025-09-19 12:50:25 +02:00
|
|
|
// the method FetchLoopOutSwap.
|
2024-05-30 13:05:47 -03:00
|
|
|
func (b *Batcher) convertSweep(ctx context.Context, dbSweep *dbSweep) (
|
|
|
|
|
*sweep, error) {
|
|
|
|
|
|
2024-06-27 12:48:46 -03:00
|
|
|
return b.loadSweep(ctx, dbSweep.SwapHash, dbSweep.Outpoint,
|
|
|
|
|
dbSweep.Amount)
|
2023-08-23 17:44:14 +03:00
|
|
|
}
|
|
|
|
|
|
2024-05-14 11:36:47 -03:00
|
|
|
// 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)
|
|
|
|
|
}
|
2023-08-23 17:44:14 +03:00
|
|
|
|
2024-05-14 11:36:47 -03:00
|
|
|
// SwapStoreWrapper is LoopOutFetcher wrapper providing SweepFetcher interface.
|
|
|
|
|
type SwapStoreWrapper struct {
|
|
|
|
|
// swapStore is used to load LoopOut swaps from the database.
|
|
|
|
|
swapStore LoopOutFetcher
|
2023-08-23 17:44:14 +03:00
|
|
|
|
2024-05-14 11:36:47 -03:00
|
|
|
// 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.
|
2025-03-25 14:30:52 -03:00
|
|
|
// In LoopOut case, swap hashes are unique.
|
2024-05-14 11:36:47 -03:00
|
|
|
// Implements SweepFetcher interface.
|
|
|
|
|
func (f *SwapStoreWrapper) FetchSweep(ctx context.Context,
|
2025-03-25 14:30:52 -03:00
|
|
|
swapHash lntypes.Hash, _ wire.OutPoint) (*SweepInfo, error) {
|
2024-05-14 11:36:47 -03:00
|
|
|
|
|
|
|
|
swap, err := f.swapStore.FetchLoopOutSwap(ctx, swapHash)
|
2023-08-23 17:44:14 +03:00
|
|
|
if err != nil {
|
2024-06-20 14:13:25 -03:00
|
|
|
return nil, fmt.Errorf("failed to fetch loop out for %x: %w",
|
2023-08-23 17:44:14 +03:00
|
|
|
swapHash[:6], err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
htlc, err := utils.GetHtlc(
|
2024-05-14 11:36:47 -03:00
|
|
|
swapHash, &swap.Contract.SwapContract, f.chainParams,
|
2023-08-23 17:44:14 +03:00
|
|
|
)
|
|
|
|
|
if err != nil {
|
2024-06-20 14:13:25 -03:00
|
|
|
return nil, fmt.Errorf("failed to get htlc: %w", err)
|
2023-08-23 17:44:14 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
swapPaymentAddr, err := utils.ObtainSwapPaymentAddr(
|
2024-05-14 11:36:47 -03:00
|
|
|
swap.Contract.SwapInvoice, f.chainParams,
|
2023-08-23 17:44:14 +03:00
|
|
|
)
|
|
|
|
|
if err != nil {
|
2024-06-20 14:13:25 -03:00
|
|
|
return nil, fmt.Errorf("failed to get payment addr: %w", err)
|
2023-08-23 17:44:14 +03:00
|
|
|
}
|
|
|
|
|
|
2024-05-14 11:36:47 -03:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-01 00:26:01 -03:00
|
|
|
// 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
|
|
|
|
|
}
|
2024-05-14 11:36:47 -03:00
|
|
|
|
2025-04-01 00:26:01 -03:00
|
|
|
return sweeps, nil
|
2024-06-27 12:48:46 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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) {
|
|
|
|
|
|
2025-03-25 14:30:52 -03:00
|
|
|
s, err := b.sweepStore.FetchSweep(ctx, swapHash, outpoint)
|
2024-05-14 11:36:47 -03:00
|
|
|
if err != nil {
|
2024-06-20 14:13:25 -03:00
|
|
|
return nil, fmt.Errorf("failed to fetch sweep data for %x: %w",
|
2024-06-27 12:48:46 -03:00
|
|
|
swapHash[:6], err)
|
2024-05-14 11:36:47 -03:00
|
|
|
}
|
|
|
|
|
|
2025-05-06 14:26:06 -03:00
|
|
|
// 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])
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-15 12:24:57 -03:00
|
|
|
// Find minimum fee rate for the sweep. Use customFeeRate if it is
|
|
|
|
|
// provided, otherwise use wallet's EstimateFeeRate.
|
2025-07-09 19:41:01 -03:00
|
|
|
minFeeRate, err := minimumSweepFeeRate(
|
|
|
|
|
ctx, b.customFeeRate, b.wallet,
|
|
|
|
|
swapHash, outpoint, s.ConfTarget,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
2024-06-27 14:10:24 -03:00
|
|
|
}
|
|
|
|
|
|
2023-08-23 17:44:14 +03:00
|
|
|
return &sweep{
|
2024-06-27 12:48:46 -03:00
|
|
|
swapHash: swapHash,
|
|
|
|
|
outpoint: outpoint,
|
|
|
|
|
value: value,
|
2024-05-14 11:36:47 -03:00
|
|
|
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,
|
2024-06-27 14:10:24 -03:00
|
|
|
minFeeRate: minFeeRate,
|
2024-06-28 21:34:55 -03:00
|
|
|
nonCoopHint: s.NonCoopHint,
|
2025-04-01 12:52:57 -03:00
|
|
|
presigned: s.IsPresigned,
|
2025-07-18 11:07:40 +02:00
|
|
|
change: s.Change,
|
2023-08-23 17:44:14 +03:00
|
|
|
}, nil
|
|
|
|
|
}
|
2024-06-27 13:04:06 -03:00
|
|
|
|
2025-07-09 19:41:01 -03:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-24 02:27:07 -03:00
|
|
|
// 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
|
2025-07-09 19:41:01 -03:00
|
|
|
}
|
2025-07-24 02:27:07 -03:00
|
|
|
|
2025-07-09 19:41:01 -03:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-27 13:04:06 -03:00
|
|
|
// newBatchConfig creates new batch config.
|
|
|
|
|
func (b *Batcher) newBatchConfig(maxTimeoutDistance int32) batchConfig {
|
|
|
|
|
return batchConfig{
|
|
|
|
|
maxTimeoutDistance: maxTimeoutDistance,
|
2025-07-09 19:41:01 -03:00
|
|
|
customFeeRate: b.customFeeRate,
|
2024-08-13 00:34:16 -03:00
|
|
|
txLabeler: b.txLabeler,
|
2024-06-27 13:04:06 -03:00
|
|
|
customMuSig2Signer: b.customMuSig2Signer,
|
2025-04-01 12:52:57 -03:00
|
|
|
presignedHelper: b.presignedHelper,
|
2025-06-17 14:01:59 -03:00
|
|
|
skippedTxns: b.skippedTxns,
|
2024-08-03 23:18:55 -03:00
|
|
|
clock: b.clock,
|
2025-04-01 12:52:57 -03:00
|
|
|
chainParams: b.chainParams,
|
2024-06-27 13:04:06 -03:00
|
|
|
}
|
|
|
|
|
}
|
2024-06-27 13:19:37 -03:00
|
|
|
|
|
|
|
|
// newBatchKit creates new batch kit.
|
|
|
|
|
func (b *Batcher) newBatchKit() batchKit {
|
|
|
|
|
return batchKit{
|
2024-08-27 11:44:28 -03:00
|
|
|
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,
|
2024-06-27 13:19:37 -03:00
|
|
|
}
|
|
|
|
|
}
|