mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 09:53:54 +01:00
cb93f8c01a
In this commit, we add a new AuxSweeper interface. This'll take a set of inputs, and a change addr for the sweep transaction, then optionally return a new sweep output to be added to the sweep transaction. We also add a new NotifyBroadcast method. This'll be used to notify that we're _about_ to broadcast a sweeping transaction. The set of inputs is passed in, which allows the caller to prepare for the ultimate broadcast of the sweeping transaction. We also add ExtraTxOut to BumpRequest pass fees to NotifyBroadcast. This allows the callee to know the total fee of the sweeping transaction.
1775 lines
54 KiB
Go
1775 lines
54 KiB
Go
package sweep
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/davecgh/go-spew/spew"
|
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
|
"github.com/lightningnetwork/lnd/fn"
|
|
"github.com/lightningnetwork/lnd/input"
|
|
"github.com/lightningnetwork/lnd/lnutils"
|
|
"github.com/lightningnetwork/lnd/lnwallet"
|
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
|
)
|
|
|
|
var (
|
|
// ErrRemoteSpend is returned in case an output that we try to sweep is
|
|
// confirmed in a tx of the remote party.
|
|
ErrRemoteSpend = errors.New("remote party swept utxo")
|
|
|
|
// ErrFeePreferenceTooLow is returned when the fee preference gives a
|
|
// fee rate that's below the relay fee rate.
|
|
ErrFeePreferenceTooLow = errors.New("fee preference too low")
|
|
|
|
// ErrExclusiveGroupSpend is returned in case a different input of the
|
|
// same exclusive group was spent.
|
|
ErrExclusiveGroupSpend = errors.New("other member of exclusive group " +
|
|
"was spent")
|
|
|
|
// ErrSweeperShuttingDown is an error returned when a client attempts to
|
|
// make a request to the UtxoSweeper, but it is unable to handle it as
|
|
// it is/has already been stopped.
|
|
ErrSweeperShuttingDown = errors.New("utxo sweeper shutting down")
|
|
|
|
// DefaultDeadlineDelta defines a default deadline delta (1 week) to be
|
|
// used when sweeping inputs with no deadline pressure.
|
|
DefaultDeadlineDelta = int32(1008)
|
|
)
|
|
|
|
// Params contains the parameters that control the sweeping process.
|
|
type Params struct {
|
|
// ExclusiveGroup is an identifier that, if set, prevents other inputs
|
|
// with the same identifier from being batched together.
|
|
ExclusiveGroup *uint64
|
|
|
|
// DeadlineHeight specifies an absolute block height that this input
|
|
// should be confirmed by. This value is used by the fee bumper to
|
|
// decide its urgency and adjust its feerate used.
|
|
DeadlineHeight fn.Option[int32]
|
|
|
|
// Budget specifies the maximum amount of satoshis that can be spent on
|
|
// fees for this sweep.
|
|
Budget btcutil.Amount
|
|
|
|
// Immediate indicates that the input should be swept immediately
|
|
// without waiting for blocks to come to trigger the sweeping of
|
|
// inputs.
|
|
Immediate bool
|
|
|
|
// StartingFeeRate is an optional parameter that can be used to specify
|
|
// the initial fee rate to use for the fee function.
|
|
StartingFeeRate fn.Option[chainfee.SatPerKWeight]
|
|
}
|
|
|
|
// String returns a human readable interpretation of the sweep parameters.
|
|
func (p Params) String() string {
|
|
deadline := "none"
|
|
p.DeadlineHeight.WhenSome(func(d int32) {
|
|
deadline = fmt.Sprintf("%d", d)
|
|
})
|
|
|
|
exclusiveGroup := "none"
|
|
if p.ExclusiveGroup != nil {
|
|
exclusiveGroup = fmt.Sprintf("%d", *p.ExclusiveGroup)
|
|
}
|
|
|
|
return fmt.Sprintf("startingFeeRate=%v, immediate=%v, "+
|
|
"exclusive_group=%v, budget=%v, deadline=%v", p.StartingFeeRate,
|
|
p.Immediate, exclusiveGroup, p.Budget, deadline)
|
|
}
|
|
|
|
// SweepState represents the current state of a pending input.
|
|
//
|
|
//nolint:revive
|
|
type SweepState uint8
|
|
|
|
const (
|
|
// Init is the initial state of a pending input. This is set when a new
|
|
// sweeping request for a given input is made.
|
|
Init SweepState = iota
|
|
|
|
// PendingPublish specifies an input's state where it's already been
|
|
// included in a sweeping tx but the tx is not published yet. Inputs
|
|
// in this state should not be used for grouping again.
|
|
PendingPublish
|
|
|
|
// Published is the state where the input's sweeping tx has
|
|
// successfully been published. Inputs in this state can only be
|
|
// updated via RBF.
|
|
Published
|
|
|
|
// PublishFailed is the state when an error is returned from publishing
|
|
// the sweeping tx. Inputs in this state can be re-grouped in to a new
|
|
// sweeping tx.
|
|
PublishFailed
|
|
|
|
// Swept is the final state of a pending input. This is set when the
|
|
// input has been successfully swept.
|
|
Swept
|
|
|
|
// Excluded is the state of a pending input that has been excluded and
|
|
// can no longer be swept. For instance, when one of the three anchor
|
|
// sweeping transactions confirmed, the remaining two will be excluded.
|
|
Excluded
|
|
|
|
// Failed is the state when a pending input has too many failed publish
|
|
// atttempts or unknown broadcast error is returned.
|
|
Failed
|
|
)
|
|
|
|
// String gives a human readable text for the sweep states.
|
|
func (s SweepState) String() string {
|
|
switch s {
|
|
case Init:
|
|
return "Init"
|
|
|
|
case PendingPublish:
|
|
return "PendingPublish"
|
|
|
|
case Published:
|
|
return "Published"
|
|
|
|
case PublishFailed:
|
|
return "PublishFailed"
|
|
|
|
case Swept:
|
|
return "Swept"
|
|
|
|
case Excluded:
|
|
return "Excluded"
|
|
|
|
case Failed:
|
|
return "Failed"
|
|
|
|
default:
|
|
return "Unknown"
|
|
}
|
|
}
|
|
|
|
// RBFInfo stores the information required to perform a RBF bump on a pending
|
|
// sweeping tx.
|
|
type RBFInfo struct {
|
|
// Txid is the txid of the sweeping tx.
|
|
Txid chainhash.Hash
|
|
|
|
// FeeRate is the fee rate of the sweeping tx.
|
|
FeeRate chainfee.SatPerKWeight
|
|
|
|
// Fee is the total fee of the sweeping tx.
|
|
Fee btcutil.Amount
|
|
}
|
|
|
|
// SweeperInput is created when an input reaches the main loop for the first
|
|
// time. It wraps the input and tracks all relevant state that is needed for
|
|
// sweeping.
|
|
type SweeperInput struct {
|
|
input.Input
|
|
|
|
// state tracks the current state of the input.
|
|
state SweepState
|
|
|
|
// listeners is a list of channels over which the final outcome of the
|
|
// sweep needs to be broadcasted.
|
|
listeners []chan Result
|
|
|
|
// ntfnRegCancel is populated with a function that cancels the chain
|
|
// notifier spend registration.
|
|
ntfnRegCancel func()
|
|
|
|
// publishAttempts records the number of attempts that have already been
|
|
// made to sweep this tx.
|
|
publishAttempts int
|
|
|
|
// params contains the parameters that control the sweeping process.
|
|
params Params
|
|
|
|
// lastFeeRate is the most recent fee rate used for this input within a
|
|
// transaction broadcast to the network.
|
|
lastFeeRate chainfee.SatPerKWeight
|
|
|
|
// rbf records the RBF constraints.
|
|
rbf fn.Option[RBFInfo]
|
|
|
|
// DeadlineHeight is the deadline height for this input. This is
|
|
// different from the DeadlineHeight in its params as it's an actual
|
|
// value than an option.
|
|
DeadlineHeight int32
|
|
}
|
|
|
|
// String returns a human readable interpretation of the pending input.
|
|
func (p *SweeperInput) String() string {
|
|
return fmt.Sprintf("%v (%v)", p.Input.OutPoint(), p.Input.WitnessType())
|
|
}
|
|
|
|
// terminated returns a boolean indicating whether the input has reached a
|
|
// final state.
|
|
func (p *SweeperInput) terminated() bool {
|
|
switch p.state {
|
|
// If the input has reached a final state, that it's either
|
|
// been swept, or failed, or excluded, we will remove it from
|
|
// our sweeper.
|
|
case Failed, Swept, Excluded:
|
|
return true
|
|
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// InputsMap is a type alias for a set of pending inputs.
|
|
type InputsMap = map[wire.OutPoint]*SweeperInput
|
|
|
|
// pendingSweepsReq is an internal message we'll use to represent an external
|
|
// caller's intent to retrieve all of the pending inputs the UtxoSweeper is
|
|
// attempting to sweep.
|
|
type pendingSweepsReq struct {
|
|
respChan chan map[wire.OutPoint]*PendingInputResponse
|
|
errChan chan error
|
|
}
|
|
|
|
// PendingInputResponse contains information about an input that is currently
|
|
// being swept by the UtxoSweeper.
|
|
type PendingInputResponse struct {
|
|
// OutPoint is the identify outpoint of the input being swept.
|
|
OutPoint wire.OutPoint
|
|
|
|
// WitnessType is the witness type of the input being swept.
|
|
WitnessType input.WitnessType
|
|
|
|
// Amount is the amount of the input being swept.
|
|
Amount btcutil.Amount
|
|
|
|
// LastFeeRate is the most recent fee rate used for the input being
|
|
// swept within a transaction broadcast to the network.
|
|
LastFeeRate chainfee.SatPerKWeight
|
|
|
|
// BroadcastAttempts is the number of attempts we've made to sweept the
|
|
// input.
|
|
BroadcastAttempts int
|
|
|
|
// Params contains the sweep parameters for this pending request.
|
|
Params Params
|
|
|
|
// DeadlineHeight records the deadline height of this input.
|
|
DeadlineHeight uint32
|
|
}
|
|
|
|
// updateReq is an internal message we'll use to represent an external caller's
|
|
// intent to update the sweep parameters of a given input.
|
|
type updateReq struct {
|
|
input wire.OutPoint
|
|
params Params
|
|
responseChan chan *updateResp
|
|
}
|
|
|
|
// updateResp is an internal message we'll use to hand off the response of a
|
|
// updateReq from the UtxoSweeper's main event loop back to the caller.
|
|
type updateResp struct {
|
|
resultChan chan Result
|
|
err error
|
|
}
|
|
|
|
// UtxoSweeper is responsible for sweeping outputs back into the wallet
|
|
type UtxoSweeper struct {
|
|
started uint32 // To be used atomically.
|
|
stopped uint32 // To be used atomically.
|
|
|
|
cfg *UtxoSweeperConfig
|
|
|
|
newInputs chan *sweepInputMessage
|
|
spendChan chan *chainntnfs.SpendDetail
|
|
|
|
// pendingSweepsReq is a channel that will be sent requests by external
|
|
// callers in order to retrieve the set of pending inputs the
|
|
// UtxoSweeper is attempting to sweep.
|
|
pendingSweepsReqs chan *pendingSweepsReq
|
|
|
|
// updateReqs is a channel that will be sent requests by external
|
|
// callers who wish to bump the fee rate of a given input.
|
|
updateReqs chan *updateReq
|
|
|
|
// inputs is the total set of inputs the UtxoSweeper has been requested
|
|
// to sweep.
|
|
inputs InputsMap
|
|
|
|
currentOutputScript fn.Option[lnwallet.AddrWithKey]
|
|
|
|
relayFeeRate chainfee.SatPerKWeight
|
|
|
|
quit chan struct{}
|
|
wg sync.WaitGroup
|
|
|
|
// currentHeight is the best known height of the main chain. This is
|
|
// updated whenever a new block epoch is received.
|
|
currentHeight int32
|
|
|
|
// bumpResultChan is a channel that receives broadcast results from the
|
|
// TxPublisher.
|
|
bumpResultChan chan *BumpResult
|
|
}
|
|
|
|
// UtxoSweeperConfig contains dependencies of UtxoSweeper.
|
|
type UtxoSweeperConfig struct {
|
|
// GenSweepScript generates a P2WKH script belonging to the wallet where
|
|
// funds can be swept.
|
|
GenSweepScript func() fn.Result[lnwallet.AddrWithKey]
|
|
|
|
// FeeEstimator is used when crafting sweep transactions to estimate
|
|
// the necessary fee relative to the expected size of the sweep
|
|
// transaction.
|
|
FeeEstimator chainfee.Estimator
|
|
|
|
// Wallet contains the wallet functions that sweeper requires.
|
|
Wallet Wallet
|
|
|
|
// Notifier is an instance of a chain notifier we'll use to watch for
|
|
// certain on-chain events.
|
|
Notifier chainntnfs.ChainNotifier
|
|
|
|
// Mempool is the mempool watcher that will be used to query whether a
|
|
// given input is already being spent by a transaction in the mempool.
|
|
Mempool chainntnfs.MempoolWatcher
|
|
|
|
// Store stores the published sweeper txes.
|
|
Store SweeperStore
|
|
|
|
// Signer is used by the sweeper to generate valid witnesses at the
|
|
// time the incubated outputs need to be spent.
|
|
Signer input.Signer
|
|
|
|
// MaxInputsPerTx specifies the default maximum number of inputs allowed
|
|
// in a single sweep tx. If more need to be swept, multiple txes are
|
|
// created and published.
|
|
MaxInputsPerTx uint32
|
|
|
|
// MaxFeeRate is the maximum fee rate allowed within the UtxoSweeper.
|
|
MaxFeeRate chainfee.SatPerVByte
|
|
|
|
// Aggregator is used to group inputs into clusters based on its
|
|
// implemention-specific strategy.
|
|
Aggregator UtxoAggregator
|
|
|
|
// Publisher is used to publish the sweep tx crafted here and monitors
|
|
// it for potential fee bumps.
|
|
Publisher Bumper
|
|
|
|
// NoDeadlineConfTarget is the conf target to use when sweeping
|
|
// non-time-sensitive outputs.
|
|
NoDeadlineConfTarget uint32
|
|
}
|
|
|
|
// Result is the struct that is pushed through the result channel. Callers can
|
|
// use this to be informed of the final sweep result. In case of a remote
|
|
// spend, Err will be ErrRemoteSpend.
|
|
type Result struct {
|
|
// Err is the final result of the sweep. It is nil when the input is
|
|
// swept successfully by us. ErrRemoteSpend is returned when another
|
|
// party took the input.
|
|
Err error
|
|
|
|
// Tx is the transaction that spent the input.
|
|
Tx *wire.MsgTx
|
|
}
|
|
|
|
// sweepInputMessage structs are used in the internal channel between the
|
|
// SweepInput call and the sweeper main loop.
|
|
type sweepInputMessage struct {
|
|
input input.Input
|
|
params Params
|
|
resultChan chan Result
|
|
}
|
|
|
|
// New returns a new Sweeper instance.
|
|
func New(cfg *UtxoSweeperConfig) *UtxoSweeper {
|
|
return &UtxoSweeper{
|
|
cfg: cfg,
|
|
newInputs: make(chan *sweepInputMessage),
|
|
spendChan: make(chan *chainntnfs.SpendDetail),
|
|
updateReqs: make(chan *updateReq),
|
|
pendingSweepsReqs: make(chan *pendingSweepsReq),
|
|
quit: make(chan struct{}),
|
|
inputs: make(InputsMap),
|
|
bumpResultChan: make(chan *BumpResult, 100),
|
|
}
|
|
}
|
|
|
|
// Start starts the process of constructing and publish sweep txes.
|
|
func (s *UtxoSweeper) Start() error {
|
|
if !atomic.CompareAndSwapUint32(&s.started, 0, 1) {
|
|
return nil
|
|
}
|
|
|
|
log.Info("Sweeper starting")
|
|
|
|
// Retrieve relay fee for dust limit calculation. Assume that this will
|
|
// not change from here on.
|
|
s.relayFeeRate = s.cfg.FeeEstimator.RelayFeePerKW()
|
|
|
|
// We need to register for block epochs and retry sweeping every block.
|
|
// We should get a notification with the current best block immediately
|
|
// if we don't provide any epoch. We'll wait for that in the collector.
|
|
blockEpochs, err := s.cfg.Notifier.RegisterBlockEpochNtfn(nil)
|
|
if err != nil {
|
|
return fmt.Errorf("register block epoch ntfn: %w", err)
|
|
}
|
|
|
|
// Start sweeper main loop.
|
|
s.wg.Add(1)
|
|
go func() {
|
|
defer blockEpochs.Cancel()
|
|
defer s.wg.Done()
|
|
|
|
s.collector(blockEpochs.Epochs)
|
|
|
|
// The collector exited and won't longer handle incoming
|
|
// requests. This can happen on shutdown, when the block
|
|
// notifier shuts down before the sweeper and its clients. In
|
|
// order to not deadlock the clients waiting for their requests
|
|
// being handled, we handle them here and immediately return an
|
|
// error. When the sweeper finally is shut down we can exit as
|
|
// the clients will be notified.
|
|
for {
|
|
select {
|
|
case inp := <-s.newInputs:
|
|
inp.resultChan <- Result{
|
|
Err: ErrSweeperShuttingDown,
|
|
}
|
|
|
|
case req := <-s.pendingSweepsReqs:
|
|
req.errChan <- ErrSweeperShuttingDown
|
|
|
|
case req := <-s.updateReqs:
|
|
req.responseChan <- &updateResp{
|
|
err: ErrSweeperShuttingDown,
|
|
}
|
|
|
|
case <-s.quit:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
// RelayFeePerKW returns the minimum fee rate required for transactions to be
|
|
// relayed.
|
|
func (s *UtxoSweeper) RelayFeePerKW() chainfee.SatPerKWeight {
|
|
return s.relayFeeRate
|
|
}
|
|
|
|
// Stop stops sweeper from listening to block epochs and constructing sweep
|
|
// txes.
|
|
func (s *UtxoSweeper) Stop() error {
|
|
if !atomic.CompareAndSwapUint32(&s.stopped, 0, 1) {
|
|
return nil
|
|
}
|
|
|
|
log.Info("Sweeper shutting down...")
|
|
defer log.Debug("Sweeper shutdown complete")
|
|
|
|
close(s.quit)
|
|
s.wg.Wait()
|
|
|
|
return nil
|
|
}
|
|
|
|
// SweepInput sweeps inputs back into the wallet. The inputs will be batched and
|
|
// swept after the batch time window ends. A custom fee preference can be
|
|
// provided to determine what fee rate should be used for the input. Note that
|
|
// the input may not always be swept with this exact value, as its possible for
|
|
// it to be batched under the same transaction with other similar fee rate
|
|
// inputs.
|
|
//
|
|
// NOTE: Extreme care needs to be taken that input isn't changed externally.
|
|
// Because it is an interface and we don't know what is exactly behind it, we
|
|
// cannot make a local copy in sweeper.
|
|
//
|
|
// TODO(yy): make sure the caller is using the Result chan.
|
|
func (s *UtxoSweeper) SweepInput(inp input.Input,
|
|
params Params) (chan Result, error) {
|
|
|
|
if inp == nil || inp.OutPoint() == input.EmptyOutPoint ||
|
|
inp.SignDesc() == nil {
|
|
|
|
return nil, errors.New("nil input received")
|
|
}
|
|
|
|
absoluteTimeLock, _ := inp.RequiredLockTime()
|
|
log.Infof("Sweep request received: out_point=%v, witness_type=%v, "+
|
|
"relative_time_lock=%v, absolute_time_lock=%v, amount=%v, "+
|
|
"parent=(%v), params=(%v)", inp.OutPoint(), inp.WitnessType(),
|
|
inp.BlocksToMaturity(), absoluteTimeLock,
|
|
btcutil.Amount(inp.SignDesc().Output.Value),
|
|
inp.UnconfParent(), params)
|
|
|
|
sweeperInput := &sweepInputMessage{
|
|
input: inp,
|
|
params: params,
|
|
resultChan: make(chan Result, 1),
|
|
}
|
|
|
|
// Deliver input to the main event loop.
|
|
select {
|
|
case s.newInputs <- sweeperInput:
|
|
case <-s.quit:
|
|
return nil, ErrSweeperShuttingDown
|
|
}
|
|
|
|
return sweeperInput.resultChan, nil
|
|
}
|
|
|
|
// removeConflictSweepDescendants removes any transactions from the wallet that
|
|
// spend outputs included in the passed outpoint set. This needs to be done in
|
|
// cases where we're not the only ones that can sweep an output, but there may
|
|
// exist unconfirmed spends that spend outputs created by a sweep transaction.
|
|
// The most common case for this is when someone sweeps our anchor outputs
|
|
// after 16 blocks. Moreover this is also needed for wallets which use neutrino
|
|
// as a backend when a channel is force closed and anchor cpfp txns are
|
|
// created to bump the initial commitment transaction. In this case an anchor
|
|
// cpfp is broadcasted for up to 3 commitment transactions (local,
|
|
// remote-dangling, remote). Using neutrino all of those transactions will be
|
|
// accepted (the commitment tx will be different in all of those cases) and have
|
|
// to be removed as soon as one of them confirmes (they do have the same
|
|
// ExclusiveGroup). For neutrino backends the corresponding BIP 157 serving full
|
|
// nodes do not signal invalid transactions anymore.
|
|
func (s *UtxoSweeper) removeConflictSweepDescendants(
|
|
outpoints map[wire.OutPoint]struct{}) error {
|
|
|
|
// Obtain all the past sweeps that we've done so far. We'll need these
|
|
// to ensure that if the spendingTx spends any of the same inputs, then
|
|
// we remove any transaction that may be spending those inputs from the
|
|
// wallet.
|
|
//
|
|
// TODO(roasbeef): can be last sweep here if we remove anything confirmed
|
|
// from the store?
|
|
pastSweepHashes, err := s.cfg.Store.ListSweeps()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// We'll now go through each past transaction we published during this
|
|
// epoch and cross reference the spent inputs. If there're any inputs
|
|
// in common with the inputs the spendingTx spent, then we'll remove
|
|
// those.
|
|
//
|
|
// TODO(roasbeef): need to start to remove all transaction hashes after
|
|
// every N blocks (assumed point of no return)
|
|
for _, sweepHash := range pastSweepHashes {
|
|
sweepTx, err := s.cfg.Wallet.FetchTx(sweepHash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Transaction wasn't found in the wallet, may have already
|
|
// been replaced/removed.
|
|
if sweepTx == nil {
|
|
// If it was removed, then we'll play it safe and mark
|
|
// it as no longer need to be rebroadcasted.
|
|
s.cfg.Wallet.CancelRebroadcast(sweepHash)
|
|
continue
|
|
}
|
|
|
|
// Check to see if this past sweep transaction spent any of the
|
|
// same inputs as spendingTx.
|
|
var isConflicting bool
|
|
for _, txIn := range sweepTx.TxIn {
|
|
if _, ok := outpoints[txIn.PreviousOutPoint]; ok {
|
|
isConflicting = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !isConflicting {
|
|
continue
|
|
}
|
|
|
|
// If it is conflicting, then we'll signal the wallet to remove
|
|
// all the transactions that are descendants of outputs created
|
|
// by the sweepTx and the sweepTx itself.
|
|
log.Debugf("Removing sweep txid=%v from wallet: %v",
|
|
sweepTx.TxHash(), spew.Sdump(sweepTx))
|
|
|
|
err = s.cfg.Wallet.RemoveDescendants(sweepTx)
|
|
if err != nil {
|
|
log.Warnf("Unable to remove descendants: %v", err)
|
|
}
|
|
|
|
// If this transaction was conflicting, then we'll stop
|
|
// rebroadcasting it in the background.
|
|
s.cfg.Wallet.CancelRebroadcast(sweepHash)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// collector is the sweeper main loop. It processes new inputs, spend
|
|
// notifications and counts down to publication of the sweep tx.
|
|
func (s *UtxoSweeper) collector(blockEpochs <-chan *chainntnfs.BlockEpoch) {
|
|
// We registered for the block epochs with a nil request. The notifier
|
|
// should send us the current best block immediately. So we need to wait
|
|
// for it here because we need to know the current best height.
|
|
select {
|
|
case bestBlock := <-blockEpochs:
|
|
s.currentHeight = bestBlock.Height
|
|
|
|
case <-s.quit:
|
|
return
|
|
}
|
|
|
|
for {
|
|
// Clean inputs, which will remove inputs that are swept,
|
|
// failed, or excluded from the sweeper and return inputs that
|
|
// are either new or has been published but failed back, which
|
|
// will be retried again here.
|
|
s.updateSweeperInputs()
|
|
|
|
select {
|
|
// A new inputs is offered to the sweeper. We check to see if
|
|
// we are already trying to sweep this input and if not, set up
|
|
// a listener to spend and schedule a sweep.
|
|
case input := <-s.newInputs:
|
|
err := s.handleNewInput(input)
|
|
if err != nil {
|
|
log.Criticalf("Unable to handle new input: %v",
|
|
err)
|
|
|
|
return
|
|
}
|
|
|
|
// If this input is forced, we perform an sweep
|
|
// immediately.
|
|
//
|
|
// TODO(ziggie): Make sure when `immediate` is selected
|
|
// as a parameter that we only trigger the sweeping of
|
|
// this specific input rather than triggering the sweeps
|
|
// of all current pending inputs registered with the
|
|
// sweeper.
|
|
if input.params.Immediate {
|
|
inputs := s.updateSweeperInputs()
|
|
s.sweepPendingInputs(inputs)
|
|
}
|
|
|
|
// A spend of one of our inputs is detected. Signal sweep
|
|
// results to the caller(s).
|
|
case spend := <-s.spendChan:
|
|
s.handleInputSpent(spend)
|
|
|
|
// A new external request has been received to retrieve all of
|
|
// the inputs we're currently attempting to sweep.
|
|
case req := <-s.pendingSweepsReqs:
|
|
s.handlePendingSweepsReq(req)
|
|
|
|
// A new external request has been received to bump the fee rate
|
|
// of a given input.
|
|
case req := <-s.updateReqs:
|
|
resultChan, err := s.handleUpdateReq(req)
|
|
req.responseChan <- &updateResp{
|
|
resultChan: resultChan,
|
|
err: err,
|
|
}
|
|
|
|
// Perform an sweep immediately if asked.
|
|
if req.params.Immediate {
|
|
inputs := s.updateSweeperInputs()
|
|
s.sweepPendingInputs(inputs)
|
|
}
|
|
|
|
case result := <-s.bumpResultChan:
|
|
// Handle the bump event.
|
|
err := s.handleBumpEvent(result)
|
|
if err != nil {
|
|
log.Errorf("Failed to handle bump event: %v",
|
|
err)
|
|
}
|
|
|
|
// A new block comes in, update the bestHeight, perform a check
|
|
// over all pending inputs and publish sweeping txns if needed.
|
|
case epoch, ok := <-blockEpochs:
|
|
if !ok {
|
|
// We should stop the sweeper before stopping
|
|
// the chain service. Otherwise it indicates an
|
|
// error.
|
|
log.Error("Block epoch channel closed")
|
|
|
|
return
|
|
}
|
|
|
|
// Update the sweeper to the best height.
|
|
s.currentHeight = epoch.Height
|
|
|
|
// Update the inputs with the latest height.
|
|
inputs := s.updateSweeperInputs()
|
|
|
|
log.Debugf("Received new block: height=%v, attempt "+
|
|
"sweeping %d inputs", epoch.Height, len(inputs))
|
|
|
|
// Attempt to sweep any pending inputs.
|
|
s.sweepPendingInputs(inputs)
|
|
|
|
case <-s.quit:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// removeExclusiveGroup removes all inputs in the given exclusive group. This
|
|
// function is called when one of the exclusive group inputs has been spent. The
|
|
// other inputs won't ever be spendable and can be removed. This also prevents
|
|
// them from being part of future sweep transactions that would fail. In
|
|
// addition sweep transactions of those inputs will be removed from the wallet.
|
|
func (s *UtxoSweeper) removeExclusiveGroup(group uint64) {
|
|
for outpoint, input := range s.inputs {
|
|
outpoint := outpoint
|
|
|
|
// Skip inputs that aren't exclusive.
|
|
if input.params.ExclusiveGroup == nil {
|
|
continue
|
|
}
|
|
|
|
// Skip inputs from other exclusive groups.
|
|
if *input.params.ExclusiveGroup != group {
|
|
continue
|
|
}
|
|
|
|
// Skip inputs that are already terminated.
|
|
if input.terminated() {
|
|
log.Tracef("Skipped sending error result for "+
|
|
"input %v, state=%v", outpoint, input.state)
|
|
|
|
continue
|
|
}
|
|
|
|
// Signal result channels.
|
|
s.signalResult(input, Result{
|
|
Err: ErrExclusiveGroupSpend,
|
|
})
|
|
|
|
// Update the input's state as it can no longer be swept.
|
|
input.state = Excluded
|
|
|
|
// Remove all unconfirmed transactions from the wallet which
|
|
// spend the passed outpoint of the same exclusive group.
|
|
outpoints := map[wire.OutPoint]struct{}{
|
|
outpoint: {},
|
|
}
|
|
err := s.removeConflictSweepDescendants(outpoints)
|
|
if err != nil {
|
|
log.Warnf("Unable to remove conflicting sweep tx from "+
|
|
"wallet for outpoint %v : %v", outpoint, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// signalResult notifies the listeners of the final result of the input sweep.
|
|
// It also cancels any pending spend notification.
|
|
func (s *UtxoSweeper) signalResult(pi *SweeperInput, result Result) {
|
|
op := pi.OutPoint()
|
|
listeners := pi.listeners
|
|
|
|
if result.Err == nil {
|
|
log.Tracef("Dispatching sweep success for %v to %v listeners",
|
|
op, len(listeners),
|
|
)
|
|
} else {
|
|
log.Tracef("Dispatching sweep error for %v to %v listeners: %v",
|
|
op, len(listeners), result.Err,
|
|
)
|
|
}
|
|
|
|
// Signal all listeners. Channel is buffered. Because we only send once
|
|
// on every channel, it should never block.
|
|
for _, resultChan := range listeners {
|
|
resultChan <- result
|
|
}
|
|
|
|
// Cancel spend notification with chain notifier. This is not necessary
|
|
// in case of a success, except for that a reorg could still happen.
|
|
if pi.ntfnRegCancel != nil {
|
|
log.Debugf("Canceling spend ntfn for %v", op)
|
|
|
|
pi.ntfnRegCancel()
|
|
}
|
|
}
|
|
|
|
// sweep takes a set of preselected inputs, creates a sweep tx and publishes
|
|
// the tx. The output address is only marked as used if the publish succeeds.
|
|
func (s *UtxoSweeper) sweep(set InputSet) error {
|
|
// Generate an output script if there isn't an unused script available.
|
|
if s.currentOutputScript.IsNone() {
|
|
addr, err := s.cfg.GenSweepScript().Unpack()
|
|
if err != nil {
|
|
return fmt.Errorf("gen sweep script: %w", err)
|
|
}
|
|
s.currentOutputScript = fn.Some(addr)
|
|
}
|
|
|
|
sweepAddr, err := s.currentOutputScript.UnwrapOrErr(
|
|
fmt.Errorf("none sweep script"),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create a fee bump request and ask the publisher to broadcast it. The
|
|
// publisher will then take over and start monitoring the tx for
|
|
// potential fee bump.
|
|
req := &BumpRequest{
|
|
Inputs: set.Inputs(),
|
|
Budget: set.Budget(),
|
|
DeadlineHeight: set.DeadlineHeight(),
|
|
DeliveryAddress: sweepAddr,
|
|
MaxFeeRate: s.cfg.MaxFeeRate.FeePerKWeight(),
|
|
StartingFeeRate: set.StartingFeeRate(),
|
|
// TODO(yy): pass the strategy here.
|
|
}
|
|
|
|
// Reschedule the inputs that we just tried to sweep. This is done in
|
|
// case the following publish fails, we'd like to update the inputs'
|
|
// publish attempts and rescue them in the next sweep.
|
|
s.markInputsPendingPublish(set)
|
|
|
|
// Broadcast will return a read-only chan that we will listen to for
|
|
// this publish result and future RBF attempt.
|
|
resp, err := s.cfg.Publisher.Broadcast(req)
|
|
if err != nil {
|
|
outpoints := make([]wire.OutPoint, len(set.Inputs()))
|
|
for i, inp := range set.Inputs() {
|
|
outpoints[i] = inp.OutPoint()
|
|
}
|
|
|
|
log.Errorf("Initial broadcast failed: %v, inputs=\n%v", err,
|
|
inputTypeSummary(set.Inputs()))
|
|
|
|
// TODO(yy): find out which input is causing the failure.
|
|
s.markInputsPublishFailed(outpoints)
|
|
|
|
return err
|
|
}
|
|
|
|
// Successfully sent the broadcast attempt, we now handle the result by
|
|
// subscribing to the result chan and listen for future updates about
|
|
// this tx.
|
|
s.wg.Add(1)
|
|
go s.monitorFeeBumpResult(resp)
|
|
|
|
return nil
|
|
}
|
|
|
|
// markInputsPendingPublish updates the pending inputs with the given tx
|
|
// inputs. It also increments the `publishAttempts`.
|
|
func (s *UtxoSweeper) markInputsPendingPublish(set InputSet) {
|
|
// Reschedule sweep.
|
|
for _, input := range set.Inputs() {
|
|
pi, ok := s.inputs[input.OutPoint()]
|
|
if !ok {
|
|
// It could be that this input is an additional wallet
|
|
// input that was attached. In that case there also
|
|
// isn't a pending input to update.
|
|
log.Tracef("Skipped marking input as pending "+
|
|
"published: %v not found in pending inputs",
|
|
input.OutPoint())
|
|
|
|
continue
|
|
}
|
|
|
|
// If this input has already terminated, there's clearly
|
|
// something wrong as it would have been removed. In this case
|
|
// we log an error and skip marking this input as pending
|
|
// publish.
|
|
if pi.terminated() {
|
|
log.Errorf("Expect input %v to not have terminated "+
|
|
"state, instead it has %v",
|
|
input.OutPoint, pi.state)
|
|
|
|
continue
|
|
}
|
|
|
|
// Update the input's state.
|
|
pi.state = PendingPublish
|
|
|
|
// Record another publish attempt.
|
|
pi.publishAttempts++
|
|
}
|
|
}
|
|
|
|
// markInputsPublished updates the sweeping tx in db and marks the list of
|
|
// inputs as published.
|
|
func (s *UtxoSweeper) markInputsPublished(tr *TxRecord,
|
|
inputs []*wire.TxIn) error {
|
|
|
|
// Mark this tx in db once successfully published.
|
|
//
|
|
// NOTE: this will behave as an overwrite, which is fine as the record
|
|
// is small.
|
|
tr.Published = true
|
|
err := s.cfg.Store.StoreTx(tr)
|
|
if err != nil {
|
|
return fmt.Errorf("store tx: %w", err)
|
|
}
|
|
|
|
// Reschedule sweep.
|
|
for _, input := range inputs {
|
|
pi, ok := s.inputs[input.PreviousOutPoint]
|
|
if !ok {
|
|
// It could be that this input is an additional wallet
|
|
// input that was attached. In that case there also
|
|
// isn't a pending input to update.
|
|
log.Tracef("Skipped marking input as published: %v "+
|
|
"not found in pending inputs",
|
|
input.PreviousOutPoint)
|
|
|
|
continue
|
|
}
|
|
|
|
// Valdiate that the input is in an expected state.
|
|
if pi.state != PendingPublish {
|
|
// We may get a Published if this is a replacement tx.
|
|
log.Debugf("Expect input %v to have %v, instead it "+
|
|
"has %v", input.PreviousOutPoint,
|
|
PendingPublish, pi.state)
|
|
|
|
continue
|
|
}
|
|
|
|
// Update the input's state.
|
|
pi.state = Published
|
|
|
|
// Update the input's latest fee rate.
|
|
pi.lastFeeRate = chainfee.SatPerKWeight(tr.FeeRate)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// markInputsPublishFailed marks the list of inputs as failed to be published.
|
|
func (s *UtxoSweeper) markInputsPublishFailed(outpoints []wire.OutPoint) {
|
|
// Reschedule sweep.
|
|
for _, op := range outpoints {
|
|
pi, ok := s.inputs[op]
|
|
if !ok {
|
|
// It could be that this input is an additional wallet
|
|
// input that was attached. In that case there also
|
|
// isn't a pending input to update.
|
|
log.Tracef("Skipped marking input as publish failed: "+
|
|
"%v not found in pending inputs", op)
|
|
|
|
continue
|
|
}
|
|
|
|
// Valdiate that the input is in an expected state.
|
|
if pi.state != PendingPublish && pi.state != Published {
|
|
log.Debugf("Expect input %v to have %v, instead it "+
|
|
"has %v", op, PendingPublish, pi.state)
|
|
|
|
continue
|
|
}
|
|
|
|
log.Warnf("Failed to publish input %v", op)
|
|
|
|
// Update the input's state.
|
|
pi.state = PublishFailed
|
|
}
|
|
}
|
|
|
|
// monitorSpend registers a spend notification with the chain notifier. It
|
|
// returns a cancel function that can be used to cancel the registration.
|
|
func (s *UtxoSweeper) monitorSpend(outpoint wire.OutPoint,
|
|
script []byte, heightHint uint32) (func(), error) {
|
|
|
|
log.Tracef("Wait for spend of %v at heightHint=%v",
|
|
outpoint, heightHint)
|
|
|
|
spendEvent, err := s.cfg.Notifier.RegisterSpendNtfn(
|
|
&outpoint, script, heightHint,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("register spend ntfn: %w", err)
|
|
}
|
|
|
|
s.wg.Add(1)
|
|
go func() {
|
|
defer s.wg.Done()
|
|
|
|
select {
|
|
case spend, ok := <-spendEvent.Spend:
|
|
if !ok {
|
|
log.Debugf("Spend ntfn for %v canceled",
|
|
outpoint)
|
|
return
|
|
}
|
|
|
|
log.Debugf("Delivering spend ntfn for %v", outpoint)
|
|
|
|
select {
|
|
case s.spendChan <- spend:
|
|
log.Debugf("Delivered spend ntfn for %v",
|
|
outpoint)
|
|
|
|
case <-s.quit:
|
|
}
|
|
case <-s.quit:
|
|
}
|
|
}()
|
|
|
|
return spendEvent.Cancel, nil
|
|
}
|
|
|
|
// PendingInputs returns the set of inputs that the UtxoSweeper is currently
|
|
// attempting to sweep.
|
|
func (s *UtxoSweeper) PendingInputs() (
|
|
map[wire.OutPoint]*PendingInputResponse, error) {
|
|
|
|
respChan := make(chan map[wire.OutPoint]*PendingInputResponse, 1)
|
|
errChan := make(chan error, 1)
|
|
select {
|
|
case s.pendingSweepsReqs <- &pendingSweepsReq{
|
|
respChan: respChan,
|
|
errChan: errChan,
|
|
}:
|
|
case <-s.quit:
|
|
return nil, ErrSweeperShuttingDown
|
|
}
|
|
|
|
select {
|
|
case pendingSweeps := <-respChan:
|
|
return pendingSweeps, nil
|
|
case err := <-errChan:
|
|
return nil, err
|
|
case <-s.quit:
|
|
return nil, ErrSweeperShuttingDown
|
|
}
|
|
}
|
|
|
|
// handlePendingSweepsReq handles a request to retrieve all pending inputs the
|
|
// UtxoSweeper is attempting to sweep.
|
|
func (s *UtxoSweeper) handlePendingSweepsReq(
|
|
req *pendingSweepsReq) map[wire.OutPoint]*PendingInputResponse {
|
|
|
|
resps := make(map[wire.OutPoint]*PendingInputResponse, len(s.inputs))
|
|
for _, inp := range s.inputs {
|
|
// Only the exported fields are set, as we expect the response
|
|
// to only be consumed externally.
|
|
op := inp.OutPoint()
|
|
resps[op] = &PendingInputResponse{
|
|
OutPoint: op,
|
|
WitnessType: inp.WitnessType(),
|
|
Amount: btcutil.Amount(
|
|
inp.SignDesc().Output.Value,
|
|
),
|
|
LastFeeRate: inp.lastFeeRate,
|
|
BroadcastAttempts: inp.publishAttempts,
|
|
Params: inp.params,
|
|
DeadlineHeight: uint32(inp.DeadlineHeight),
|
|
}
|
|
}
|
|
|
|
select {
|
|
case req.respChan <- resps:
|
|
case <-s.quit:
|
|
log.Debug("Skipped sending pending sweep response due to " +
|
|
"UtxoSweeper shutting down")
|
|
}
|
|
|
|
return resps
|
|
}
|
|
|
|
// UpdateParams allows updating the sweep parameters of a pending input in the
|
|
// UtxoSweeper. This function can be used to provide an updated fee preference
|
|
// and force flag that will be used for a new sweep transaction of the input
|
|
// that will act as a replacement transaction (RBF) of the original sweeping
|
|
// transaction, if any. The exclusive group is left unchanged.
|
|
//
|
|
// NOTE: This currently doesn't do any fee rate validation to ensure that a bump
|
|
// is actually successful. The responsibility of doing so should be handled by
|
|
// the caller.
|
|
func (s *UtxoSweeper) UpdateParams(input wire.OutPoint,
|
|
params Params) (chan Result, error) {
|
|
|
|
responseChan := make(chan *updateResp, 1)
|
|
select {
|
|
case s.updateReqs <- &updateReq{
|
|
input: input,
|
|
params: params,
|
|
responseChan: responseChan,
|
|
}:
|
|
case <-s.quit:
|
|
return nil, ErrSweeperShuttingDown
|
|
}
|
|
|
|
select {
|
|
case response := <-responseChan:
|
|
return response.resultChan, response.err
|
|
case <-s.quit:
|
|
return nil, ErrSweeperShuttingDown
|
|
}
|
|
}
|
|
|
|
// handleUpdateReq handles an update request by simply updating the sweep
|
|
// parameters of the pending input. Currently, no validation is done on the new
|
|
// fee preference to ensure it will properly create a replacement transaction.
|
|
//
|
|
// TODO(wilmer):
|
|
// - Validate fee preference to ensure we'll create a valid replacement
|
|
// transaction to allow the new fee rate to propagate throughout the
|
|
// network.
|
|
// - Ensure we don't combine this input with any other unconfirmed inputs that
|
|
// did not exist in the original sweep transaction, resulting in an invalid
|
|
// replacement transaction.
|
|
func (s *UtxoSweeper) handleUpdateReq(req *updateReq) (
|
|
chan Result, error) {
|
|
|
|
// If the UtxoSweeper is already trying to sweep this input, then we can
|
|
// simply just increase its fee rate. This will allow the input to be
|
|
// batched with others which also have a similar fee rate, creating a
|
|
// higher fee rate transaction that replaces the original input's
|
|
// sweeping transaction.
|
|
sweeperInput, ok := s.inputs[req.input]
|
|
if !ok {
|
|
return nil, lnwallet.ErrNotMine
|
|
}
|
|
|
|
// Create the updated parameters struct. Leave the exclusive group
|
|
// unchanged.
|
|
newParams := Params{
|
|
StartingFeeRate: req.params.StartingFeeRate,
|
|
Immediate: req.params.Immediate,
|
|
Budget: req.params.Budget,
|
|
DeadlineHeight: req.params.DeadlineHeight,
|
|
ExclusiveGroup: sweeperInput.params.ExclusiveGroup,
|
|
}
|
|
|
|
log.Debugf("Updating parameters for %v(state=%v) from (%v) to (%v)",
|
|
req.input, sweeperInput.state, sweeperInput.params, newParams)
|
|
|
|
sweeperInput.params = newParams
|
|
|
|
// We need to reset the state so this input will be attempted again by
|
|
// our sweeper.
|
|
//
|
|
// TODO(yy): a dedicated state?
|
|
sweeperInput.state = Init
|
|
|
|
// If the new input specifies a deadline, update the deadline height.
|
|
sweeperInput.DeadlineHeight = req.params.DeadlineHeight.UnwrapOr(
|
|
sweeperInput.DeadlineHeight,
|
|
)
|
|
|
|
resultChan := make(chan Result, 1)
|
|
sweeperInput.listeners = append(sweeperInput.listeners, resultChan)
|
|
|
|
return resultChan, nil
|
|
}
|
|
|
|
// ListSweeps returns a list of the sweeps recorded by the sweep store.
|
|
func (s *UtxoSweeper) ListSweeps() ([]chainhash.Hash, error) {
|
|
return s.cfg.Store.ListSweeps()
|
|
}
|
|
|
|
// mempoolLookup takes an input's outpoint and queries the mempool to see
|
|
// whether it's already been spent in a transaction found in the mempool.
|
|
// Returns the transaction if found.
|
|
func (s *UtxoSweeper) mempoolLookup(op wire.OutPoint) fn.Option[wire.MsgTx] {
|
|
// For neutrino backend, there's no mempool available, so we exit
|
|
// early.
|
|
if s.cfg.Mempool == nil {
|
|
log.Debugf("Skipping mempool lookup for %v, no mempool ", op)
|
|
|
|
return fn.None[wire.MsgTx]()
|
|
}
|
|
|
|
// Query this input in the mempool. If this outpoint is already spent
|
|
// in mempool, we should get a spending event back immediately.
|
|
return s.cfg.Mempool.LookupInputMempoolSpend(op)
|
|
}
|
|
|
|
// handleNewInput processes a new input by registering spend notification and
|
|
// scheduling sweeping for it.
|
|
func (s *UtxoSweeper) handleNewInput(input *sweepInputMessage) error {
|
|
// Create a default deadline height, which will be used when there's no
|
|
// DeadlineHeight specified for a given input.
|
|
defaultDeadline := s.currentHeight + int32(s.cfg.NoDeadlineConfTarget)
|
|
|
|
outpoint := input.input.OutPoint()
|
|
pi, pending := s.inputs[outpoint]
|
|
if pending {
|
|
log.Debugf("Already has pending input %v received", outpoint)
|
|
|
|
s.handleExistingInput(input, pi)
|
|
|
|
return nil
|
|
}
|
|
|
|
// This is a new input, and we want to query the mempool to see if this
|
|
// input has already been spent. If so, we'll start the input with
|
|
// state Published and attach the RBFInfo.
|
|
state, rbfInfo := s.decideStateAndRBFInfo(input.input.OutPoint())
|
|
|
|
// Create a new pendingInput and initialize the listeners slice with
|
|
// the passed in result channel. If this input is offered for sweep
|
|
// again, the result channel will be appended to this slice.
|
|
pi = &SweeperInput{
|
|
state: state,
|
|
listeners: []chan Result{input.resultChan},
|
|
Input: input.input,
|
|
params: input.params,
|
|
rbf: rbfInfo,
|
|
// Set the acutal deadline height.
|
|
DeadlineHeight: input.params.DeadlineHeight.UnwrapOr(
|
|
defaultDeadline,
|
|
),
|
|
}
|
|
|
|
s.inputs[outpoint] = pi
|
|
log.Tracef("input %v, state=%v, added to inputs", outpoint, pi.state)
|
|
|
|
// Start watching for spend of this input, either by us or the remote
|
|
// party.
|
|
cancel, err := s.monitorSpend(
|
|
outpoint, input.input.SignDesc().Output.PkScript,
|
|
input.input.HeightHint(),
|
|
)
|
|
if err != nil {
|
|
err := fmt.Errorf("wait for spend: %w", err)
|
|
s.markInputFailed(pi, err)
|
|
|
|
return err
|
|
}
|
|
|
|
pi.ntfnRegCancel = cancel
|
|
|
|
return nil
|
|
}
|
|
|
|
// decideStateAndRBFInfo queries the mempool to see whether the given input has
|
|
// already been spent. If so, the state Published will be returned, otherwise
|
|
// state Init. When spent, it will query the sweeper store to fetch the fee
|
|
// info of the spending transction, and construct an RBFInfo based on it.
|
|
// Suppose an error occurs, fn.None is returned.
|
|
func (s *UtxoSweeper) decideStateAndRBFInfo(op wire.OutPoint) (
|
|
SweepState, fn.Option[RBFInfo]) {
|
|
|
|
// Check if we can find the spending tx of this input in mempool.
|
|
txOption := s.mempoolLookup(op)
|
|
|
|
// Extract the spending tx from the option.
|
|
var tx *wire.MsgTx
|
|
txOption.WhenSome(func(t wire.MsgTx) {
|
|
tx = &t
|
|
})
|
|
|
|
// Exit early if it's not found.
|
|
//
|
|
// NOTE: this is not accurate for backends that don't support mempool
|
|
// lookup:
|
|
// - for neutrino we don't have a mempool.
|
|
// - for btcd below v0.24.1 we don't have `gettxspendingprevout`.
|
|
if tx == nil {
|
|
return Init, fn.None[RBFInfo]()
|
|
}
|
|
|
|
// Otherwise the input is already spent in the mempool, so eventually
|
|
// we will return Published.
|
|
//
|
|
// We also need to update the RBF info for this input. If the sweeping
|
|
// transaction is broadcast by us, we can find the fee info in the
|
|
// sweeper store.
|
|
txid := tx.TxHash()
|
|
tr, err := s.cfg.Store.GetTx(txid)
|
|
|
|
// If the tx is not found in the store, it means it's not broadcast by
|
|
// us, hence we can't find the fee info. This is fine as, later on when
|
|
// this tx is confirmed, we will remove the input from our inputs.
|
|
if errors.Is(err, ErrTxNotFound) {
|
|
log.Warnf("Spending tx %v not found in sweeper store", txid)
|
|
return Published, fn.None[RBFInfo]()
|
|
}
|
|
|
|
// Exit if we get an db error.
|
|
if err != nil {
|
|
log.Errorf("Unable to get tx %v from sweeper store: %v",
|
|
txid, err)
|
|
|
|
return Published, fn.None[RBFInfo]()
|
|
}
|
|
|
|
// Prepare the fee info and return it.
|
|
rbf := fn.Some(RBFInfo{
|
|
Txid: txid,
|
|
Fee: btcutil.Amount(tr.Fee),
|
|
FeeRate: chainfee.SatPerKWeight(tr.FeeRate),
|
|
})
|
|
|
|
return Published, rbf
|
|
}
|
|
|
|
// handleExistingInput processes an input that is already known to the sweeper.
|
|
// It will overwrite the params of the old input with the new ones.
|
|
func (s *UtxoSweeper) handleExistingInput(input *sweepInputMessage,
|
|
oldInput *SweeperInput) {
|
|
|
|
// Before updating the input details, check if an exclusive group was
|
|
// set. In case the same input is registered again without an exclusive
|
|
// group set, the previous input and its sweep parameters are outdated
|
|
// hence need to be replaced. This scenario currently only happens for
|
|
// anchor outputs. When a channel is force closed, in the worst case 3
|
|
// different sweeps with the same exclusive group are registered with
|
|
// the sweeper to bump the closing transaction (cpfp) when its time
|
|
// critical. Receiving an input which was already registered with the
|
|
// sweeper but now without an exclusive group means non of the previous
|
|
// inputs were used as CPFP, so we need to make sure we update the
|
|
// sweep parameters but also remove all inputs with the same exclusive
|
|
// group because the are outdated too.
|
|
var prevExclGroup *uint64
|
|
if oldInput.params.ExclusiveGroup != nil &&
|
|
input.params.ExclusiveGroup == nil {
|
|
|
|
prevExclGroup = new(uint64)
|
|
*prevExclGroup = *oldInput.params.ExclusiveGroup
|
|
}
|
|
|
|
// Update input details and sweep parameters. The re-offered input
|
|
// details may contain a change to the unconfirmed parent tx info.
|
|
oldInput.params = input.params
|
|
oldInput.Input = input.input
|
|
|
|
// If the new input specifies a deadline, update the deadline height.
|
|
oldInput.DeadlineHeight = input.params.DeadlineHeight.UnwrapOr(
|
|
oldInput.DeadlineHeight,
|
|
)
|
|
|
|
// Add additional result channel to signal spend of this input.
|
|
oldInput.listeners = append(oldInput.listeners, input.resultChan)
|
|
|
|
if prevExclGroup != nil {
|
|
s.removeExclusiveGroup(*prevExclGroup)
|
|
}
|
|
}
|
|
|
|
// handleInputSpent takes a spend event of our input and updates the sweeper's
|
|
// internal state to remove the input.
|
|
func (s *UtxoSweeper) handleInputSpent(spend *chainntnfs.SpendDetail) {
|
|
// Query store to find out if we ever published this tx.
|
|
spendHash := *spend.SpenderTxHash
|
|
isOurTx, err := s.cfg.Store.IsOurTx(spendHash)
|
|
if err != nil {
|
|
log.Errorf("cannot determine if tx %v is ours: %v",
|
|
spendHash, err)
|
|
return
|
|
}
|
|
|
|
// If this isn't our transaction, it means someone else swept outputs
|
|
// that we were attempting to sweep. This can happen for anchor outputs
|
|
// as well as justice transactions. In this case, we'll notify the
|
|
// wallet to remove any spends that descent from this output.
|
|
if !isOurTx {
|
|
// Construct a map of the inputs this transaction spends.
|
|
spendingTx := spend.SpendingTx
|
|
inputsSpent := make(
|
|
map[wire.OutPoint]struct{}, len(spendingTx.TxIn),
|
|
)
|
|
for _, txIn := range spendingTx.TxIn {
|
|
inputsSpent[txIn.PreviousOutPoint] = struct{}{}
|
|
}
|
|
|
|
log.Debugf("Attempting to remove descendant txns invalidated "+
|
|
"by (txid=%v): %v", spendingTx.TxHash(),
|
|
spew.Sdump(spendingTx))
|
|
|
|
err := s.removeConflictSweepDescendants(inputsSpent)
|
|
if err != nil {
|
|
log.Warnf("unable to remove descendant transactions "+
|
|
"due to tx %v: ", spendHash)
|
|
}
|
|
|
|
log.Debugf("Detected third party spend related to in flight "+
|
|
"inputs (is_ours=%v): %v", isOurTx,
|
|
lnutils.SpewLogClosure(spend.SpendingTx))
|
|
}
|
|
|
|
// We now use the spending tx to update the state of the inputs.
|
|
s.markInputsSwept(spend.SpendingTx, isOurTx)
|
|
}
|
|
|
|
// markInputsSwept marks all inputs swept by the spending transaction as swept.
|
|
// It will also notify all the subscribers of this input.
|
|
func (s *UtxoSweeper) markInputsSwept(tx *wire.MsgTx, isOurTx bool) {
|
|
for _, txIn := range tx.TxIn {
|
|
outpoint := txIn.PreviousOutPoint
|
|
|
|
// Check if this input is known to us. It could probably be
|
|
// unknown if we canceled the registration, deleted from inputs
|
|
// map but the ntfn was in-flight already. Or this could be not
|
|
// one of our inputs.
|
|
input, ok := s.inputs[outpoint]
|
|
if !ok {
|
|
// It's very likely that a spending tx contains inputs
|
|
// that we don't know.
|
|
log.Tracef("Skipped marking input as swept: %v not "+
|
|
"found in pending inputs", outpoint)
|
|
|
|
continue
|
|
}
|
|
|
|
// This input may already been marked as swept by a previous
|
|
// spend notification, which is likely to happen as one sweep
|
|
// transaction usually sweeps multiple inputs.
|
|
if input.terminated() {
|
|
log.Debugf("Skipped marking input as swept: %v "+
|
|
"state=%v", outpoint, input.state)
|
|
|
|
continue
|
|
}
|
|
|
|
input.state = Swept
|
|
|
|
// Return either a nil or a remote spend result.
|
|
var err error
|
|
if !isOurTx {
|
|
log.Warnf("Input=%v was spent by remote or third "+
|
|
"party in tx=%v", outpoint, tx.TxHash())
|
|
err = ErrRemoteSpend
|
|
}
|
|
|
|
// Signal result channels.
|
|
s.signalResult(input, Result{
|
|
Tx: tx,
|
|
Err: err,
|
|
})
|
|
|
|
// Remove all other inputs in this exclusive group.
|
|
if input.params.ExclusiveGroup != nil {
|
|
s.removeExclusiveGroup(*input.params.ExclusiveGroup)
|
|
}
|
|
}
|
|
}
|
|
|
|
// markInputFailed marks the given input as failed and won't be retried. It
|
|
// will also notify all the subscribers of this input.
|
|
func (s *UtxoSweeper) markInputFailed(pi *SweeperInput, err error) {
|
|
log.Errorf("Failed to sweep input: %v, error: %v", pi, err)
|
|
|
|
pi.state = Failed
|
|
|
|
// Remove all other inputs in this exclusive group.
|
|
if pi.params.ExclusiveGroup != nil {
|
|
s.removeExclusiveGroup(*pi.params.ExclusiveGroup)
|
|
}
|
|
|
|
s.signalResult(pi, Result{Err: err})
|
|
}
|
|
|
|
// updateSweeperInputs updates the sweeper's internal state and returns a map
|
|
// of inputs to be swept. It will remove the inputs that are in final states,
|
|
// and returns a map of inputs that have either state Init or PublishFailed.
|
|
func (s *UtxoSweeper) updateSweeperInputs() InputsMap {
|
|
// Create a map of inputs to be swept.
|
|
inputs := make(InputsMap)
|
|
|
|
// Iterate the pending inputs and update the sweeper's state.
|
|
//
|
|
// TODO(yy): sweeper is made to communicate via go channels, so no
|
|
// locks are needed to access the map. However, it'd be safer if we
|
|
// turn this inputs map into a SyncMap in case we wanna add concurrent
|
|
// access to the map in the future.
|
|
for op, input := range s.inputs {
|
|
// If the input has reached a final state, that it's either
|
|
// been swept, or failed, or excluded, we will remove it from
|
|
// our sweeper.
|
|
if input.terminated() {
|
|
log.Debugf("Removing input(State=%v) %v from sweeper",
|
|
input.state, op)
|
|
|
|
delete(s.inputs, op)
|
|
|
|
continue
|
|
}
|
|
|
|
// If this input has been included in a sweep tx that's not
|
|
// published yet, we'd skip this input and wait for the sweep
|
|
// tx to be published.
|
|
if input.state == PendingPublish {
|
|
continue
|
|
}
|
|
|
|
// If this input has already been published, we will need to
|
|
// check the RBF condition before attempting another sweeping.
|
|
if input.state == Published {
|
|
continue
|
|
}
|
|
|
|
// If the input has a locktime that's not yet reached, we will
|
|
// skip this input and wait for the locktime to be reached.
|
|
locktime, _ := input.RequiredLockTime()
|
|
if uint32(s.currentHeight) < locktime {
|
|
log.Warnf("Skipping input %v due to locktime=%v not "+
|
|
"reached, current height is %v", op, locktime,
|
|
s.currentHeight)
|
|
|
|
continue
|
|
}
|
|
|
|
// If the input has a CSV that's not yet reached, we will skip
|
|
// this input and wait for the expiry.
|
|
locktime = input.BlocksToMaturity() + input.HeightHint()
|
|
if s.currentHeight < int32(locktime)-1 {
|
|
log.Infof("Skipping input %v due to CSV expiry=%v not "+
|
|
"reached, current height is %v", op, locktime,
|
|
s.currentHeight)
|
|
|
|
continue
|
|
}
|
|
|
|
// If this input is new or has been failed to be published,
|
|
// we'd retry it. The assumption here is that when an error is
|
|
// returned from `PublishTransaction`, it means the tx has
|
|
// failed to meet the policy, hence it's not in the mempool.
|
|
inputs[op] = input
|
|
}
|
|
|
|
return inputs
|
|
}
|
|
|
|
// sweepPendingInputs is called when the ticker fires. It will create clusters
|
|
// and attempt to create and publish the sweeping transactions.
|
|
func (s *UtxoSweeper) sweepPendingInputs(inputs InputsMap) {
|
|
// Cluster all of our inputs based on the specific Aggregator.
|
|
sets := s.cfg.Aggregator.ClusterInputs(inputs)
|
|
|
|
// sweepWithLock is a helper closure that executes the sweep within a
|
|
// coin select lock to prevent the coins being selected for other
|
|
// transactions like funding of a channel.
|
|
sweepWithLock := func(set InputSet) error {
|
|
return s.cfg.Wallet.WithCoinSelectLock(func() error {
|
|
// Try to add inputs from our wallet.
|
|
err := set.AddWalletInputs(s.cfg.Wallet)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create sweeping transaction for each set.
|
|
err = s.sweep(set)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
for _, set := range sets {
|
|
var err error
|
|
if set.NeedWalletInput() {
|
|
// Sweep the set of inputs that need the wallet inputs.
|
|
err = sweepWithLock(set)
|
|
} else {
|
|
// Sweep the set of inputs that don't need the wallet
|
|
// inputs.
|
|
err = s.sweep(set)
|
|
}
|
|
|
|
if err != nil {
|
|
log.Errorf("Failed to sweep %v: %v", set, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// monitorFeeBumpResult subscribes to the passed result chan to listen for
|
|
// future updates about the sweeping tx.
|
|
//
|
|
// NOTE: must run as a goroutine.
|
|
func (s *UtxoSweeper) monitorFeeBumpResult(resultChan <-chan *BumpResult) {
|
|
defer s.wg.Done()
|
|
|
|
for {
|
|
select {
|
|
case r := <-resultChan:
|
|
// Validate the result is valid.
|
|
if err := r.Validate(); err != nil {
|
|
log.Errorf("Received invalid result: %v", err)
|
|
continue
|
|
}
|
|
|
|
// Send the result back to the main event loop.
|
|
select {
|
|
case s.bumpResultChan <- r:
|
|
case <-s.quit:
|
|
log.Debug("Sweeper shutting down, skip " +
|
|
"sending bump result")
|
|
|
|
return
|
|
}
|
|
|
|
// The sweeping tx has been confirmed, we can exit the
|
|
// monitor now.
|
|
//
|
|
// TODO(yy): can instead remove the spend subscription
|
|
// in sweeper and rely solely on this event to mark
|
|
// inputs as Swept?
|
|
if r.Event == TxConfirmed || r.Event == TxFailed {
|
|
log.Debugf("Received %v for sweep tx %v, exit "+
|
|
"fee bump monitor", r.Event,
|
|
r.Tx.TxHash())
|
|
|
|
// Cancel the rebroadcasting of the failed tx.
|
|
s.cfg.Wallet.CancelRebroadcast(r.Tx.TxHash())
|
|
|
|
return
|
|
}
|
|
|
|
case <-s.quit:
|
|
log.Debugf("Sweeper shutting down, exit fee " +
|
|
"bump handler")
|
|
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleBumpEventTxFailed handles the case where the tx has been failed to
|
|
// publish.
|
|
func (s *UtxoSweeper) handleBumpEventTxFailed(r *BumpResult) error {
|
|
tx, err := r.Tx, r.Err
|
|
|
|
log.Errorf("Fee bump attempt failed for tx=%v: %v", tx.TxHash(), err)
|
|
|
|
outpoints := make([]wire.OutPoint, 0, len(tx.TxIn))
|
|
for _, inp := range tx.TxIn {
|
|
outpoints = append(outpoints, inp.PreviousOutPoint)
|
|
}
|
|
|
|
// TODO(yy): should we also remove the failed tx from db?
|
|
s.markInputsPublishFailed(outpoints)
|
|
|
|
return err
|
|
}
|
|
|
|
// handleBumpEventTxReplaced handles the case where the sweeping tx has been
|
|
// replaced by a new one.
|
|
func (s *UtxoSweeper) handleBumpEventTxReplaced(r *BumpResult) error {
|
|
oldTx := r.ReplacedTx
|
|
newTx := r.Tx
|
|
|
|
// Prepare a new record to replace the old one.
|
|
tr := &TxRecord{
|
|
Txid: newTx.TxHash(),
|
|
FeeRate: uint64(r.FeeRate),
|
|
Fee: uint64(r.Fee),
|
|
}
|
|
|
|
// Get the old record for logging purpose.
|
|
oldTxid := oldTx.TxHash()
|
|
record, err := s.cfg.Store.GetTx(oldTxid)
|
|
if err != nil {
|
|
log.Errorf("Fetch tx record for %v: %v", oldTxid, err)
|
|
return err
|
|
}
|
|
|
|
// Cancel the rebroadcasting of the replaced tx.
|
|
s.cfg.Wallet.CancelRebroadcast(oldTxid)
|
|
|
|
log.Infof("RBFed tx=%v(fee=%v sats, feerate=%v sats/kw) with new "+
|
|
"tx=%v(fee=%v, "+"feerate=%v)", record.Txid, record.Fee,
|
|
record.FeeRate, tr.Txid, tr.Fee, tr.FeeRate)
|
|
|
|
// The old sweeping tx has been replaced by a new one, we will update
|
|
// the tx record in the sweeper db.
|
|
//
|
|
// TODO(yy): we may also need to update the inputs in this tx to a new
|
|
// state. Suppose a replacing tx only spends a subset of the inputs
|
|
// here, we'd end up with the rest being marked as `Published` and
|
|
// won't be aggregated in the next sweep. Atm it's fine as we always
|
|
// RBF the same input set.
|
|
if err := s.cfg.Store.DeleteTx(oldTxid); err != nil {
|
|
log.Errorf("Delete tx record for %v: %v", oldTxid, err)
|
|
return err
|
|
}
|
|
|
|
// Mark the inputs as published using the replacing tx.
|
|
return s.markInputsPublished(tr, r.Tx.TxIn)
|
|
}
|
|
|
|
// handleBumpEventTxPublished handles the case where the sweeping tx has been
|
|
// successfully published.
|
|
func (s *UtxoSweeper) handleBumpEventTxPublished(r *BumpResult) error {
|
|
tx := r.Tx
|
|
tr := &TxRecord{
|
|
Txid: tx.TxHash(),
|
|
FeeRate: uint64(r.FeeRate),
|
|
Fee: uint64(r.Fee),
|
|
}
|
|
|
|
// Inputs have been successfully published so we update their
|
|
// states.
|
|
err := s.markInputsPublished(tr, tx.TxIn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Debugf("Published sweep tx %v, num_inputs=%v, height=%v",
|
|
tx.TxHash(), len(tx.TxIn), s.currentHeight)
|
|
|
|
// If there's no error, remove the output script. Otherwise keep it so
|
|
// that it can be reused for the next transaction and causes no address
|
|
// inflation.
|
|
s.currentOutputScript = fn.None[lnwallet.AddrWithKey]()
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleBumpEvent handles the result sent from the bumper based on its event
|
|
// type.
|
|
//
|
|
// NOTE: TxConfirmed event is not handled, since we already subscribe to the
|
|
// input's spending event, we don't need to do anything here.
|
|
func (s *UtxoSweeper) handleBumpEvent(r *BumpResult) error {
|
|
log.Debugf("Received bump event [%v] for tx %v", r.Event, r.Tx.TxHash())
|
|
|
|
switch r.Event {
|
|
// The tx has been published, we update the inputs' state and create a
|
|
// record to be stored in the sweeper db.
|
|
case TxPublished:
|
|
return s.handleBumpEventTxPublished(r)
|
|
|
|
// The tx has failed, we update the inputs' state.
|
|
case TxFailed:
|
|
return s.handleBumpEventTxFailed(r)
|
|
|
|
// The tx has been replaced, we will remove the old tx and replace it
|
|
// with the new one.
|
|
case TxReplaced:
|
|
return s.handleBumpEventTxReplaced(r)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// IsSweeperOutpoint determines whether the outpoint was created by the sweeper.
|
|
//
|
|
// NOTE: It is enough to check the txid because the sweeper will create
|
|
// outpoints which solely belong to the internal LND wallet.
|
|
func (s *UtxoSweeper) IsSweeperOutpoint(op wire.OutPoint) bool {
|
|
found, err := s.cfg.Store.IsOurTx(op.Hash)
|
|
// In case there is an error fetching the transaction details from the
|
|
// sweeper store we assume the outpoint is still used by the sweeper
|
|
// (worst case scenario).
|
|
//
|
|
// TODO(ziggie): Ensure that confirmed outpoints are deleted from the
|
|
// bucket.
|
|
if err != nil && !errors.Is(err, errNoTxHashesBucket) {
|
|
log.Errorf("failed to fetch info for outpoint(%v:%d) "+
|
|
"with: %v, we assume it is still in use by the sweeper",
|
|
op.Hash, op.Index, err)
|
|
|
|
return true
|
|
}
|
|
|
|
return found
|
|
}
|