loop/sweepbatcher/sweep_batch.go
Boris Nagaev 54652dc641
sweepbatcher: subtle adjustments for change
- ensurePresigned: use passed minRelayFeeRate instead of chainfee.FeePerKwFloor
 - presign: use minRelayFeeRate for start and minRelayFee
 - presign: make sure minRelayFeeRate is set
 - add tests for presign to test this new behavior; make sure the number of
   transactions is lower if minRelayFeeRate is higher
 - update error message in constructUnsignedTx: use <, not <= (more accurate)
 - use utils.DustLimitForPkScript instead of lnwallet.DustLimitForSize in tests
 - in tests adjust amounts to edge values, add controls
2025-07-29 12:33:56 +02:00

2621 lines
77 KiB
Go

package sweepbatcher
import (
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
"math"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/psbt"
"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/lightninglabs/lndclient"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap"
sweeppkg "github.com/lightninglabs/loop/sweep"
"github.com/lightninglabs/loop/utils"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
)
const (
// defaultFeeRateStep is the default value by which the batch tx's
// fee rate is increased when an rbf is attempted.
defaultFeeRateStep = chainfee.SatPerKWeight(100)
// batchConfHeight is the default confirmation height of the batch
// transaction.
batchConfHeight = 3
// maxFeeToSwapAmtRatio is the maximum fee to swap amount ratio that
// we allow for a batch transaction.
maxFeeToSwapAmtRatio = 0.2
// MaxSweepsPerBatch is the maximum number of sweeps in a single batch.
// It is needed to prevent sweep tx from becoming non-standard. Max
// standard transaction is 400k wu, a non-cooperative input is 393 wu.
MaxSweepsPerBatch = 1000
)
var (
ErrBatchShuttingDown = errors.New("batch shutting down")
)
// sweep stores any data related to sweeping a specific outpoint.
type sweep struct {
// swapHash is the hash of the swap that the sweep belongs to.
// Multiple sweeps may belong to the same swap.
swapHash lntypes.Hash
// outpoint is the outpoint being swept.
outpoint wire.OutPoint
// value is the value of the outpoint being swept.
value btcutil.Amount
// 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
// notifier is a collection of channels used to communicate the status
// of the sweep back to the swap that requested it.
notifier *SpendNotifier
// minFeeRate is minimum fee rate that must be used by a batch of
// the sweep. If it is specified, confTarget is ignored.
minFeeRate chainfee.SatPerKWeight
// 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
// coopFailed is set, if we have tried to spend the sweep cooperatively,
// but it failed. We try to spend a sweep cooperatively only once. This
// status is not persisted in the DB.
coopFailed bool
// presigned is set, if the sweep should be handled in presigned mode.
presigned bool
// change is the optional change output of the sweep.
change *wire.TxOut
}
// batchState is the state of the batch.
type batchState uint8
const (
// Open is the state in which the batch is able to accept new sweeps.
Open batchState = 0
// Closed is the state in which the batch is no longer able to accept
// new sweeps. NOTE: this state exists only in-memory. In the database
// it is stored as Open and converted to Closed after a spend
// notification arrives (quickly after start of Batch.Run).
Closed batchState = 1
// Confirmed is the state in which the batch transaction has reached the
// configured conf height.
Confirmed batchState = 2
)
// batchConfig is the configuration for a batch.
type batchConfig struct {
// maxTimeoutDistance is the maximum timeout distance that 2 distinct
// sweeps can have in the same batch.
maxTimeoutDistance int32
// batchConfTarget is the confirmation target of the batch transaction.
batchConfTarget int32
// 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
// batchPublishDelay is the delay between receiving a new block or
// initial delay completion and publishing the batch transaction.
batchPublishDelay 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
// 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{}
// chainParams are the chain parameters of the chain that is used by
// batches.
chainParams *chaincfg.Params
}
// rbfCache stores data related to our last fee bump.
type rbfCache struct {
// LastHeight is the last height at which we increased our feerate.
LastHeight int32
// FeeRate is the last used fee rate we used to publish a batch tx.
FeeRate chainfee.SatPerKWeight
// SkipNextBump instructs updateRbfRate to skip one fee bumping.
// It is set upon updating FeeRate externally.
SkipNextBump bool
}
// zeroSweepID is default value for sweep.primarySweepID and batchKit.primaryID.
var zeroSweepID wire.OutPoint
// batch is a collection of sweeps that are published together.
type batch struct {
// id is the primary identifier of this batch.
id int32
// state is the current state of the batch.
state batchState
// primarySweepID is the outpoint of the primary sweep in the batch.
primarySweepID wire.OutPoint
// sweeps store the sweeps that this batch currently contains.
sweeps map[wire.OutPoint]sweep
// currentHeight is the current block height.
currentHeight int32
// spendChan is the channel over which spend notifications are received.
spendChan chan *chainntnfs.SpendDetail
// spendErrChan is the channel over which spend notifier errors are
// received.
spendErrChan chan error
// confChan is the channel over which confirmation notifications are
// received.
confChan chan *chainntnfs.TxConfirmation
// reorgChan is the channel over which reorg notifications are received.
reorgChan chan struct{}
// 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 the channel over which errors are received.
errChan chan error
// batchTxid is the transaction that is currently being monitored for
// confirmations.
batchTxid *chainhash.Hash
// batchPkScript is the pkScript of the batch transaction's output.
batchPkScript []byte
// batchAddress is the address of the batch transaction's output.
batchAddress btcutil.Address
// rbfCache stores data related to the RBF fee bumping mechanism.
rbfCache rbfCache
// callEnter is used to sequentialize calls to the batch handler's
// main event loop.
callEnter chan struct{}
// callLeave is used to resume the execution flow of the batch handler's
// main event loop.
callLeave chan struct{}
// stopping signals that the batch is stopping.
stopping chan struct{}
// finished signals that the batch has stopped and all child goroutines
// have finished.
finished chan struct{}
// quit is owned by the parent batcher and signals that the batch must
// stop.
quit chan struct{}
// wallet is the wallet client used to create and publish the batch
// transaction.
wallet lndclient.WalletKitClient
// chainNotifier is the chain notifier client used to monitor the
// blockchain for spends and confirmations.
chainNotifier lndclient.ChainNotifierClient
// signerClient is the signer client used to sign the batch transaction.
signerClient lndclient.SignerClient
// muSig2SignSweep includes all the required functionality to collect
// and verify signatures by the swap server in order to cooperatively
// sweep funds.
muSig2SignSweep MuSig2SignSweep
// verifySchnorrSig is a function that verifies a schnorr signature.
verifySchnorrSig VerifySchnorrSig
// publishErrorHandler is a function that handles transaction publishing
// error. By default, it logs all errors as warnings, but "insufficient
// fee" as Info.
publishErrorHandler PublishErrorHandler
// purger is a function that can take a sweep which is being purged and
// hand it over to the batcher for further processing.
purger Purger
// store includes all the database interactions that are needed by the
// batch.
store BatcherStore
// cfg is the configuration for this batch.
cfg *batchConfig
// log_ is the logger for this batch.
log_ atomic.Pointer[btclog.Logger]
wg sync.WaitGroup
}
// Purger is a function that takes a sweep request and feeds it back to the
// batcher main entry point. The name is inspired by its purpose, which is to
// purge the batch from sweeps that didn't make it to the confirmed tx.
type Purger func(ctx context.Context, sweepReq *SweepRequest) error
// batchKit is a kit of dependencies that are used to initialize a batch. This
// struct is only used as a wrapper for the arguments that are required to
// create a new batch.
type batchKit struct {
id int32
batchTxid *chainhash.Hash
batchPkScript []byte
state batchState
primaryID wire.OutPoint
sweeps map[wire.OutPoint]sweep
rbfCache rbfCache
wallet lndclient.WalletKitClient
chainNotifier lndclient.ChainNotifierClient
signerClient lndclient.SignerClient
musig2SignSweep MuSig2SignSweep
verifySchnorrSig VerifySchnorrSig
publishErrorHandler PublishErrorHandler
purger Purger
store BatcherStore
log btclog.Logger
quit chan struct{}
}
// scheduleNextCall schedules the next call to the batch handler's main event
// loop. It returns a function that must be called when the call is finished.
func (b *batch) scheduleNextCall() (func(), error) {
select {
case b.callEnter <- struct{}{}:
case <-b.quit:
return func() {}, ErrBatcherShuttingDown
case <-b.stopping:
return func() {}, ErrBatchShuttingDown
case <-b.finished:
return func() {}, ErrBatchShuttingDown
}
return func() {
b.callLeave <- struct{}{}
}, nil
}
// NewBatch creates a new batch.
func NewBatch(cfg batchConfig, bk batchKit) *batch {
return &batch{
// We set the ID to a negative value to flag that this batch has
// never been persisted, so it needs to be assigned a new ID.
id: -1,
state: Open,
sweeps: make(map[wire.OutPoint]sweep),
confChan: make(chan *chainntnfs.TxConfirmation, 1),
testReqs: make(chan *testRequest),
errChan: make(chan error, 1),
callEnter: make(chan struct{}),
callLeave: make(chan struct{}),
stopping: make(chan struct{}),
finished: make(chan struct{}),
quit: bk.quit,
batchTxid: bk.batchTxid,
wallet: bk.wallet,
chainNotifier: bk.chainNotifier,
signerClient: bk.signerClient,
muSig2SignSweep: bk.musig2SignSweep,
verifySchnorrSig: bk.verifySchnorrSig,
publishErrorHandler: bk.publishErrorHandler,
purger: bk.purger,
store: bk.store,
cfg: &cfg,
}
}
// NewBatchFromDB creates a new batch that already existed in storage.
func NewBatchFromDB(cfg batchConfig, bk batchKit) (*batch, error) {
// Make sure the batch is not empty.
if len(bk.sweeps) == 0 {
// This should never happen, as this precondition is already
// ensured in spinUpBatchFromDB.
return nil, fmt.Errorf("empty batch is not allowed")
}
// Assign batchConfTarget to primary sweep's confTarget.
for _, sweep := range bk.sweeps {
if sweep.outpoint == bk.primaryID {
cfg.batchConfTarget = sweep.confTarget
break
}
}
b := &batch{
id: bk.id,
state: bk.state,
primarySweepID: bk.primaryID,
sweeps: bk.sweeps,
confChan: make(chan *chainntnfs.TxConfirmation, 1),
testReqs: make(chan *testRequest),
errChan: make(chan error, 1),
callEnter: make(chan struct{}),
callLeave: make(chan struct{}),
stopping: make(chan struct{}),
finished: make(chan struct{}),
quit: bk.quit,
batchTxid: bk.batchTxid,
batchPkScript: bk.batchPkScript,
rbfCache: bk.rbfCache,
wallet: bk.wallet,
chainNotifier: bk.chainNotifier,
signerClient: bk.signerClient,
muSig2SignSweep: bk.musig2SignSweep,
verifySchnorrSig: bk.verifySchnorrSig,
publishErrorHandler: bk.publishErrorHandler,
purger: bk.purger,
store: bk.store,
cfg: &cfg,
}
b.setLog(bk.log)
return b, nil
}
// log returns current logger.
func (b *batch) log() btclog.Logger {
return *b.log_.Load()
}
// setLog atomically replaces the logger.
func (b *batch) setLog(logger btclog.Logger) {
b.log_.Store(&logger)
}
// Debugf logs a message with level DEBUG.
func (b *batch) Debugf(format string, params ...interface{}) {
b.log().Debugf(format, params...)
}
// Infof logs a message with level INFO.
func (b *batch) Infof(format string, params ...interface{}) {
b.log().Infof(format, params...)
}
// Warnf logs a message with level WARN.
func (b *batch) Warnf(format string, params ...interface{}) {
b.log().Warnf(format, params...)
}
// Errorf logs a message with level ERROR.
func (b *batch) Errorf(format string, params ...interface{}) {
b.log().Errorf(format, params...)
}
// checkSweepToAdd checks if a sweep can be added or updated in the batch. The
// caller must lock the event loop using scheduleNextCall. The function returns
// if the sweep already exists in the batch. If presigned mode is enabled, the
// result depends on the outcome of the method presignedHelper.SignTx for a
// non-empty batch. For an empty batch, the input needs to pass
// PresignSweepsGroup.
func (b *batch) checkSweepToAdd(_ context.Context, sweep *sweep) (bool, error) {
// If the provided sweep is nil, we can't proceed with any checks, so
// we just return early.
if sweep == nil {
return false, fmt.Errorf("the sweep is nil")
}
// Before we run through the acceptance checks, let's just see if this
// sweep is already in our batch. In that case, just update the sweep.
if _, ok := b.sweeps[sweep.outpoint]; ok {
return true, nil
}
// Enforce MaxSweepsPerBatch. If there are already too many sweeps in
// the batch, do not add another sweep to prevent the tx from becoming
// non-standard.
if len(b.sweeps) >= MaxSweepsPerBatch {
return false, fmt.Errorf("the batch has already too many "+
"sweeps %d >= %d", len(b.sweeps), MaxSweepsPerBatch)
}
// Since all the actions of the batch happen sequentially, we could
// arrive here after the batch got closed because of a spend. In this
// case we cannot add the sweep to this batch.
if b.state != Open {
return false, fmt.Errorf("the batch state (%v) is not open",
b.state)
}
// If this batch contains a single sweep that spends to a non-wallet
// address, or the incoming sweep is spending to non-wallet address,
// we cannot add this sweep to the batch.
for _, s := range b.sweeps {
if s.isExternalAddr {
return false, fmt.Errorf("the batch already has a "+
"sweep %x with an external address",
s.swapHash[:6])
}
if sweep.isExternalAddr {
return false, fmt.Errorf("the batch is not empty and "+
"new sweep %x has an external address",
sweep.swapHash[:6])
}
}
// Check the timeout of the incoming sweep against the timeout of all
// already contained sweeps. If that difference exceeds the configured
// maximum we cannot add this sweep.
for _, s := range b.sweeps {
timeoutDistance :=
int32(math.Abs(float64(sweep.timeout - s.timeout)))
if timeoutDistance > b.cfg.maxTimeoutDistance {
return false, fmt.Errorf("too long timeout distance "+
"between the batch and sweep %x: %d > %d",
sweep.swapHash[:6], timeoutDistance,
b.cfg.maxTimeoutDistance)
}
}
// Everything is ok, the sweep can be added to the batch.
return false, nil
}
// addSweeps tries to add sweeps to the batch. If this is the first sweep being
// added to the batch then it also sets the primary sweep ID. It returns if the
// sweeps were accepted to the batch.
func (b *batch) addSweeps(ctx context.Context, sweeps []*sweep) (bool, error) {
done, err := b.scheduleNextCall()
defer done()
if err != nil {
return false, err
}
// This must be a bug, so log a warning.
if len(sweeps) == 0 {
b.Warnf("An attempt to add zero sweeps.")
return false, nil
}
// Track how many new and existing sweeps are among the sweeps.
var numExisting, numNew int
for _, s := range sweeps {
existing, err := b.checkSweepToAdd(ctx, s)
if err != nil {
b.Infof("Failed to add sweep %v to batch %d: %v",
s.outpoint, b.id, err)
return false, nil
}
if existing {
numExisting++
} else {
numNew++
}
}
// Make sure the whole group is either new or existing. If this is not
// the case, this might be a bug, so print a warning.
if numExisting > 0 && numNew > 0 {
b.Warnf("There are %d existing and %d new sweeps among the "+
"group. They must not be mixed.", numExisting, numNew)
return false, nil
}
// Make sure all the sweeps spend different outpoints.
outpointsSet := make(map[wire.OutPoint]struct{}, len(sweeps))
for _, s := range sweeps {
if _, has := outpointsSet[s.outpoint]; has {
b.Warnf("Multiple sweeps spend outpoint %v", s.outpoint)
return false, nil
}
outpointsSet[s.outpoint] = struct{}{}
}
// Track if there is a presigned and a regular sweep.
var addingPresigned, addingRegular bool
for _, s := range sweeps {
if s.presigned {
addingPresigned = true
} else {
addingRegular = true
}
}
if addingPresigned && addingRegular {
b.Warnf("There are presigned and regular sweeps in the group")
return false, nil
}
// If presigned mode is enabled, we should first presign the new version
// of batch transaction. Also ensure that all the sweeps in the batch
// use the same mode (presigned or regular).
if addingPresigned {
// Ensure that all the sweeps in the batch use presigned mode.
for _, s := range b.sweeps {
if !s.presigned {
b.Warnf("Failed to add presigned sweep %x to "+
"the batch, because the batch has "+
"non-presigned sweep %x",
sweeps[0].swapHash[:6], s.swapHash[:6])
return false, nil
}
}
switch {
// We don't need to run checks if existing sweeps are updated.
case numExisting == len(sweeps):
// If new sweeps are added to the batch, we need to presign new
// version of batch transaction.
case len(b.sweeps) != 0:
if err := b.presign(ctx, sweeps); err != nil {
b.Warnf("Failed to add sweep %x to the batch, "+
"because failed to presign new version"+
" of batch tx: %v",
sweeps[0].swapHash[:6], err)
return false, nil
}
// If this is a new batch being formed, make sure we already
// have a presigned transaction.
default:
const allowNonEmptyBatch = false
err := b.ensurePresigned(
ctx, sweeps, allowNonEmptyBatch,
)
if err != nil {
b.Warnf("Failed to check signing of input %x,"+
" this means that PresignSweepsGroup "+
"was not called prior to AddSweep for"+
" this input: %v",
sweeps[0].swapHash[:6], err)
return false, nil
}
}
} else {
// Ensure that all the sweeps in the batch don't use presigned.
for _, s := range b.sweeps {
if s.presigned {
b.Warnf("failed to add a non-presigned sweep "+
"%x to the batch, because the batch "+
"has presigned sweep %x",
sweeps[0].swapHash[:6], s.swapHash[:6])
return false, nil
}
}
}
// Past this point we know that a new incoming sweep passes the
// acceptance criteria and is now ready to be added to this batch.
// For an existing group, update the sweeps in the batch.
if numExisting == len(sweeps) {
for _, s := range sweeps {
oldSweep, ok := b.sweeps[s.outpoint]
if !ok {
return false, fmt.Errorf("sweep %v not found "+
"in batch %d", s.outpoint, b.id)
}
// Preserve coopFailed value not to forget about
// cooperative spending failure in this sweep.
tmp := *s
tmp.coopFailed = oldSweep.coopFailed
// If the sweep was resumed from storage, and the swap
// requested to sweep again, a new sweep notifier will
// be created by the swap. By re-assigning to the
// batch's sweep we make sure that everything, including
// the notifier, is up to date.
b.sweeps[s.outpoint] = tmp
// If this is the primary sweep, we also need to update
// the batch's confirmation target and fee rate.
if b.primarySweepID == s.outpoint {
b.cfg.batchConfTarget = s.confTarget
b.rbfCache.SkipNextBump = true
}
// Update batch's fee rate to be greater than or equal
// to minFeeRate of the sweep. Make sure batch's fee
// rate does not decrease (otherwise it won't pass RBF
// rules and won't be broadcasted) and that it is not
// lower that minFeeRate of other sweeps (so it is
// applied).
if b.rbfCache.FeeRate < s.minFeeRate {
b.Infof("Increasing feerate of the batch "+
"from %v to %v", b.rbfCache.FeeRate,
s.minFeeRate)
b.rbfCache.FeeRate = s.minFeeRate
}
}
return true, nil
} else if numNew != len(sweeps) {
// Sanity check: all the sweeps must be either existing or new.
// We have checked this above, let's check here as well.
return false, fmt.Errorf("bug in numExisting and numNew logic:"+
" numExisting=%d, numNew=%d, len(sweeps)=%d, "+
"len(b.sweeps)=%d", numExisting, numNew, len(sweeps),
len(b.sweeps))
}
// Here is the code to add new sweeps to a batch.
for _, s := range sweeps {
// If this is the first sweep being added to the batch, make it
// the primary sweep.
if b.primarySweepID == zeroSweepID {
b.primarySweepID = s.outpoint
b.cfg.batchConfTarget = s.confTarget
b.rbfCache.FeeRate = s.minFeeRate
b.rbfCache.SkipNextBump = true
// We also need to start the spend monitor for this new
// primary sweep.
err := b.monitorSpend(ctx, *s)
if err != nil {
return false, err
}
}
// Make sure the sweep is not present in the batch. If it is
// present, this is a bug, return an error to stop here.
if _, has := b.sweeps[s.outpoint]; has {
return false, fmt.Errorf("sweep %v is already present "+
"in batch %d", s.outpoint, b.id)
}
// Add the sweep to the batch's sweeps.
b.Infof("adding sweep %v, swap %x", s.outpoint, s.swapHash[:6])
b.sweeps[s.outpoint] = *s
// Update FeeRate. Max(s.minFeeRate) for all the sweeps of
// the batch is the basis for fee bumps.
if b.rbfCache.FeeRate < s.minFeeRate {
b.Infof("Increasing feerate of the batch "+
"from %v to %v", b.rbfCache.FeeRate,
s.minFeeRate)
b.rbfCache.FeeRate = s.minFeeRate
b.rbfCache.SkipNextBump = true
}
if err := b.persistSweep(ctx, *s, false); err != nil {
return true, err
}
}
return true, nil
}
// sweepExists returns true if the batch contains the sweep with the given
// outpoint.
func (b *batch) sweepExists(outpoint wire.OutPoint) bool {
done, err := b.scheduleNextCall()
defer done()
if err != nil {
return false
}
_, ok := b.sweeps[outpoint]
return ok
}
// Wait waits for the batch to gracefully stop.
func (b *batch) Wait() {
b.Infof("Stopping")
<-b.finished
}
// stillWaitingMsg is the format of the message printed if the batch is about
// to publish, but initial delay has not ended yet.
const stillWaitingMsg = "Skipping publishing, initial delay will end at " +
"%v, now is %v."
// Run is the batch's main event loop.
func (b *batch) Run(ctx context.Context) error {
runCtx, cancel := context.WithCancel(ctx)
defer func() {
cancel()
close(b.stopping)
// Make sure not to call b.wg.Wait from any other place to avoid
// race condition between b.wg.Add(1) and b.wg.Wait().
b.wg.Wait()
close(b.finished)
}()
if b.muSig2SignSweep == nil && b.cfg.customMuSig2Signer == nil {
return fmt.Errorf("no musig2 signer available")
}
if b.muSig2SignSweep != nil && b.cfg.customMuSig2Signer != nil {
return fmt.Errorf("both musig2 signers provided")
}
// Cache clock variable.
clock := b.cfg.clock
startTime := clock.Now()
blockChan, blockErrChan, err :=
b.chainNotifier.RegisterBlockEpochNtfn(runCtx)
if err != nil {
return fmt.Errorf("block registration error: %w", err)
}
// Set currentHeight here, because it may be needed in monitorSpend.
select {
case b.currentHeight = <-blockChan:
b.Debugf("initial height for the batch is %v", b.currentHeight)
case <-runCtx.Done():
return fmt.Errorf("context expired while waiting for current "+
"height: %w", runCtx.Err())
}
// If a primary sweep exists we immediately start monitoring for its
// spend.
if b.primarySweepID != zeroSweepID {
sweep := b.sweeps[b.primarySweepID]
err := b.monitorSpend(runCtx, sweep)
if err != nil {
return fmt.Errorf("monitorSpend error: %w", err)
}
}
// skipBefore is the time before which we skip batch publishing.
// This is needed to facilitate better grouping of sweeps.
// The value is set only if the batch has at least one sweep.
// For batches loaded from DB initialDelay should be 0.
var skipBefore *time.Time
// initialDelayChan is a timer which fires upon initial delay end.
// If initialDelay is set to 0, it will not trigger to avoid setting up
// timerChan twice, which could lead to double publishing if
// batchPublishDelay is also 0.
var initialDelayChan <-chan time.Time
// We use a timer in order to not publish new transactions at the same
// time as the block epoch notification. This is done to prevent
// unnecessary transaction publishments when a spend is detected on that
// block. This timer starts after new block arrives (including the
// current tip which we read from blockChan above) or when initialDelay
// completes.
timerChan := clock.TickAfter(b.cfg.batchPublishDelay)
b.Infof("started, primary %s, total sweeps %d, state: %d",
b.primarySweepID, len(b.sweeps), b.state)
for {
// If the batch is not empty, find earliest initialDelay.
var totalSweptAmt btcutil.Amount
for _, sweep := range b.sweeps {
totalSweptAmt += sweep.value
}
skipBeforeUpdated := false
if totalSweptAmt != 0 {
initialDelay, err := b.cfg.initialDelayProvider(
ctx, len(b.sweeps), totalSweptAmt,
)
if err != nil {
b.Warnf("InitialDelayProvider failed: %v. We "+
"publish this batch without a delay.",
err)
initialDelay = 0
}
if initialDelay < 0 {
b.Warnf("Negative delay: %v. We publish this "+
"batch without a delay.", initialDelay)
initialDelay = 0
}
delayStop := startTime.Add(initialDelay)
if skipBefore == nil || delayStop.Before(*skipBefore) {
skipBefore = &delayStop
skipBeforeUpdated = true
}
}
// Create new timer only if the value of skipBefore was updated.
// Don't create the timer if the delay is <= 0 to avoid double
// publishing if batchPublishDelay is also 0.
if skipBeforeUpdated {
delay := skipBefore.Sub(clock.Now())
if delay > 0 {
initialDelayChan = clock.TickAfter(delay)
}
}
select {
case <-b.callEnter:
<-b.callLeave
// blockChan provides immediately the current tip.
case height := <-blockChan:
b.Debugf("received block %v", height)
// Set the timer to publish the batch transaction after
// the configured delay.
timerChan = clock.TickAfter(b.cfg.batchPublishDelay)
b.currentHeight = height
case <-initialDelayChan:
b.Debugf("initial delay of duration %v has ended",
clock.Now().Sub(startTime))
// Set the timer to publish the batch transaction after
// the configured delay.
timerChan = clock.TickAfter(b.cfg.batchPublishDelay)
case <-timerChan:
// Check that batch is still open.
if b.state != Open {
b.Debugf("Skipping publishing, because "+
"the batch is not open (%v).", b.state)
continue
}
if skipBefore == nil {
b.Debugf("Skipping publishing, because " +
"the batch is empty.")
continue
}
// If the batch became urgent, skipBefore is set to now.
if b.isUrgent(*skipBefore) {
*skipBefore = clock.Now()
}
// Check that the initial delay has ended. We have also
// batchPublishDelay on top of initialDelay, so if
// initialDelayChan has just fired, this check passes.
now := clock.Now()
if skipBefore.After(now) {
b.Debugf(stillWaitingMsg, *skipBefore, now)
continue
}
// Update feerate of sweeps. This is normally done by
// AddSweep, but it may not be called after the sweep
// is confirmed, but fresh feerate is still needed to
// keep publishing in case of reorg.
b.updateFeeRate(ctx)
err := b.publish(ctx)
if err != nil {
return fmt.Errorf("publish error: %w", err)
}
case spend := <-b.spendChan:
err := b.handleSpend(runCtx, spend.SpendingTx)
if err != nil {
return fmt.Errorf("handleSpend error: %w", err)
}
case err := <-b.spendErrChan:
b.writeToSpendErrChan(ctx, err)
return fmt.Errorf("spend notifier failed: %w", err)
case conf := <-b.confChan:
if err := b.handleConf(runCtx, conf); err != nil {
return fmt.Errorf("handleConf error: %w", err)
}
return nil
// A re-org has been detected. We set the batch state back to
// open since our batch transaction is no longer present in any
// block. We can accept more sweeps and try to publish.
case <-b.reorgChan:
b.state = Open
b.Warnf("reorg detected, batch is able to " +
"accept new sweeps")
case testReq := <-b.testReqs:
testReq.handler()
close(testReq.quit)
case err := <-blockErrChan:
return fmt.Errorf("blocks monitoring error: %w", err)
case err := <-b.errChan:
return fmt.Errorf("error with the batch: %w", err)
case <-runCtx.Done():
return fmt.Errorf("batch context expired: %w",
runCtx.Err())
}
}
}
// updateFeeRate gets fresh values of minFeeRate for sweeps and updates the
// feerate of the batch if needed. This method must be called from event loop.
func (b *batch) updateFeeRate(ctx context.Context) {
for outpoint, s := range b.sweeps {
minFeeRate, err := minimumSweepFeeRate(
ctx, b.cfg.customFeeRate, b.wallet,
s.swapHash, s.outpoint, s.confTarget,
)
if err != nil {
b.Warnf("failed to determine feerate for sweep %v of "+
"swap %x, confTarget %d: %w", s.outpoint,
s.swapHash[:6], s.confTarget, err)
continue
}
if minFeeRate <= s.minFeeRate {
continue
}
b.Infof("Increasing feerate of sweep %v of swap %x from %v "+
"to %v", s.outpoint, s.swapHash[:6], s.minFeeRate,
minFeeRate)
s.minFeeRate = minFeeRate
b.sweeps[outpoint] = s
if s.minFeeRate <= b.rbfCache.FeeRate {
continue
}
b.Infof("Increasing feerate of the batch from %v to %v",
b.rbfCache.FeeRate, s.minFeeRate)
b.rbfCache.FeeRate = s.minFeeRate
}
}
// testRunInEventLoop runs a function in the event loop blocking until
// the function returns. For unit tests only!
func (b *batch) testRunInEventLoop(ctx context.Context, handler func()) {
// If the event loop is finished, run the function.
select {
case <-b.stopping:
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():
}
}
// timeout returns minimum timeout as block height among sweeps of the batch.
// If the batch is empty, return -1.
func (b *batch) timeout() int32 {
// Find minimum among sweeps' timeouts.
minTimeout := int32(-1)
for _, sweep := range b.sweeps {
if minTimeout == -1 || minTimeout > sweep.timeout {
minTimeout = sweep.timeout
}
}
return minTimeout
}
// isUrgent checks if the batch became urgent. This is determined by comparing
// the remaining number of blocks until timeout to the initial delay remained,
// given one block is 10 minutes.
func (b *batch) isUrgent(skipBefore time.Time) bool {
timeout := b.timeout()
if timeout <= 0 {
// This may happen if the batch is empty or if SweepInfo.Timeout
// is not set, may be possible in tests or if there is a bug.
b.Warnf("Method timeout() returned %v. Number of "+
"sweeps: %d. It may be an empty batch.",
timeout, len(b.sweeps))
return false
}
if b.currentHeight == 0 {
// currentHeight is not initiated yet.
return false
}
blocksToTimeout := timeout - b.currentHeight
const blockTime = 10 * time.Minute
timeBank := time.Duration(blocksToTimeout) * blockTime
// We want to have at least 2x as much time to be safe.
const safetyFactor = 2
remainingWaiting := skipBefore.Sub(b.cfg.clock.Now())
if timeBank >= safetyFactor*remainingWaiting {
// There is enough time, keep waiting.
return false
}
b.Debugf("cancelling waiting for urgent sweep (timeBank is %v, "+
"remainingWaiting is %v)", timeBank, remainingWaiting)
// Signal to the caller to cancel initialDelay.
return true
}
// isPresigned returns if the batch uses presigned mode. Currently presigned and
// non-presigned sweeps never appear in the same batch. Fails if the batch is
// empty or contains both presigned and regular sweeps.
func (b *batch) isPresigned() (bool, error) {
var (
hasPresigned bool
hasRegular bool
)
for _, sweep := range b.sweeps {
if sweep.presigned {
hasPresigned = true
} else {
hasRegular = true
}
}
switch {
case hasPresigned && !hasRegular:
return true, nil
case !hasPresigned && hasRegular:
return false, nil
case hasPresigned && hasRegular:
return false, fmt.Errorf("the batch has both presigned and " +
"non-presigned sweeps")
default:
return false, fmt.Errorf("the batch is empty")
}
}
// publish creates and publishes the latest batch transaction to the network.
func (b *batch) publish(ctx context.Context) error {
var (
err error
fee btcutil.Amount
signSuccess bool
)
if len(b.sweeps) == 0 {
b.Debugf("skipping publish: no sweeps in the batch")
return nil
}
// Run the RBF rate update.
err = b.updateRbfRate(ctx)
if err != nil {
return err
}
// logPublishError is a function which logs publish errors.
logPublishError := func(errMsg string, err error) {
b.publishErrorHandler(err, errMsg, b.log())
}
// Determine if we should use presigned mode for the batch.
presigned, err := b.isPresigned()
if err != nil {
return fmt.Errorf("failed to determine if the batch %d uses "+
"presigned mode: %w", b.id, err)
}
if presigned {
fee, err, signSuccess = b.publishPresigned(ctx)
} else {
fee, err, signSuccess = b.publishMixedBatch(ctx)
}
if err != nil {
if signSuccess {
logPublishError("publish error", err)
// Publishing error is expected: "insufficient fee" and
// "output already spent". Don't return the error here
// not to break the main loop of the sweep batch.
return nil
} else {
logPublishError("signing error", err)
// Signing error is not expected, because we have
// non-cooperative method of signing which should
// always succeed.
return err
}
}
b.Infof("published, total sweeps: %v, fees: %v", len(b.sweeps), fee)
for _, sweep := range b.sweeps {
b.Infof("published sweep %x, value: %v",
sweep.swapHash[:6], sweep.value)
}
return b.persist(ctx)
}
// createPsbt creates serialized PSBT and prevOuts map from unsignedTx and
// the list of sweeps.
func (b *batch) createPsbt(unsignedTx *wire.MsgTx, sweeps []sweep) ([]byte,
map[wire.OutPoint]*wire.TxOut, error) {
// Create PSBT packet object.
packet, err := psbt.NewFromUnsignedTx(unsignedTx)
if err != nil {
return nil, nil, fmt.Errorf("failed to create PSBT: %w", err)
}
// Sanity check: the number of inputs in PSBT must be equal to the
// number of sweeps.
if len(packet.Inputs) != len(sweeps) {
return nil, nil, fmt.Errorf("invalid number of packet inputs")
}
// Create prevOuts map.
prevOuts := make(map[wire.OutPoint]*wire.TxOut, len(sweeps))
// Fill input info in PSBT and prevOuts.
for i, sweep := range sweeps {
txOut := &wire.TxOut{
Value: int64(sweep.value),
PkScript: sweep.htlc.PkScript,
}
prevOuts[sweep.outpoint] = txOut
packet.Inputs[i].WitnessUtxo = txOut
}
// Serialize PSBT.
var psbtBuf bytes.Buffer
err = packet.Serialize(&psbtBuf)
if err != nil {
return nil, nil, fmt.Errorf("failed to serialize PSBT: %w", err)
}
return psbtBuf.Bytes(), prevOuts, nil
}
// constructUnsignedTx creates unsigned tx from the sweeps, paying to the addr.
// It also returns absolute fee (from weight and clamped). The main output is
// the first output of the transaction, followed by an optional list of change
// outputs. If the main output value is below dust limit this function will
// return an error.
func constructUnsignedTx(sweeps []sweep, address btcutil.Address,
currentHeight int32, feeRate chainfee.SatPerKWeight,
minRelayFeeRate chainfee.SatPerKWeight) (*wire.MsgTx,
lntypes.WeightUnit, btcutil.Amount, btcutil.Amount, error) {
// Sanity check, there should be at least 1 sweep in this batch.
if len(sweeps) == 0 {
return nil, 0, 0, 0, fmt.Errorf("no sweeps in batch")
}
// Create the batch transaction.
batchTx := &wire.MsgTx{
Version: 2,
LockTime: uint32(currentHeight),
}
var changeOutputs []*wire.TxOut
for _, sweep := range sweeps {
if sweep.change != nil {
changeOutputs = append(changeOutputs, sweep.change)
}
}
// Add transaction inputs and estimate its weight.
var weightEstimate input.TxWeightEstimator
for _, sweep := range sweeps {
if sweep.nonCoopHint || sweep.coopFailed {
// Non-cooperative sweep.
batchTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: sweep.outpoint,
Sequence: sweep.htlc.SuccessSequence(),
})
err := sweep.htlcSuccessEstimator(&weightEstimate)
if err != nil {
return nil, 0, 0, 0, fmt.Errorf("sweep."+
"htlcSuccessEstimator failed: %w", err)
}
} else {
// Cooperative sweep.
batchTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: sweep.outpoint,
})
weightEstimate.AddTaprootKeySpendInput(
txscript.SigHashDefault,
)
}
}
// Convert the destination address to pkScript.
batchPkScript, err := txscript.PayToAddrScript(address)
if err != nil {
return nil, 0, 0, 0, fmt.Errorf("txscript.PayToAddrScript "+
"failed: %w", err)
}
if len(batchPkScript) == 0 {
return nil, 0, 0, 0, fmt.Errorf("txscript.PayToAddrScript " +
"returned an empty pkScript")
}
// Add the output to weight estimates.
err = sweeppkg.AddOutputEstimate(&weightEstimate, address)
if err != nil {
return nil, 0, 0, 0, fmt.Errorf("sweep.AddOutputEstimate "+
"failed: %w", err)
}
// Add the optional change outputs to weight estimates.
for _, o := range changeOutputs {
weightEstimate.AddOutput(o.PkScript)
}
// Keep track of the total amount this batch is sweeping back.
batchAmt := btcutil.Amount(0)
for _, sweep := range sweeps {
batchAmt += sweep.value
}
// Find weight and fee.
weight := weightEstimate.Weight()
feeForWeight := feeRate.FeeForWeight(weight)
// Fee can be rounded towards zero, leading to actual feeRate being
// slightly lower than the requested value. Increase the fee if this is
// the case.
if chainfee.NewSatPerKWeight(feeForWeight, weight) < feeRate {
feeForWeight++
}
// Add the batch transaction output, which excludes the fees paid to
// miners. Reduce the amount by the sum of change outputs, if any.
var sumChange int64
for _, change := range changeOutputs {
sumChange += change.Value
}
// Ensure that the batch amount is greater than the sum of change.
if batchAmt <= btcutil.Amount(sumChange) {
return nil, 0, 0, 0, fmt.Errorf("batch amount %v is <= the "+
"sum of change outputs %v", batchAmt,
btcutil.Amount(sumChange))
}
// Clamp the calculated fee to the max allowed fee amount for the batch.
fee, err := clampBatchFee(
feeForWeight, batchAmt-btcutil.Amount(sumChange),
minRelayFeeRate, weight,
)
if err != nil {
return nil, 0, 0, 0, fmt.Errorf("failed to clamp batch "+
"fee: %w", err)
}
// Ensure that batch amount is equal or exceeds the sum of change
// outputs and the fee, and that it is also greater than dust limit
// for the main output.
dustLimit := utils.DustLimitForPkScript(batchPkScript)
if fee+btcutil.Amount(sumChange)+dustLimit > batchAmt {
return nil, 0, 0, 0, fmt.Errorf("batch amount %v is < the "+
"sum of change outputs %v plus fee %v and dust "+
"limit %v", batchAmt, btcutil.Amount(sumChange),
fee, dustLimit)
}
// Add the main output first.
batchTx.AddTxOut(&wire.TxOut{
PkScript: batchPkScript,
Value: int64(batchAmt-fee) - sumChange,
})
// Then add change outputs.
for _, txOut := range changeOutputs {
batchTx.AddTxOut(&wire.TxOut{
PkScript: txOut.PkScript,
Value: txOut.Value,
})
}
// Check that for each swap, inputs exceed the change outputs.
if len(changeOutputs) != 0 {
swap2Inputs := make(map[lntypes.Hash]btcutil.Amount)
swap2Change := make(map[lntypes.Hash]btcutil.Amount)
for _, sweep := range sweeps {
swap2Inputs[sweep.swapHash] += sweep.value
if sweep.change != nil {
swap2Change[sweep.swapHash] +=
btcutil.Amount(sweep.change.Value)
}
}
for swapHash, inputs := range swap2Inputs {
change := swap2Change[swapHash]
if inputs <= change {
return nil, 0, 0, 0, fmt.Errorf(""+
"inputs %v <= change %v for swap %x",
inputs, change, swapHash[:6])
}
}
}
// Ensure that each output is above dust limit.
for _, txOut := range batchTx.TxOut {
dustLimit = utils.DustLimitForPkScript(txOut.PkScript)
if btcutil.Amount(txOut.Value) < dustLimit {
return nil, 0, 0, 0, fmt.Errorf("output %v is below "+
"dust limit %v", btcutil.Amount(txOut.Value),
dustLimit)
}
}
return batchTx, weight, feeForWeight, fee, nil
}
// publishMixedBatch constructs and publishes a batch transaction that can
// include sweeps spent both cooperatively and non-cooperatively. If a sweep is
// marked with nonCoopHint or coopFailed flags, it is spent non-cooperatively.
// If a cooperative sweep fails to sign cooperatively, the whole transaction
// is re-signed again, with this sweep signing non-cooperatively. This process
// is optimized, trying to detect all non-cooperative sweeps in one round. The
// function returns the absolute fee. The last result of the function indicates
// if signing succeeded.
func (b *batch) publishMixedBatch(ctx context.Context) (btcutil.Amount, error,
bool) {
// Sanity check, there should be at least 1 sweep in this batch.
if len(b.sweeps) == 0 {
return 0, fmt.Errorf("no sweeps in batch"), false
}
// Append this sweep to an array of sweeps. This is needed to keep the
// order of sweeps stored, as iterating the sweeps map does not
// guarantee same order.
sweeps := make([]sweep, 0, len(b.sweeps))
for _, sweep := range b.sweeps {
sweeps = append(sweeps, sweep)
}
// Determine if an external address is used.
addrOverride := false
for _, sweep := range sweeps {
if sweep.isExternalAddr {
addrOverride = true
}
}
// Find destination address.
var address btcutil.Address
if addrOverride {
// Sanity check, there should be exactly 1 sweep in this batch.
if len(sweeps) != 1 {
return 0, fmt.Errorf("external address sweep batched " +
"with other sweeps"), false
}
address = sweeps[0].destAddr
} else {
var err error
address, err = b.getBatchDestAddr(ctx)
if err != nil {
return 0, err, false
}
}
// Each iteration of this loop is one attempt to sign the transaction
// cooperatively. We try cooperative signing only for the sweeps not
// known in advance to be non-cooperative (nonCoopHint) and not failed
// to sign cooperatively in previous rounds (coopFailed). If any of them
// fails, the sweep is excluded from all following rounds and another
// round is attempted. Otherwise, the cycle completes and we sign the
// remaining sweeps non-cooperatively.
var (
tx *wire.MsgTx
weight lntypes.WeightUnit
feeForWeight btcutil.Amount
fee btcutil.Amount
minRelayFeeRate chainfee.SatPerKWeight
coopInputs int
)
minRelayFeeRate, err := b.wallet.MinRelayFee(ctx)
if err != nil {
return 0, fmt.Errorf("failed to get min relay fee: %w", err),
false
}
for attempt := 1; ; attempt++ {
b.Infof("Attempt %d of collecting cooperative signatures.",
attempt)
// Construct unsigned batch transaction.
var err error
tx, weight, feeForWeight, fee, err = constructUnsignedTx(
sweeps, address, b.currentHeight, b.rbfCache.FeeRate,
minRelayFeeRate,
)
if err != nil {
return 0, fmt.Errorf("failed to construct tx: %w", err),
false
}
// Create PSBT and prevOutsMap.
psbtBytes, prevOutsMap, err := b.createPsbt(tx, sweeps)
if err != nil {
return 0, fmt.Errorf("createPsbt failed: %w", err),
false
}
// Keep track if any new sweep failed to sign cooperatively.
newCoopFailures := false
// Try to sign all cooperative sweeps first.
coopInputs = 0
for i, sweep := range sweeps {
// Skip non-cooperative sweeps.
if sweep.nonCoopHint || sweep.coopFailed {
continue
}
// Try to sign the sweep cooperatively.
finalSig, err := b.musig2sign(
ctx, i, sweep, tx, prevOutsMap, psbtBytes,
)
if err != nil {
b.Infof("cooperative signing failed for "+
"sweep %x: %v", sweep.swapHash[:6], err)
// Set coopFailed flag for this sweep in all the
// places we store the sweep.
sweep.coopFailed = true
sweeps[i] = sweep
b.sweeps[sweep.outpoint] = sweep
// Update newCoopFailures to know if we need
// another attempt of cooperative signing.
newCoopFailures = true
} else {
// Put the signature to witness of the input.
tx.TxIn[i].Witness = wire.TxWitness{finalSig}
coopInputs++
}
}
// If there was any failure of cooperative signing, we need to
// update weight estimates (since non-cooperative signing has
// larger witness) and hence update the whole transaction and
// all the signatures. Otherwise, we complete cooperative part.
if !newCoopFailures {
break
}
}
// Calculate the expected number of non-cooperative sweeps.
nonCoopInputs := len(sweeps) - coopInputs
// Now sign the remaining sweeps' inputs non-cooperatively.
// For that, first collect sign descriptors for the signatures.
// Also collect prevOuts for all inputs.
signDescs := make([]*lndclient.SignDescriptor, 0, nonCoopInputs)
prevOutsList := make([]*wire.TxOut, 0, len(sweeps))
for i, sweep := range sweeps {
// Create and store the previous outpoint for this sweep.
prevOut := &wire.TxOut{
Value: int64(sweep.value),
PkScript: sweep.htlc.PkScript,
}
prevOutsList = append(prevOutsList, prevOut)
// Skip cooperative sweeps.
if !sweep.nonCoopHint && !sweep.coopFailed {
continue
}
key, err := btcec.ParsePubKey(
sweep.htlcKeys.ReceiverScriptKey[:],
)
if err != nil {
return 0, fmt.Errorf("btcec.ParsePubKey failed: %w",
err), false
}
// Create and store the sign descriptor for this sweep.
signDesc := lndclient.SignDescriptor{
WitnessScript: sweep.htlc.SuccessScript(),
Output: prevOut,
HashType: sweep.htlc.SigHash(),
InputIndex: i,
KeyDesc: keychain.KeyDescriptor{
PubKey: key,
},
}
if sweep.htlc.Version == swap.HtlcV3 {
signDesc.SignMethod = input.TaprootScriptSpendSignMethod
}
signDescs = append(signDescs, &signDesc)
}
// Sanity checks.
if len(signDescs) != nonCoopInputs {
// This must not happen by construction.
return 0, fmt.Errorf("unexpected size of signDescs: %d != %d",
len(signDescs), nonCoopInputs), false
}
if len(prevOutsList) != len(sweeps) {
// This must not happen by construction.
return 0, fmt.Errorf("unexpected size of prevOutsList: "+
"%d != %d", len(prevOutsList), len(sweeps)), false
}
var rawSigs [][]byte
if nonCoopInputs > 0 {
// Produce the signatures for our inputs using sign descriptors.
var err error
rawSigs, err = b.signerClient.SignOutputRaw(
ctx, tx, signDescs, prevOutsList,
)
if err != nil {
return 0, fmt.Errorf("signerClient.SignOutputRaw "+
"failed: %w", err), false
}
}
// Sanity checks.
if len(rawSigs) != nonCoopInputs {
// This must not happen by construction.
return 0, fmt.Errorf("unexpected size of rawSigs: %d != %d",
len(rawSigs), nonCoopInputs), false
}
// Generate success witnesses for non-cooperative sweeps.
sigIndex := 0
for i, sweep := range sweeps {
// Skip cooperative sweeps.
if !sweep.nonCoopHint && !sweep.coopFailed {
continue
}
witness, err := sweep.htlc.GenSuccessWitness(
rawSigs[sigIndex], sweep.preimage,
)
if err != nil {
return 0, fmt.Errorf("sweep.htlc.GenSuccessWitness "+
"failed: %w", err), false
}
sigIndex++
// Add the success witness to our batch transaction's inputs.
tx.TxIn[i].Witness = witness
}
// Log transaction's details.
var coopHexs, nonCoopHexs []string
for _, sweep := range sweeps {
swapHex := fmt.Sprintf("%x", sweep.swapHash[:6])
if sweep.nonCoopHint || sweep.coopFailed {
nonCoopHexs = append(nonCoopHexs, swapHex)
} else {
coopHexs = append(coopHexs, swapHex)
}
}
txHash := tx.TxHash()
b.Infof("attempting to publish batch tx=%v with feerate=%v, "+
"weight=%v, feeForWeight=%v, fee=%v, sweeps=%d, "+
"%d cooperative: (%s) and %d non-cooperative (%s), destAddr=%s",
txHash, b.rbfCache.FeeRate, weight, feeForWeight, fee,
len(tx.TxIn), coopInputs, strings.Join(coopHexs, ", "),
nonCoopInputs, strings.Join(nonCoopHexs, ", "), address)
b.debugLogTx("serialized batch", tx)
// Make sure tx weight matches the expected value.
realWeight := lntypes.WeightUnit(
blockchain.GetTransactionWeight(btcutil.NewTx(tx)),
)
if realWeight != weight {
b.Warnf("actual weight of tx %v is %v, estimated as %d",
txHash, realWeight, weight)
}
// Publish the transaction.
err = b.wallet.PublishTransaction(
ctx, tx, b.cfg.txLabeler(b.id),
)
if err != nil {
return 0, fmt.Errorf("publishing tx failed: %w", err), true
}
// Store the batch transaction's txid and pkScript, for monitoring
// purposes.
b.batchTxid = &txHash
b.batchPkScript = tx.TxOut[0].PkScript
return fee, nil, true
}
func (b *batch) debugLogTx(msg string, tx *wire.MsgTx) {
// Serialize the transaction and convert to hex string.
buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize()))
if err := tx.Serialize(buf); err != nil {
b.Errorf("failed to serialize tx for debug log: %v", err)
return
}
b.Debugf("%s: %s", msg, hex.EncodeToString(buf.Bytes()))
}
// musig2sign signs one sweep using musig2.
func (b *batch) musig2sign(ctx context.Context, inputIndex int, sweep sweep,
unsignedTx *wire.MsgTx, prevOuts map[wire.OutPoint]*wire.TxOut,
psbt []byte) ([]byte, error) {
prevOutputFetcher := txscript.NewMultiPrevOutFetcher(prevOuts)
sigHashes := txscript.NewTxSigHashes(unsignedTx, prevOutputFetcher)
sigHash, err := txscript.CalcTaprootSignatureHash(
sigHashes, txscript.SigHashDefault, unsignedTx, inputIndex,
prevOutputFetcher,
)
if err != nil {
return nil, err
}
var (
signers [][]byte
muSig2Version input.MuSig2Version
)
// Depending on the MuSig2 version we either pass 32 byte
// Schnorr public keys or normal 33 byte public keys.
if sweep.protocolVersion >= loopdb.ProtocolVersionMuSig2 {
muSig2Version = input.MuSig2Version100RC2
signers = [][]byte{
sweep.htlcKeys.SenderInternalPubKey[:],
sweep.htlcKeys.ReceiverInternalPubKey[:],
}
} else {
muSig2Version = input.MuSig2Version040
signers = [][]byte{
sweep.htlcKeys.SenderInternalPubKey[1:],
sweep.htlcKeys.ReceiverInternalPubKey[1:],
}
}
htlcScript, ok := sweep.htlc.HtlcScript.(*swap.HtlcScriptV3)
if !ok {
return nil, fmt.Errorf("invalid htlc script version")
}
var digest [32]byte
copy(digest[:], sigHash)
// If a custom signer is installed, use it instead of b.signerClient
// and b.muSig2SignSweep.
if b.cfg.customMuSig2Signer != nil {
// Produce a signature.
finalSig, err := b.cfg.customMuSig2Signer(
ctx, muSig2Version, sweep.swapHash,
htlcScript.RootHash, digest,
)
if err != nil {
return nil, fmt.Errorf("customMuSig2Signer failed: %w",
err)
}
// To be sure that we're good, parse and validate that the
// combined signature is indeed valid for the sig hash and the
// internal pubkey.
err = b.verifySchnorrSig(
htlcScript.TaprootKey, sigHash, finalSig,
)
if err != nil {
return nil, fmt.Errorf("verifySchnorrSig failed: %w",
err)
}
return finalSig, nil
}
// Now we're creating a local MuSig2 session using the receiver key's
// key locator and the htlc's root hash.
keyLocator := &sweep.htlcKeys.ClientScriptKeyLocator
musig2SessionInfo, err := b.signerClient.MuSig2CreateSession(
ctx, muSig2Version, keyLocator, signers,
lndclient.MuSig2TaprootTweakOpt(htlcScript.RootHash[:], false),
)
if err != nil {
return nil, fmt.Errorf("signerClient.MuSig2CreateSession "+
"failed: %w", err)
}
// With the session active, we can now send the server our
// public nonce and the sig hash, so that it can create it's own
// MuSig2 session and return the server side nonce and partial
// signature.
serverNonce, serverSig, err := b.muSig2SignSweep(
ctx, sweep.protocolVersion, sweep.swapHash,
sweep.swapInvoicePaymentAddr,
musig2SessionInfo.PublicNonce[:], psbt, prevOuts,
)
if err != nil {
return nil, err
}
var serverPublicNonce [musig2.PubNonceSize]byte
copy(serverPublicNonce[:], serverNonce)
// Register the server's nonce before attempting to create our
// partial signature.
haveAllNonces, err := b.signerClient.MuSig2RegisterNonces(
ctx, musig2SessionInfo.SessionID,
[][musig2.PubNonceSize]byte{serverPublicNonce},
)
if err != nil {
return nil, err
}
// Sanity check that we have all the nonces.
if !haveAllNonces {
return nil, fmt.Errorf("invalid MuSig2 session: " +
"nonces missing")
}
// Since our MuSig2 session has all nonces, we can now create
// the local partial signature by signing the sig hash.
_, err = b.signerClient.MuSig2Sign(
ctx, musig2SessionInfo.SessionID, digest, false,
)
if err != nil {
return nil, err
}
// Now combine the partial signatures to use the final combined
// signature in the sweep transaction's witness.
haveAllSigs, finalSig, err := b.signerClient.MuSig2CombineSig(
ctx, musig2SessionInfo.SessionID, [][]byte{serverSig},
)
if err != nil {
return nil, err
}
if !haveAllSigs {
return nil, fmt.Errorf("failed to combine signatures")
}
// To be sure that we're good, parse and validate that the
// combined signature is indeed valid for the sig hash and the
// internal pubkey.
err = b.verifySchnorrSig(htlcScript.TaprootKey, sigHash, finalSig)
if err != nil {
return nil, err
}
return finalSig, nil
}
// updateRbfRate updates the fee rate we should use for the new batch
// transaction. This fee rate does not guarantee RBF success, but the continuous
// increase leads to an eventual successful RBF replacement.
func (b *batch) updateRbfRate(ctx context.Context) error {
// If the feeRate is unset then we never published before, so we
// retrieve the fee estimate from our wallet.
if b.rbfCache.FeeRate == 0 {
// We set minFeeRate in each sweep, so fee rate is expected to
// be initiated here.
b.Warnf("rbfCache.FeeRate is 0, which must not happen.")
if b.cfg.batchConfTarget == 0 {
b.Warnf("updateRbfRate called with zero " +
"batchConfTarget")
}
b.Infof("initializing rbf fee rate for conf target=%v",
b.cfg.batchConfTarget)
rate, err := b.wallet.EstimateFeeRate(
ctx, b.cfg.batchConfTarget,
)
if err != nil {
return err
}
// Set the initial value for our fee rate.
b.rbfCache.FeeRate = rate
} else if noBumping := b.cfg.customFeeRate != nil; !noBumping {
if b.rbfCache.SkipNextBump {
// Skip fee bumping, unset the flag, to bump next time.
b.rbfCache.SkipNextBump = false
} else {
// Bump the fee rate by the configured step.
b.rbfCache.FeeRate += defaultFeeRateStep
}
}
b.rbfCache.LastHeight = b.currentHeight
return b.persist(ctx)
}
// monitorSpend monitors the primary sweep's outpoint for spends. The reason we
// monitor the primary sweep's outpoint is because the primary sweep was the
// first sweep that entered this batch, therefore it is present in all the
// versions of the batch transaction. This means that even if an older version
// of the batch transaction gets confirmed, due to the uncertainty of RBF
// replacements and network propagation, we can always detect the transaction.
func (b *batch) monitorSpend(ctx context.Context, primarySweep sweep) error {
if b.spendChan != nil || b.spendErrChan != nil || b.reorgChan != nil {
return fmt.Errorf("an attempt to run monitorSpend multiple " +
"times per batch")
}
reorgChan := make(chan struct{}, 1)
spendChan, spendErrChan, err := b.chainNotifier.RegisterSpendNtfn(
ctx, &primarySweep.outpoint, primarySweep.htlc.PkScript,
primarySweep.initiationHeight,
lndclient.WithReOrgChan(reorgChan),
)
if err != nil {
return fmt.Errorf("failed to register spend notifier for "+
"primary sweep %v, pkscript %x, height %d: %w",
primarySweep.outpoint, primarySweep.htlc.PkScript,
primarySweep.initiationHeight, err)
}
b.Infof("monitoring spend for outpoint %s",
primarySweep.outpoint.String())
// This is safe to do as we always call monitorSpend from the event
// loop's goroutine.
b.spendChan = spendChan
b.spendErrChan = spendErrChan
b.reorgChan = reorgChan
return nil
}
// monitorConfirmations monitors the batch transaction for confirmations.
func (b *batch) monitorConfirmations(ctx context.Context) error {
// Find initiationHeight.
primarySweep, ok := b.sweeps[b.primarySweepID]
if !ok {
return fmt.Errorf("can't find primarySweep")
}
confCtx, cancel := context.WithCancel(ctx)
confChan, errChan, err := b.chainNotifier.RegisterConfirmationsNtfn(
confCtx, b.batchTxid, b.batchPkScript, batchConfHeight,
primarySweep.initiationHeight,
)
if err != nil {
cancel()
return err
}
b.wg.Add(1)
go func() {
defer cancel()
defer b.wg.Done()
select {
case conf := <-confChan:
select {
case b.confChan <- conf:
case <-ctx.Done():
}
case err := <-errChan:
b.writeToConfErrChan(ctx, err)
b.writeToErrChan(fmt.Errorf("confirmations "+
"monitoring error: %w", err))
case <-ctx.Done():
}
}()
return nil
}
// getFeePortionForSweep calculates the fee portion that each sweep should pay
// for the batch transaction. The fee is split evenly among the sweeps, If the
// fee cannot be split evenly, the remainder is paid by the first sweep.
func getFeePortionForSweep(spendTx *wire.MsgTx, numSweeps int,
totalSweptAmt btcutil.Amount) (btcutil.Amount, btcutil.Amount) {
totalFee := int64(totalSweptAmt)
if len(spendTx.TxOut) > 0 {
totalFee -= spendTx.TxOut[0].Value
}
feePortionPerSweep := totalFee / int64(numSweeps)
roundingDiff := totalFee - (int64(numSweeps) * feePortionPerSweep)
return btcutil.Amount(feePortionPerSweep), btcutil.Amount(roundingDiff)
}
// getFeePortionPaidBySweep returns the fee portion that the sweep should pay
// for the batch transaction. If the sweep is the primary sweep in the batch, it
// pays the rounding difference.
func getFeePortionPaidBySweep(feePortionPerSweep, roundingDiff btcutil.Amount,
primary bool) btcutil.Amount {
if primary {
return feePortionPerSweep + roundingDiff
}
return feePortionPerSweep
}
// handleSpend handles a spend notification.
func (b *batch) handleSpend(ctx context.Context, spendTx *wire.MsgTx) error {
var (
txHash = spendTx.TxHash()
notifyList = make([]sweep, 0, len(b.sweeps))
)
b.batchTxid = &txHash
if len(spendTx.TxOut) > 0 {
b.batchPkScript = spendTx.TxOut[0].PkScript
} else {
b.Warnf("transaction %v has no outputs", txHash)
}
// Make a set of confirmed sweeps.
confirmedSet := make(map[wire.OutPoint]struct{}, len(spendTx.TxIn))
for _, txIn := range spendTx.TxIn {
confirmedSet[txIn.PreviousOutPoint] = struct{}{}
}
// As a previous version of the batch transaction may get confirmed,
// which does not contain the latest sweeps, we need to detect which
// sweeps are in the transaction to correctly calculate fee portions
// and notify proper sweeps.
var (
totalSweptAmt btcutil.Amount
confirmedSweeps = []wire.OutPoint{}
)
for _, sweep := range b.sweeps {
// Skip sweeps that were not included into the confirmed tx.
_, found := confirmedSet[sweep.outpoint]
if !found {
continue
}
totalSweptAmt += sweep.value
notifyList = append(notifyList, sweep)
confirmedSweeps = append(confirmedSweeps, sweep.outpoint)
}
// Calculate the fee portion that each sweep should pay for the batch.
feePortionPaidPerSweep, roundingDifference := getFeePortionForSweep(
spendTx, len(notifyList), totalSweptAmt,
)
// Calculate fees per swaps. Only the first sweep in a swap has a
// notifier, so we calculate total fee per swap and send it to a sweep
// having that swap and a notifier.
swap2fee := make(map[lntypes.Hash]btcutil.Amount)
for _, sweep := range notifyList {
primary := sweep.outpoint == b.primarySweepID
swap2fee[sweep.swapHash] += getFeePortionPaidBySweep(
feePortionPaidPerSweep, roundingDifference, primary,
)
}
// Now send notifications to notifiers.
for _, sweep := range notifyList {
// If the sweep's notifier is empty then this means that a swap
// is not waiting to read an update from it or this is not the
// first sweep in a swap, so we can skip the notification part.
if sweep.notifier == nil ||
*sweep.notifier == (SpendNotifier{}) {
continue
}
// Make sure there is only one sweep with a notifier per swap
// hash, otherwise our fee calculation is incorrect.
fee, has := swap2fee[sweep.swapHash]
if !has {
return fmt.Errorf("no fee for swap %v; maybe "+
"multiple sweeps with a notifier per swap?",
sweep.swapHash)
}
delete(swap2fee, sweep.swapHash)
spendDetail := SpendDetail{
Tx: spendTx,
OnChainFeePortion: fee,
}
// Dispatch the sweep notifier, we don't care about the outcome
// of this action so we don't wait for it.
go func() {
// Make sure this context doesn't expire so we
// successfully notify the caller.
ctx := context.WithoutCancel(ctx)
sweep.notifySweepSpend(ctx, &spendDetail)
}()
}
b.Infof("spent, confirmed sweeps: %v", confirmedSweeps)
// We are no longer able to accept new sweeps, so we mark the batch as
// closed and persist on storage.
b.state = Closed
if err := b.persist(ctx); err != nil {
return fmt.Errorf("saving batch failed: %w", err)
}
if err := b.monitorConfirmations(ctx); err != nil {
return fmt.Errorf("monitorConfirmations failed: %w", err)
}
return nil
}
// handleConf handles a confirmation notification. This is the final step of the
// batch. Here we signal to the batcher that this batch was completed.
func (b *batch) handleConf(ctx context.Context,
conf *chainntnfs.TxConfirmation) error {
spendTx := conf.Tx
if spendTx == nil {
return fmt.Errorf("confirmation doesn't have spendTx, "+
"height=%d, TxIndex=%d", conf.BlockHeight, conf.TxIndex)
}
txHash := spendTx.TxHash()
if b.batchTxid == nil || *b.batchTxid != txHash {
b.Warnf("Mismatch of batch txid: tx in spend notification had "+
"txid %v, but confirmation notification has txif %v. "+
"Using the later.", b.batchTxid, txHash)
}
b.batchTxid = &txHash
b.Infof("confirmed in txid %s", b.batchTxid)
b.state = Confirmed
if err := b.persist(ctx); err != nil {
return fmt.Errorf("saving batch failed: %w", err)
}
// If the batch is in presigned mode, cleanup presignedHelper.
presigned, err := b.isPresigned()
if err != nil {
return fmt.Errorf("failed to determine if the batch %d uses "+
"presigned mode: %w", b.id, err)
}
// Sort sweeps by the addition order. This is important in presigned
// mode to pass them in correct order to purger (AddSweep) so the
// primary sweep is determined correctly and the presigned transaction
// is found. In regular mode the order doesn't matter, but we do it the
// same way for simplicity.
allSweeps, err := b.getOrderedSweeps(ctx)
if err != nil {
return fmt.Errorf("getOrderedSweeps(%d) failed: %w",
b.id, err)
}
// Make a set of confirmed sweeps.
confirmedSet := make(map[wire.OutPoint]struct{}, len(spendTx.TxIn))
for _, txIn := range spendTx.TxIn {
confirmedSet[txIn.PreviousOutPoint] = struct{}{}
}
// As a previous version of the batch transaction may get confirmed,
// which does not contain the latest sweeps, we need to detect the
// sweeps that did not make it to the confirmed transaction and feed
// them back to the batcher. This will ensure that the sweeps will enter
// a new batch instead of remaining dangling.
var (
confirmedSweeps = []wire.OutPoint{}
purgeList = make([]SweepRequest, 0, len(b.sweeps))
totalSweptAmt btcutil.Amount
)
for _, sweep := range allSweeps {
_, found := confirmedSet[sweep.outpoint]
if found {
// Save the sweep as completed. Note that sweeps are
// marked completed after the batch is marked confirmed
// because the check in handleSweeps checks sweep's
// status first and then checks the batch status.
err := b.persistSweep(ctx, sweep, true)
if err != nil {
return err
}
confirmedSweeps = append(
confirmedSweeps, sweep.outpoint,
)
totalSweptAmt += sweep.value
continue
}
// If the sweep's outpoint was not found in the transaction's
// inputs this means it was left out. So we delete it from this
// batch and feed it back to the batcher.
newSweep := sweep
delete(b.sweeps, sweep.outpoint)
newInput := Input{
Outpoint: newSweep.outpoint,
Value: newSweep.value,
}
// In presigned mode we should form a SweepRequest per swap
// (i.e. per group) and keep them ordered. It should reproduce
// the arguments and the order of the original external AddSweep
// calls.
L := len(purgeList)
if presigned && L != 0 &&
purgeList[L-1].SwapHash == newSweep.swapHash {
// Add the input to existing SweepRequest for this swap.
purgeList[L-1].Inputs = append(
purgeList[L-1].Inputs, newInput,
)
} else {
// Add the current sweep as a new element to purgeList.
// This is possible either in regular mode or in
// presigned mode in the beginning or on new swap.
purgeList = append(purgeList, SweepRequest{
SwapHash: newSweep.swapHash,
Inputs: []Input{newInput},
Notifier: newSweep.notifier,
})
}
}
var (
purgedSweeps = []wire.OutPoint{}
purgedSwaps = []lntypes.Hash{}
)
for _, sweepReq := range purgeList {
purgedSwaps = append(purgedSwaps, sweepReq.SwapHash)
for _, input := range sweepReq.Inputs {
purgedSweeps = append(purgedSweeps, input.Outpoint)
}
}
b.Infof("fully confirmed sweeps: %v, purged sweeps: %v, "+
"purged swaps: %v", confirmedSweeps, purgedSweeps, purgedSwaps)
// Proceed with purging the sweeps. This will feed the sweeps that
// didn't make it to the confirmed batch transaction back to the batcher
// for re-entry. This batch doesn't care for the outcome of this
// operation so we don't wait for it.
go func() {
// Make sure this context doesn't expire so we successfully
// add the sweeps to the batcher.
ctx := context.WithoutCancel(ctx)
// Iterate over the purge list and feed the sweeps back to the
// batcher.
for _, sweepReq := range purgeList {
err := b.purger(ctx, &sweepReq)
if err != nil {
b.Errorf("unable to purge sweep group %x: %v",
sweepReq.SwapHash[:6], err)
}
}
}()
if presigned {
b.Infof("Cleaning up presigned store")
inputs := make([]wire.OutPoint, 0, len(spendTx.TxIn))
for _, txIn := range spendTx.TxIn {
inputs = append(inputs, txIn.PreviousOutPoint)
}
err := b.cfg.presignedHelper.CleanupTransactions(ctx, inputs)
if err != nil {
return fmt.Errorf("failed to clean up store for "+
"batch %d, inputs %v: %w", b.id, inputs, err)
}
}
// Calculate the fee portion that each sweep should pay for the batch.
feePortionPaidPerSweep, roundingDifference := getFeePortionForSweep(
spendTx, len(b.sweeps), totalSweptAmt,
)
// Calculate fees per swaps. Only the first sweep in a swap has a
// notifier, so we calculate total fee per swap and send it to a sweep
// having that swap and a notifier.
swap2fee := make(map[lntypes.Hash]btcutil.Amount)
for _, sweep := range b.sweeps {
primary := sweep.outpoint == b.primarySweepID
swap2fee[sweep.swapHash] += getFeePortionPaidBySweep(
feePortionPaidPerSweep, roundingDifference, primary,
)
}
// Send the confirmation to all the notifiers.
for _, s := range b.sweeps {
// If the sweep's notifier is empty then this means that
// a swap is not waiting to read an update from it, so
// we can skip the notification part.
if s.notifier == nil || s.notifier.ConfChan == nil {
continue
}
// Make sure there is only one sweep with a notifier per swap
// hash, otherwise our fee calculation is incorrect.
fee, has := swap2fee[s.swapHash]
if !has {
return fmt.Errorf("no fee for swap %v; maybe "+
"multiple sweeps with a notifier per swap?",
s.swapHash)
}
delete(swap2fee, s.swapHash)
confDetail := &ConfDetail{
TxConfirmation: conf,
OnChainFeePortion: fee,
}
// Notify the caller in a goroutine to avoid possible dead-lock.
go func(notifier *SpendNotifier) {
// Note that we don't unblock on ctx, because it will
// expire soon, when batch.Run completes. The caller is
// responsible to consume ConfChan or close QuitChan.
select {
// Try to write the confirmation to the notification
// channel.
case notifier.ConfChan <- confDetail:
// If a quit signal was provided by the swap,
// continue.
case <-notifier.QuitChan:
}
}(s.notifier)
}
return nil
}
// isComplete returns true if the batch is completed. This method is used by the
// batcher for lazy deletion of batches.
func (b *batch) isComplete() bool {
done, err := b.scheduleNextCall()
defer done()
// We override the ErrBatchShuttingDown error as that is the expected
// error to be returned by the scheduler once the batch's main run loop
// has exited.
if err != nil && err != ErrBatchShuttingDown {
return false
}
return b.state == Confirmed
}
// persist updates the batch in the database.
func (b *batch) persist(ctx context.Context) error {
bch := &dbBatch{}
bch.ID = b.id
bch.Confirmed = b.state == Confirmed
if b.batchTxid != nil {
bch.BatchTxid = *b.batchTxid
}
bch.BatchPkScript = b.batchPkScript
bch.LastRbfHeight = b.rbfCache.LastHeight
bch.LastRbfSatPerKw = int32(b.rbfCache.FeeRate)
bch.MaxTimeoutDistance = b.cfg.maxTimeoutDistance
return b.store.UpdateSweepBatch(ctx, bch)
}
// getBatchDestAddr returns the batch's destination address. If the batch
// has already generated an address then the same one will be returned.
// The method must not be used in presigned mode. Use getPresignedSweepsDestAddr
// instead.
func (b *batch) getBatchDestAddr(ctx context.Context) (btcutil.Address, error) {
// Determine if we should use presigned mode for the batch.
presigned, err := b.isPresigned()
if err != nil {
return nil, fmt.Errorf("failed to determine if the batch %d "+
"uses presigned mode: %w", b.id, err)
}
// Make sure that the method is not used for presigned batches.
if presigned {
return nil, fmt.Errorf("getBatchDestAddr used in presigned " +
"mode")
}
var address btcutil.Address
// If a batch address is set, use that. Otherwise, generate a
// new address.
if b.batchAddress != nil {
address = b.batchAddress
} else {
var err error
// Generate a wallet address for the batch transaction's output.
address, err = b.wallet.NextAddr(
ctx, "", walletrpc.AddressType_TAPROOT_PUBKEY, false,
)
if err != nil {
return address, err
}
// Save that new address in order to re-use in future
// versions of the batch tx.
b.batchAddress = address
}
return address, nil
}
func (b *batch) insertAndAcquireID(ctx context.Context) (int32, error) {
bch := &dbBatch{}
bch.Confirmed = b.state == Confirmed
bch.MaxTimeoutDistance = b.cfg.maxTimeoutDistance
id, err := b.store.InsertSweepBatch(ctx, bch)
if err != nil {
return 0, err
}
b.id = id
b.setLog(batchPrefixLogger(fmt.Sprintf("%d", b.id)))
return id, nil
}
// notifySweepSpend writes the spendTx to the sweep's notifier channel.
func (s *sweep) notifySweepSpend(ctx context.Context,
spendDetail *SpendDetail) {
select {
// Try to write the update to the notification channel.
case s.notifier.SpendChan <- spendDetail:
// If a quit signal was provided by the swap, continue.
case <-s.notifier.QuitChan:
// If the context was canceled, return.
case <-ctx.Done():
}
}
func (b *batch) writeToErrChan(err error) {
select {
case b.errChan <- err:
default:
}
}
// writeToSpendErrChan sends an error to spend error channels of all the sweeps.
func (b *batch) writeToSpendErrChan(ctx context.Context, spendErr error) {
notifiers := make([]*SpendNotifier, 0, len(b.sweeps))
for _, s := range b.sweeps {
// If the sweep's notifier is empty then this means that a swap
// is not waiting to read an update from it, so we can skip
// the notification part.
if s.notifier == nil || s.notifier.SpendErrChan == nil {
continue
}
notifiers = append(notifiers, s.notifier)
}
for _, notifier := range notifiers {
select {
// Try to write the error to the notification
// channel.
case notifier.SpendErrChan <- spendErr:
// If a quit signal was provided by the swap,
// continue.
case <-notifier.QuitChan:
// If the context was canceled, stop.
case <-ctx.Done():
}
}
}
// writeToConfErrChan sends an error to confirmation error channels of all the
// sweeps.
func (b *batch) writeToConfErrChan(ctx context.Context, confErr error) {
done, err := b.scheduleNextCall()
if err != nil {
done()
return
}
notifiers := make([]*SpendNotifier, 0, len(b.sweeps))
for _, s := range b.sweeps {
// If the sweep's notifier is empty then this means that a swap
// is not waiting to read an update from it, so we can skip
// the notification part.
if s.notifier == nil || s.notifier.ConfErrChan == nil {
continue
}
notifiers = append(notifiers, s.notifier)
}
done()
for _, notifier := range notifiers {
select {
// Try to write the error to the notification
// channel.
case notifier.ConfErrChan <- confErr:
// If a quit signal was provided by the swap,
// continue.
case <-notifier.QuitChan:
// If the context was canceled, stop.
case <-ctx.Done():
}
}
}
func (b *batch) persistSweep(ctx context.Context, sweep sweep,
completed bool) error {
return b.store.UpsertSweep(ctx, &dbSweep{
BatchID: b.id,
SwapHash: sweep.swapHash,
Outpoint: sweep.outpoint,
Amount: sweep.value,
Completed: completed,
})
}
// clampBatchFee takes the fee amount and total amount of the sweeps in the
// batch and makes sure the fee is not too high. If the fee is too high, it is
// clamped to the maximum allowed fee. If the clamped fee results in a fee rate
// below the minimum relay fee, an error is returned.
func clampBatchFee(fee btcutil.Amount, totalAmount btcutil.Amount,
minRelayFeeRate chainfee.SatPerKWeight,
weight lntypes.WeightUnit) (btcutil.Amount, error) {
maxFeeAmount := btcutil.Amount(float64(totalAmount) *
maxFeeToSwapAmtRatio)
clampedFee := fee
if fee > maxFeeAmount {
clampedFee = maxFeeAmount
}
clampedFeeRate := chainfee.NewSatPerKWeight(clampedFee, weight)
if clampedFeeRate < minRelayFeeRate {
return 0, fmt.Errorf("clamped fee rate %v is less than "+
"minimum relay fee %v", clampedFeeRate, minRelayFeeRate)
}
return clampedFee, nil
}