From 0a611aae00514631725346e57ef45fe8656eb34f Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 19 Mar 2024 04:45:05 +0800 Subject: [PATCH] multi: add new config option `BudgetConfig` and `NoDeadlineConfTarget` This commit adds a new group config `BudgetConfig` to allow users specifying their own preference when sweeping outputs. And a new config option `NoDeadlineConfTarget` is added in case the user wants to use a different "lazy" conf target. --- config.go | 5 +- contractcourt/chain_arbitrator.go | 6 +- contractcourt/config.go | 113 ++++++++++++++++++++++++++++++ contractcourt/config_test.go | 83 ++++++++++++++++++++++ lncfg/sweeper.go | 27 ++++++- sample-lnd.conf | 51 ++++++++++++++ server.go | 24 ++++--- sweep/sweeper.go | 8 ++- sweep/sweeper_test.go | 1 + 9 files changed, 298 insertions(+), 20 deletions(-) create mode 100644 contractcourt/config.go create mode 100644 contractcourt/config_test.go diff --git a/config.go b/config.go index e14b9af0a..0e283b91c 100644 --- a/config.go +++ b/config.go @@ -42,7 +42,6 @@ import ( "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing" "github.com/lightningnetwork/lnd/signal" - "github.com/lightningnetwork/lnd/sweep" "github.com/lightningnetwork/lnd/tor" ) @@ -689,9 +688,7 @@ func DefaultConfig() Config { RemoteSigner: &lncfg.RemoteSigner{ Timeout: lncfg.DefaultRemoteSignerRPCTimeout, }, - Sweeper: &lncfg.Sweeper{ - MaxFeeRate: sweep.DefaultMaxFeeRate, - }, + Sweeper: lncfg.DefaultSweeperConfig(), Htlcswitch: &lncfg.Htlcswitch{ MailboxDeliveryTimeout: htlcswitch.DefaultMailboxDeliveryTimeout, }, diff --git a/contractcourt/chain_arbitrator.go b/contractcourt/chain_arbitrator.go index 1fa348b3f..c0550a6ee 100644 --- a/contractcourt/chain_arbitrator.go +++ b/contractcourt/chain_arbitrator.go @@ -199,6 +199,9 @@ type ChainArbitratorConfig struct { // HtlcNotifier is an interface that htlc events are sent to. HtlcNotifier HtlcNotifier + + // Budget is the configured budget for the arbitrator. + Budget BudgetConfig } // ChainArbitrator is a sub-system that oversees the on-chain resolution of all @@ -497,7 +500,8 @@ func (c *ChainArbitrator) Start() error { return nil } - log.Info("ChainArbitrator starting") + log.Infof("ChainArbitrator starting with config: budget=[%v]", + &c.cfg.Budget) // First, we'll fetch all the channels that are still open, in order to // collect them within our set of active contracts. diff --git a/contractcourt/config.go b/contractcourt/config.go new file mode 100644 index 000000000..7f4563fe1 --- /dev/null +++ b/contractcourt/config.go @@ -0,0 +1,113 @@ +package contractcourt + +import ( + "fmt" + + "github.com/btcsuite/btcd/btcutil" +) + +const ( + // MinBudgetValue is the minimal budget that we allow when configuring + // the budget used in sweeping outputs. The actual budget can be lower + // if the user decides to NOT set this value. + // + // NOTE: This value is chosen so the linear fee function can increase + // at least 1 sat/kw per block. + MinBudgetValue btcutil.Amount = 1008 + + // MinBudgetRatio is the minimal ratio that we allow when configuring + // the budget ratio used in sweeping outputs. + MinBudgetRatio = 0.001 + + // DefaultBudgetRatio defines a default budget ratio to be used when + // sweeping inputs. This is a large value, which is fine as the final + // fee rate is capped at the max fee rate configured. + DefaultBudgetRatio = 0.5 +) + +// BudgetConfig is a struct that holds the configuration when offering outputs +// to the sweeper. +// +//nolint:lll +type BudgetConfig struct { + ToLocal btcutil.Amount `long:"tolocal" description:"The amount in satoshis to allocate as the budget to pay fees when sweeping the to_local output. If set, the budget calculated using the ratio (if set) will be capped at this value."` + ToLocalRatio float64 `long:"tolocalratio" description:"The ratio of the value in to_local output to allocate as the budget to pay fees when sweeping it."` + + AnchorCPFP btcutil.Amount `long:"anchorcpfp" description:"The amount in satoshis to allocate as the budget to pay fees when CPFPing a force close tx using the anchor output. If set, the budget calculated using the ratio (if set) will be capped at this value."` + AnchorCPFPRatio float64 `long:"anchorcpfpratio" description:"The ratio of a special value to allocate as the budget to pay fees when CPFPing a force close tx using the anchor output. The special value is the sum of all time-sensitive HTLCs on this commitment subtracted by their budgets."` + + DeadlineHTLC btcutil.Amount `long:"deadlinehtlc" description:"The amount in satoshis to allocate as the budget to pay fees when sweeping a time-sensitive (first-level) HTLC. If set, the budget calculated using the ratio (if set) will be capped at this value."` + DeadlineHTLCRatio float64 `long:"deadlinehtlcratio" description:"The ratio of the value in a time-sensitive (first-level) HTLC to allocate as the budget to pay fees when sweeping it."` + + NoDeadlineHTLC btcutil.Amount `long:"nodeadlinehtlc" description:"The amount in satoshis to allocate as the budget to pay fees when sweeping a non-time-sensitive (second-level) HTLC. If set, the budget calculated using the ratio (if set) will be capped at this value."` + NoDeadlineHTLCRatio float64 `long:"nodeadlinehtlcratio" description:"The ratio of the value in a non-time-sensitive (second-level) HTLC to allocate as the budget to pay fees when sweeping it."` +} + +// Validate checks the budget configuration for any invalid values. +func (b *BudgetConfig) Validate() error { + // Exit early if no budget config is set. + if b == nil { + return fmt.Errorf("no budget config set") + } + + // Sanity check all fields. + if b.ToLocal != 0 && b.ToLocal < MinBudgetValue { + return fmt.Errorf("tolocal must be at least %v", + MinBudgetValue) + } + if b.ToLocalRatio != 0 && b.ToLocalRatio < MinBudgetRatio { + return fmt.Errorf("tolocalratio must be at least %v", + MinBudgetRatio) + } + + if b.AnchorCPFP != 0 && b.AnchorCPFP < MinBudgetValue { + return fmt.Errorf("anchorcpfp must be at least %v", + MinBudgetValue) + } + if b.AnchorCPFPRatio != 0 && b.AnchorCPFPRatio < MinBudgetRatio { + return fmt.Errorf("anchorcpfpratio must be at least %v", + MinBudgetRatio) + } + + if b.DeadlineHTLC != 0 && b.DeadlineHTLC < MinBudgetValue { + return fmt.Errorf("deadlinehtlc must be at least %v", + MinBudgetValue) + } + if b.DeadlineHTLCRatio != 0 && b.DeadlineHTLCRatio < MinBudgetRatio { + return fmt.Errorf("deadlinehtlcratio must be at least %v", + MinBudgetRatio) + } + + if b.NoDeadlineHTLC != 0 && b.NoDeadlineHTLC < MinBudgetValue { + return fmt.Errorf("nodeadlinehtlc must be at least %v", + MinBudgetValue) + } + if b.NoDeadlineHTLCRatio != 0 && + b.NoDeadlineHTLCRatio < MinBudgetRatio { + + return fmt.Errorf("nodeadlinehtlcratio must be at least %v", + MinBudgetRatio) + } + + return nil +} + +// String returns a human-readable description of the budget configuration. +func (b *BudgetConfig) String() string { + return fmt.Sprintf("tolocal=%v tolocalratio=%v anchorcpfp=%v "+ + "anchorcpfpratio=%v deadlinehtlc=%v deadlinehtlcratio=%v "+ + "nodeadlinehtlc=%v nodeadlinehtlcratio=%v", + b.ToLocal, b.ToLocalRatio, b.AnchorCPFP, b.AnchorCPFPRatio, + b.DeadlineHTLC, b.DeadlineHTLCRatio, b.NoDeadlineHTLC, + b.NoDeadlineHTLCRatio) +} + +// DefaultSweeperConfig returns the default configuration for the sweeper. +func DefaultBudgetConfig() *BudgetConfig { + return &BudgetConfig{ + ToLocalRatio: DefaultBudgetRatio, + AnchorCPFPRatio: DefaultBudgetRatio, + DeadlineHTLCRatio: DefaultBudgetRatio, + NoDeadlineHTLCRatio: DefaultBudgetRatio, + } +} diff --git a/contractcourt/config_test.go b/contractcourt/config_test.go new file mode 100644 index 000000000..c22e32df2 --- /dev/null +++ b/contractcourt/config_test.go @@ -0,0 +1,83 @@ +package contractcourt + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// TestBudgetConfigValidate checks that the budget config validation works as +// expected. +func TestBudgetConfigValidate(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + cfg *BudgetConfig + expectedErrStr string + }{ + { + name: "valid config", + cfg: DefaultBudgetConfig(), + }, + { + name: "nil config", + cfg: nil, + expectedErrStr: "no budget config set", + }, + { + name: "invalid tolocal", + cfg: &BudgetConfig{ToLocal: -1}, + expectedErrStr: "tolocal", + }, + { + name: "invalid tolocalratio", + cfg: &BudgetConfig{ToLocalRatio: -1}, + expectedErrStr: "tolocalratio", + }, + { + name: "invalid anchorcpfp", + cfg: &BudgetConfig{AnchorCPFP: -1}, + expectedErrStr: "anchorcpfp", + }, + { + name: "invalid anchorcpfpratio", + cfg: &BudgetConfig{AnchorCPFPRatio: -1}, + expectedErrStr: "anchorcpfpratio", + }, + { + name: "invalid deadlinehtlc", + cfg: &BudgetConfig{DeadlineHTLC: -1}, + expectedErrStr: "deadlinehtlc", + }, + { + name: "invalid deadlinehtlcratio", + cfg: &BudgetConfig{DeadlineHTLCRatio: -1}, + expectedErrStr: "deadlinehtlcratio", + }, + + { + name: "invalid nodeadlinehtlc", + cfg: &BudgetConfig{NoDeadlineHTLC: -1}, + expectedErrStr: "nodeadlinehtlc", + }, + { + name: "invalid nodeadlinehtlcratio", + cfg: &BudgetConfig{NoDeadlineHTLCRatio: -1}, + expectedErrStr: "nodeadlinehtlcratio", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.cfg.Validate() + + if tc.expectedErrStr == "" { + require.NoError(t, err) + return + } + + require.ErrorContains(t, err, tc.expectedErrStr) + }) + } +} diff --git a/lncfg/sweeper.go b/lncfg/sweeper.go index 5bd3b1964..037102c69 100644 --- a/lncfg/sweeper.go +++ b/lncfg/sweeper.go @@ -4,7 +4,9 @@ import ( "fmt" "time" + "github.com/lightningnetwork/lnd/contractcourt" "github.com/lightningnetwork/lnd/lnwallet/chainfee" + "github.com/lightningnetwork/lnd/sweep" ) const ( @@ -20,7 +22,11 @@ const ( //nolint:lll type Sweeper struct { BatchWindowDuration time.Duration `long:"batchwindowduration" description:"Duration of the sweep batch window. The sweep is held back during the batch window to allow more inputs to be added and thereby lower the fee per input." hidden:"true"` - MaxFeeRate chainfee.SatPerVByte `long:"maxfeerate" description:"Maximum fee rate in sat/vb that the sweeper is allowed to use when sweeping funds. Setting this value too low can result in transactions not being confirmed in time, causing HTLCs to expire hence potentially losing funds."` + MaxFeeRate chainfee.SatPerVByte `long:"maxfeerate" description:"Maximum fee rate in sat/vb that the sweeper is allowed to use when sweeping funds, the fee rate derived from budgets are capped at this value. Setting this value too low can result in transactions not being confirmed in time, causing HTLCs to expire hence potentially losing funds."` + + NoDeadlineConfTarget uint32 `long:"nodeadlineconftarget" description:"The conf target to use when sweeping non-time-sensitive outputs. This is useful for sweeping outputs that are not time-sensitive, and can be swept at a lower fee rate."` + + Budget *contractcourt.BudgetConfig `group:"sweeper.budget" namespace:"budget" long:"budget" description:"An optional config group that's used for the automatic sweep fee estimation. The Budget config gives options to limits ones fee exposure when sweeping unilateral close outputs and the fee rate calculated from budgets is capped at sweeper.maxfeerate. Check the budget config options for more details."` } // Validate checks the values configured for the sweeper. @@ -39,5 +45,24 @@ func (s *Sweeper) Validate() error { return fmt.Errorf("maxfeerate must be <= 10000 sat/vb") } + // Make sure the conf target is at least 144 blocks (1 day). + if s.NoDeadlineConfTarget < 144 { + return fmt.Errorf("nodeadlineconftarget must be at least 144") + } + + // Validate the budget configuration. + if err := s.Budget.Validate(); err != nil { + return fmt.Errorf("invalid budget config: %w", err) + } + return nil } + +// DefaultSweeperConfig returns the default configuration for the sweeper. +func DefaultSweeperConfig() *Sweeper { + return &Sweeper{ + MaxFeeRate: sweep.DefaultMaxFeeRate, + NoDeadlineConfTarget: uint32(sweep.DefaultDeadlineDelta), + Budget: contractcourt.DefaultBudgetConfig(), + } +} diff --git a/sample-lnd.conf b/sample-lnd.conf index 9cbc29fa9..58dbbcc46 100644 --- a/sample-lnd.conf +++ b/sample-lnd.conf @@ -1622,6 +1622,57 @@ ; causing HTLCs to expire hence potentially losing funds. ; sweeper.maxfeerate=1000 +; The conf target to use when sweeping non-time-sensitive outputs. This is +; useful for sweeping outputs that are not time-sensitive, and can be swept at +; a lower fee rate. +; sweeper.nodeadlineconftarget=1008 + + +; An optional config group that's used for the automatic sweep fee estimation. +; The Budget config gives options to limits ones fee exposure when sweeping +; unilateral close outputs and the fee rate calculated from budgets is capped +; at sweeper.maxfeerate. Check the budget config options for more details. +; sweeper.budget= + +[sweeper.budget] + +; The amount in satoshis to allocate as the budget to pay fees when sweeping +; the to_local output. If set, the budget calculated using the ratio (if set) +; will be capped at this value. +; sweeper.budget.tolocal= + +; The ratio of the value in to_local output to allocate as the budget to pay +; fees when sweeping it. +; sweeper.budget.tolocalratio=0.5 + +; The amount in satoshis to allocate as the budget to pay fees when CPFPing a +; force close tx using the anchor output. If set, the budget calculated using +; the ratio (if set) will be capped at this value. +; sweeper.budget.anchorcpfp= + +; The ratio of a special value to allocate as the budget to pay fees when +; CPFPing a force close tx using the anchor output. The special value is the +; sum of all time-sensitive HTLCs on this commitment subtracted by their +; budgets. +; sweeper.budget.anchorcpfpratio=0.5 + +; The amount in satoshis to allocate as the budget to pay fees when sweeping a +; time-sensitive (first-level) HTLC. If set, the budget calculated using the +; ratio (if set) will be capped at this value. +; sweeper.budget.deadlinehtlc= + +; The ratio of the value in a time-sensitive (first-level) HTLC to allocate as +; the budget to pay fees when sweeping it. +; sweeper.budget.deadlinehtlcratio=0.5 + +; The amount in satoshis to allocate as the budget to pay fees when sweeping a +; non-time-sensitive (second-level) HTLC. If set, the budget calculated using +; the ratio (if set) will be capped at this value. +; sweeper.budget.nodeadlinehtlc= + +; The ratio of the value in a non-time-sensitive (second-level) HTLC to +; allocate as the budget to pay fees when sweeping it. +; sweeper.budget.nodeadlinehtlcratio=0.5 [htlcswitch] diff --git a/server.go b/server.go index f8da9df76..b85505cd4 100644 --- a/server.go +++ b/server.go @@ -1075,17 +1075,18 @@ func newServer(cfg *Config, listenAddrs []net.Addr, }) s.sweeper = sweep.New(&sweep.UtxoSweeperConfig{ - FeeEstimator: cc.FeeEstimator, - GenSweepScript: newSweepPkScriptGen(cc.Wallet), - Signer: cc.Wallet.Cfg.Signer, - Wallet: newSweeperWallet(cc.Wallet), - Mempool: cc.MempoolNotifier, - Notifier: cc.ChainNotifier, - Store: sweeperStore, - MaxInputsPerTx: sweep.DefaultMaxInputsPerTx, - MaxFeeRate: cfg.Sweeper.MaxFeeRate, - Aggregator: aggregator, - Publisher: s.txPublisher, + FeeEstimator: cc.FeeEstimator, + GenSweepScript: newSweepPkScriptGen(cc.Wallet), + Signer: cc.Wallet.Cfg.Signer, + Wallet: newSweeperWallet(cc.Wallet), + Mempool: cc.MempoolNotifier, + Notifier: cc.ChainNotifier, + Store: sweeperStore, + MaxInputsPerTx: sweep.DefaultMaxInputsPerTx, + MaxFeeRate: cfg.Sweeper.MaxFeeRate, + Aggregator: aggregator, + Publisher: s.txPublisher, + NoDeadlineConfTarget: cfg.Sweeper.NoDeadlineConfTarget, }) s.utxoNursery = contractcourt.NewUtxoNursery(&contractcourt.NurseryConfig{ @@ -1234,6 +1235,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr, SubscribeBreachComplete: s.breachArbitrator.SubscribeBreachComplete, //nolint:lll PutFinalHtlcOutcome: s.chanStateDB.PutOnchainFinalHtlcOutcome, //nolint: lll HtlcNotifier: s.htlcNotifier, + Budget: *s.cfg.Sweeper.Budget, }, dbs.ChanStateDB) // Select the configuration and funding parameters for Bitcoin. diff --git a/sweep/sweeper.go b/sweep/sweeper.go index 6b8abd6fa..a50342191 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -38,8 +38,6 @@ var ( // DefaultDeadlineDelta defines a default deadline delta (1 week) to be // used when sweeping inputs with no deadline pressure. - // - // TODO(yy): make this configurable. DefaultDeadlineDelta = int32(1008) ) @@ -372,6 +370,10 @@ type UtxoSweeperConfig struct { // 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 @@ -1551,7 +1553,7 @@ func (s *UtxoSweeper) updateSweeperInputs() InputsMap { func (s *UtxoSweeper) sweepPendingInputs(inputs InputsMap) { // Create a default deadline height, which will be used when there's no // DeadlineHeight specified for a given input. - defaultDeadline := s.currentHeight + DefaultDeadlineDelta + defaultDeadline := s.currentHeight + int32(s.cfg.NoDeadlineConfTarget) // Cluster all of our inputs based on the specific Aggregator. sets := s.cfg.Aggregator.ClusterInputs(inputs, defaultDeadline) diff --git a/sweep/sweeper_test.go b/sweep/sweeper_test.go index fecdf2105..fde472450 100644 --- a/sweep/sweeper_test.go +++ b/sweep/sweeper_test.go @@ -2677,6 +2677,7 @@ func TestSweepPendingInputs(t *testing.T) { GenSweepScript: func() ([]byte, error) { return testPubKey.SerializeCompressed(), nil }, + NoDeadlineConfTarget: uint32(DefaultDeadlineDelta), }) // Set a current height to test the deadline override.