mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-06-03 03:29:50 +02:00
sweep: introduce BudgetInputSet
to manage budget-based inputs
This commit adds `BudgetInputSet` which implements `InputSet`. It handles the pending inputs based on the supplied budgets and will be used in the following commit.
This commit is contained in:
parent
bd5eec8e1f
commit
e7400f6a94
@ -52,11 +52,22 @@ type Params struct {
|
|||||||
|
|
||||||
// Force indicates whether the input should be swept regardless of
|
// Force indicates whether the input should be swept regardless of
|
||||||
// whether it is economical to do so.
|
// whether it is economical to do so.
|
||||||
|
//
|
||||||
|
// TODO(yy): Remove this param once deadline based sweeping is in place.
|
||||||
Force bool
|
Force bool
|
||||||
|
|
||||||
// ExclusiveGroup is an identifier that, if set, prevents other inputs
|
// ExclusiveGroup is an identifier that, if set, prevents other inputs
|
||||||
// with the same identifier from being batched together.
|
// with the same identifier from being batched together.
|
||||||
ExclusiveGroup *uint64
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParamsUpdate contains a new set of parameters to update a pending sweep with.
|
// ParamsUpdate contains a new set of parameters to update a pending sweep with.
|
||||||
@ -196,6 +207,11 @@ type pendingInput struct {
|
|||||||
rbf fn.Option[RBFInfo]
|
rbf fn.Option[RBFInfo]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String returns a human readable interpretation of the pending input.
|
||||||
|
func (p *pendingInput) String() string {
|
||||||
|
return fmt.Sprintf("%v (%v)", p.Input.OutPoint(), p.Input.WitnessType())
|
||||||
|
}
|
||||||
|
|
||||||
// parameters returns the sweep parameters for this input.
|
// parameters returns the sweep parameters for this input.
|
||||||
//
|
//
|
||||||
// NOTE: Part of the txInput interface.
|
// NOTE: Part of the txInput interface.
|
||||||
|
@ -35,6 +35,14 @@ var (
|
|||||||
// ErrNotEnoughInputs is returned when there are not enough wallet
|
// ErrNotEnoughInputs is returned when there are not enough wallet
|
||||||
// inputs to construct a non-dust change output for an input set.
|
// inputs to construct a non-dust change output for an input set.
|
||||||
ErrNotEnoughInputs = fmt.Errorf("not enough inputs")
|
ErrNotEnoughInputs = fmt.Errorf("not enough inputs")
|
||||||
|
|
||||||
|
// ErrDeadlinesMismatch is returned when the deadlines of the input
|
||||||
|
// sets do not match.
|
||||||
|
ErrDeadlinesMismatch = fmt.Errorf("deadlines mismatch")
|
||||||
|
|
||||||
|
// ErrDustOutput is returned when the output value is below the dust
|
||||||
|
// limit.
|
||||||
|
ErrDustOutput = fmt.Errorf("dust output")
|
||||||
)
|
)
|
||||||
|
|
||||||
// InputSet defines an interface that's responsible for filtering a set of
|
// InputSet defines an interface that's responsible for filtering a set of
|
||||||
@ -542,3 +550,269 @@ func createWalletTxInput(utxo *lnwallet.Utxo) (input.Input, error) {
|
|||||||
&utxo.OutPoint, witnessType, signDesc, heightHint,
|
&utxo.OutPoint, witnessType, signDesc, heightHint,
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BudgetInputSet implements the interface `InputSet`. It takes a list of
|
||||||
|
// pending inputs which share the same deadline height and groups them into a
|
||||||
|
// set conditionally based on their economical values.
|
||||||
|
type BudgetInputSet struct {
|
||||||
|
// inputs is the set of inputs that have been added to the set after
|
||||||
|
// considering their economical contribution.
|
||||||
|
inputs []*pendingInput
|
||||||
|
|
||||||
|
// deadlineHeight is the height which the inputs in this set must be
|
||||||
|
// confirmed by.
|
||||||
|
deadlineHeight fn.Option[int32]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile-time constraint to ensure budgetInputSet implements InputSet.
|
||||||
|
var _ InputSet = (*BudgetInputSet)(nil)
|
||||||
|
|
||||||
|
// validateInputs is used when creating new BudgetInputSet to ensure there are
|
||||||
|
// no duplicate inputs and they all share the same deadline heights, if set.
|
||||||
|
func validateInputs(inputs []pendingInput) error {
|
||||||
|
// Sanity check the input slice to ensure it's non-empty.
|
||||||
|
if len(inputs) == 0 {
|
||||||
|
return fmt.Errorf("inputs slice is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// dedupInputs is a map used to track unique outpoints of the inputs.
|
||||||
|
dedupInputs := make(map[*wire.OutPoint]struct{})
|
||||||
|
|
||||||
|
// deadlineSet stores unique deadline heights.
|
||||||
|
deadlineSet := make(map[fn.Option[int32]]struct{})
|
||||||
|
|
||||||
|
for _, input := range inputs {
|
||||||
|
input.params.DeadlineHeight.WhenSome(func(h int32) {
|
||||||
|
deadlineSet[input.params.DeadlineHeight] = struct{}{}
|
||||||
|
})
|
||||||
|
|
||||||
|
dedupInputs[input.OutPoint()] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the inputs share the same deadline height when there is
|
||||||
|
// one.
|
||||||
|
if len(deadlineSet) > 1 {
|
||||||
|
return fmt.Errorf("inputs have different deadline heights")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide a defensive check to ensure that we don't have any duplicate
|
||||||
|
// inputs within the set.
|
||||||
|
if len(dedupInputs) != len(inputs) {
|
||||||
|
return fmt.Errorf("duplicate inputs")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBudgetInputSet creates a new BudgetInputSet.
|
||||||
|
func NewBudgetInputSet(inputs []pendingInput) (*BudgetInputSet, error) {
|
||||||
|
// Validate the supplied inputs.
|
||||||
|
if err := validateInputs(inputs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(yy): all the inputs share the same deadline height, which means
|
||||||
|
// there exists an opportunity to refactor the deadline height to be
|
||||||
|
// tracked on the set-level, not per input. This would allow us to
|
||||||
|
// avoid the overhead of tracking the same height for each input in the
|
||||||
|
// set.
|
||||||
|
deadlineHeight := inputs[0].params.DeadlineHeight
|
||||||
|
bi := &BudgetInputSet{
|
||||||
|
deadlineHeight: deadlineHeight,
|
||||||
|
inputs: make([]*pendingInput, 0, len(inputs)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, input := range inputs {
|
||||||
|
bi.addInput(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Tracef("Created %v", bi.String())
|
||||||
|
|
||||||
|
return bi, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a human-readable description of the input set.
|
||||||
|
func (b *BudgetInputSet) String() string {
|
||||||
|
deadlineDesc := "none"
|
||||||
|
b.deadlineHeight.WhenSome(func(h int32) {
|
||||||
|
deadlineDesc = fmt.Sprintf("%d", h)
|
||||||
|
})
|
||||||
|
|
||||||
|
inputsDesc := ""
|
||||||
|
for _, input := range b.inputs {
|
||||||
|
inputsDesc += fmt.Sprintf("\n%v", input)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("BudgetInputSet(budget=%v, deadline=%v, "+
|
||||||
|
"inputs=[%v])", b.Budget(), deadlineDesc, inputsDesc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addInput adds an input to the input set.
|
||||||
|
func (b *BudgetInputSet) addInput(input pendingInput) {
|
||||||
|
b.inputs = append(b.inputs, &input)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NeedWalletInput returns true if the input set needs more wallet inputs.
|
||||||
|
//
|
||||||
|
// A set may need wallet inputs when it has a required output or its total
|
||||||
|
// value cannot cover its total budget.
|
||||||
|
func (b *BudgetInputSet) NeedWalletInput() bool {
|
||||||
|
var (
|
||||||
|
// budgetNeeded is the amount that needs to be covered from
|
||||||
|
// other inputs.
|
||||||
|
budgetNeeded btcutil.Amount
|
||||||
|
|
||||||
|
// budgetBorrowable is the amount that can be borrowed from
|
||||||
|
// other inputs.
|
||||||
|
budgetBorrowable btcutil.Amount
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, inp := range b.inputs {
|
||||||
|
// If this input has a required output, we can assume it's a
|
||||||
|
// second-level htlc txns input. Although this input must have
|
||||||
|
// a value that can cover its budget, it cannot be used to pay
|
||||||
|
// fees. Instead, we need to borrow budget from other inputs to
|
||||||
|
// make the sweep happen. Once swept, the input value will be
|
||||||
|
// credited to the wallet.
|
||||||
|
if inp.RequiredTxOut() != nil {
|
||||||
|
budgetNeeded += inp.params.Budget
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the amount left after covering the input's own budget.
|
||||||
|
// This amount can then be lent to the above input.
|
||||||
|
budget := inp.params.Budget
|
||||||
|
output := btcutil.Amount(inp.SignDesc().Output.Value)
|
||||||
|
budgetBorrowable += output - budget
|
||||||
|
|
||||||
|
// If the input's budget is not even covered by itself, we need
|
||||||
|
// to borrow outputs from other inputs.
|
||||||
|
if budgetBorrowable < 0 {
|
||||||
|
log.Debugf("Input %v specified a budget that exceeds "+
|
||||||
|
"its output value: %v > %v", inp, budget,
|
||||||
|
output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Tracef("NeedWalletInput: budgetNeeded=%v, budgetBorrowable=%v",
|
||||||
|
budgetNeeded, budgetBorrowable)
|
||||||
|
|
||||||
|
// If we don't have enough extra budget to borrow, we need wallet
|
||||||
|
// inputs.
|
||||||
|
return budgetBorrowable < budgetNeeded
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyInputs returns a copy of the slice of the inputs in the set.
|
||||||
|
func (b *BudgetInputSet) copyInputs() []*pendingInput {
|
||||||
|
inputs := make([]*pendingInput, len(b.inputs))
|
||||||
|
copy(inputs, b.inputs)
|
||||||
|
return inputs
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddWalletInputs adds wallet inputs to the set until the specified budget is
|
||||||
|
// met. When sweeping inputs with required outputs, although there's budget
|
||||||
|
// specified, it cannot be directly spent from these required outputs. Instead,
|
||||||
|
// we need to borrow budget from other inputs to make the sweep happen.
|
||||||
|
// There are two sources to borrow from: 1) other inputs, 2) wallet utxos. If
|
||||||
|
// we are calling this method, it means other inputs cannot cover the specified
|
||||||
|
// budget, so we need to borrow from wallet utxos.
|
||||||
|
//
|
||||||
|
// Return an error if there are not enough wallet inputs, and the budget set is
|
||||||
|
// set to its initial state by removing any wallet inputs added.
|
||||||
|
//
|
||||||
|
// NOTE: must be called with the wallet lock held via `WithCoinSelectLock`.
|
||||||
|
func (b *BudgetInputSet) AddWalletInputs(wallet Wallet) error {
|
||||||
|
// Retrieve wallet utxos. Only consider confirmed utxos to prevent
|
||||||
|
// problems around RBF rules for unconfirmed inputs. This currently
|
||||||
|
// ignores the configured coin selection strategy.
|
||||||
|
utxos, err := wallet.ListUnspentWitnessFromDefaultAccount(
|
||||||
|
1, math.MaxInt32,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list unspent witness: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the UTXOs by putting smaller values at the start of the slice
|
||||||
|
// to avoid locking large UTXO for sweeping.
|
||||||
|
//
|
||||||
|
// TODO(yy): add more choices to CoinSelectionStrategy and use the
|
||||||
|
// configured value here.
|
||||||
|
sort.Slice(utxos, func(i, j int) bool {
|
||||||
|
return utxos[i].Value < utxos[j].Value
|
||||||
|
})
|
||||||
|
|
||||||
|
// Make a copy of the current inputs. If the wallet doesn't have enough
|
||||||
|
// utxos to cover the budget, we will revert the current set to its
|
||||||
|
// original state by removing the added wallet inputs.
|
||||||
|
originalInputs := b.copyInputs()
|
||||||
|
|
||||||
|
// Add wallet inputs to the set until the specified budget is covered.
|
||||||
|
for _, utxo := range utxos {
|
||||||
|
input, err := createWalletTxInput(utxo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pi := pendingInput{
|
||||||
|
Input: input,
|
||||||
|
params: Params{
|
||||||
|
// Inherit the deadline height from the input
|
||||||
|
// set.
|
||||||
|
DeadlineHeight: b.deadlineHeight,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
b.addInput(pi)
|
||||||
|
|
||||||
|
// Return if we've reached the minimum output amount.
|
||||||
|
if !b.NeedWalletInput() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The wallet doesn't have enough utxos to cover the budget. Revert the
|
||||||
|
// input set to its original state.
|
||||||
|
b.inputs = originalInputs
|
||||||
|
|
||||||
|
return ErrNotEnoughInputs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Budget returns the total budget of the set.
|
||||||
|
//
|
||||||
|
// NOTE: part of the InputSet interface.
|
||||||
|
func (b *BudgetInputSet) Budget() btcutil.Amount {
|
||||||
|
budget := btcutil.Amount(0)
|
||||||
|
for _, input := range b.inputs {
|
||||||
|
budget += input.params.Budget
|
||||||
|
}
|
||||||
|
|
||||||
|
return budget
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeadlineHeight returns the deadline height of the set.
|
||||||
|
//
|
||||||
|
// NOTE: part of the InputSet interface.
|
||||||
|
func (b *BudgetInputSet) DeadlineHeight() fn.Option[int32] {
|
||||||
|
return b.deadlineHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inputs returns the inputs that should be used to create a tx.
|
||||||
|
//
|
||||||
|
// NOTE: part of the InputSet interface.
|
||||||
|
func (b *BudgetInputSet) Inputs() []input.Input {
|
||||||
|
inputs := make([]input.Input, 0, len(b.inputs))
|
||||||
|
for _, inp := range b.inputs {
|
||||||
|
inputs = append(inputs, inp.Input)
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputs
|
||||||
|
}
|
||||||
|
|
||||||
|
// FeeRate returns the fee rate that should be used for the tx.
|
||||||
|
//
|
||||||
|
// NOTE: part of the InputSet interface.
|
||||||
|
//
|
||||||
|
// TODO(yy): will be removed once fee bumper is implemented.
|
||||||
|
func (b *BudgetInputSet) FeeRate() chainfee.SatPerKWeight {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
package sweep
|
package sweep
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcutil"
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/lightningnetwork/lnd/fn"
|
||||||
"github.com/lightningnetwork/lnd/input"
|
"github.com/lightningnetwork/lnd/input"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -237,3 +241,432 @@ func TestTxInputSetRequiredOutput(t *testing.T) {
|
|||||||
}
|
}
|
||||||
require.True(t, set.enoughInput())
|
require.True(t, set.enoughInput())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestNewBudgetInputSet checks `NewBudgetInputSet` correctly validates the
|
||||||
|
// supplied inputs and returns the error.
|
||||||
|
func TestNewBudgetInputSet(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
rt := require.New(t)
|
||||||
|
|
||||||
|
// Pass an empty slice and expect an error.
|
||||||
|
set, err := NewBudgetInputSet([]pendingInput{})
|
||||||
|
rt.ErrorContains(err, "inputs slice is empty")
|
||||||
|
rt.Nil(set)
|
||||||
|
|
||||||
|
// Create two inputs with different deadline heights.
|
||||||
|
inp0 := createP2WKHInput(1000)
|
||||||
|
inp1 := createP2WKHInput(1000)
|
||||||
|
inp2 := createP2WKHInput(1000)
|
||||||
|
input0 := pendingInput{
|
||||||
|
Input: inp0,
|
||||||
|
params: Params{
|
||||||
|
Budget: 100,
|
||||||
|
DeadlineHeight: fn.None[int32](),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
input1 := pendingInput{
|
||||||
|
Input: inp1,
|
||||||
|
params: Params{
|
||||||
|
Budget: 100,
|
||||||
|
DeadlineHeight: fn.Some(int32(1)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
input2 := pendingInput{
|
||||||
|
Input: inp2,
|
||||||
|
params: Params{
|
||||||
|
Budget: 100,
|
||||||
|
DeadlineHeight: fn.Some(int32(2)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass a slice of inputs with different deadline heights.
|
||||||
|
set, err = NewBudgetInputSet([]pendingInput{input1, input2})
|
||||||
|
rt.ErrorContains(err, "inputs have different deadline heights")
|
||||||
|
rt.Nil(set)
|
||||||
|
|
||||||
|
// Pass a slice of inputs that only one input has the deadline height.
|
||||||
|
set, err = NewBudgetInputSet([]pendingInput{input0, input2})
|
||||||
|
rt.NoError(err)
|
||||||
|
rt.NotNil(set)
|
||||||
|
|
||||||
|
// Pass a slice of inputs that are duplicates.
|
||||||
|
set, err = NewBudgetInputSet([]pendingInput{input1, input1})
|
||||||
|
rt.ErrorContains(err, "duplicate inputs")
|
||||||
|
rt.Nil(set)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBudgetInputSetAddInput checks that `addInput` correctly updates the
|
||||||
|
// budget of the input set.
|
||||||
|
func TestBudgetInputSetAddInput(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Create a testing input with a budget of 100 satoshis.
|
||||||
|
input := createP2WKHInput(1000)
|
||||||
|
pi := &pendingInput{
|
||||||
|
Input: input,
|
||||||
|
params: Params{
|
||||||
|
Budget: 100,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize an input set, which adds the above input.
|
||||||
|
set, err := NewBudgetInputSet([]pendingInput{*pi})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Add the input to the set again.
|
||||||
|
set.addInput(*pi)
|
||||||
|
|
||||||
|
// The set should now have two inputs.
|
||||||
|
require.Len(t, set.inputs, 2)
|
||||||
|
require.Equal(t, pi, set.inputs[0])
|
||||||
|
require.Equal(t, pi, set.inputs[1])
|
||||||
|
|
||||||
|
// The set should have a budget of 200 satoshis.
|
||||||
|
require.Equal(t, btcutil.Amount(200), set.Budget())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNeedWalletInput checks that NeedWalletInput correctly determines if a
|
||||||
|
// wallet input is needed.
|
||||||
|
func TestNeedWalletInput(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Create a mock input that doesn't have required outputs.
|
||||||
|
mockInput := &input.MockInput{}
|
||||||
|
mockInput.On("RequiredTxOut").Return(nil)
|
||||||
|
defer mockInput.AssertExpectations(t)
|
||||||
|
|
||||||
|
// Create a mock input that has required outputs.
|
||||||
|
mockInputRequireOutput := &input.MockInput{}
|
||||||
|
mockInputRequireOutput.On("RequiredTxOut").Return(&wire.TxOut{})
|
||||||
|
defer mockInputRequireOutput.AssertExpectations(t)
|
||||||
|
|
||||||
|
// We now create two pending inputs each has a budget of 100 satoshis.
|
||||||
|
const budget = 100
|
||||||
|
|
||||||
|
// Create the pending input that doesn't have a required output.
|
||||||
|
piBudget := &pendingInput{
|
||||||
|
Input: mockInput,
|
||||||
|
params: Params{Budget: budget},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the pending input that has a required output.
|
||||||
|
piRequireOutput := &pendingInput{
|
||||||
|
Input: mockInputRequireOutput,
|
||||||
|
params: Params{Budget: budget},
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
setupInputs func() []*pendingInput
|
||||||
|
need bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
// When there are no pending inputs, we won't need a
|
||||||
|
// wallet input. Technically this should be an invalid
|
||||||
|
// state.
|
||||||
|
name: "no inputs",
|
||||||
|
setupInputs: func() []*pendingInput {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
need: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// When there's no required output, we don't need a
|
||||||
|
// wallet input.
|
||||||
|
name: "no required outputs",
|
||||||
|
setupInputs: func() []*pendingInput {
|
||||||
|
// Create a sign descriptor to be used in the
|
||||||
|
// pending input when calculating budgets can
|
||||||
|
// be borrowed.
|
||||||
|
sd := &input.SignDescriptor{
|
||||||
|
Output: &wire.TxOut{
|
||||||
|
Value: budget,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mockInput.On("SignDesc").Return(sd).Once()
|
||||||
|
|
||||||
|
return []*pendingInput{piBudget}
|
||||||
|
},
|
||||||
|
need: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// When the output value cannot cover the budget, we
|
||||||
|
// need a wallet input.
|
||||||
|
name: "output value cannot cover budget",
|
||||||
|
setupInputs: func() []*pendingInput {
|
||||||
|
// Create a sign descriptor to be used in the
|
||||||
|
// pending input when calculating budgets can
|
||||||
|
// be borrowed.
|
||||||
|
sd := &input.SignDescriptor{
|
||||||
|
Output: &wire.TxOut{
|
||||||
|
Value: budget - 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mockInput.On("SignDesc").Return(sd).Once()
|
||||||
|
|
||||||
|
// These two methods are only invoked when the
|
||||||
|
// unit test is running with a logger.
|
||||||
|
mockInput.On("OutPoint").Return(
|
||||||
|
&wire.OutPoint{Hash: chainhash.Hash{1}},
|
||||||
|
).Maybe()
|
||||||
|
mockInput.On("WitnessType").Return(
|
||||||
|
input.CommitmentAnchor,
|
||||||
|
).Maybe()
|
||||||
|
|
||||||
|
return []*pendingInput{piBudget}
|
||||||
|
},
|
||||||
|
need: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// When there's only inputs that require outputs, we
|
||||||
|
// need wallet inputs.
|
||||||
|
name: "only required outputs",
|
||||||
|
setupInputs: func() []*pendingInput {
|
||||||
|
return []*pendingInput{piRequireOutput}
|
||||||
|
},
|
||||||
|
need: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// When there's a mix of inputs, but the borrowable
|
||||||
|
// budget cannot cover the required, we need a wallet
|
||||||
|
// input.
|
||||||
|
name: "not enough budget to be borrowed",
|
||||||
|
setupInputs: func() []*pendingInput {
|
||||||
|
// Create a sign descriptor to be used in the
|
||||||
|
// pending input when calculating budgets can
|
||||||
|
// be borrowed.
|
||||||
|
//
|
||||||
|
// NOTE: the value is exactly the same as the
|
||||||
|
// budget so we can't borrow any more.
|
||||||
|
sd := &input.SignDescriptor{
|
||||||
|
Output: &wire.TxOut{
|
||||||
|
Value: budget,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mockInput.On("SignDesc").Return(sd).Once()
|
||||||
|
|
||||||
|
return []*pendingInput{
|
||||||
|
piBudget, piRequireOutput,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
need: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// When there's a mix of inputs, and the budget can be
|
||||||
|
// borrowed covers the required, we don't need wallet
|
||||||
|
// inputs.
|
||||||
|
name: "enough budget to be borrowed",
|
||||||
|
setupInputs: func() []*pendingInput {
|
||||||
|
// Create a sign descriptor to be used in the
|
||||||
|
// pending input when calculating budgets can
|
||||||
|
// be borrowed.
|
||||||
|
//
|
||||||
|
// NOTE: the value is exactly the same as the
|
||||||
|
// budget so we can't borrow any more.
|
||||||
|
sd := &input.SignDescriptor{
|
||||||
|
Output: &wire.TxOut{
|
||||||
|
Value: budget * 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mockInput.On("SignDesc").Return(sd).Once()
|
||||||
|
piBudget.Input = mockInput
|
||||||
|
|
||||||
|
return []*pendingInput{
|
||||||
|
piBudget, piRequireOutput,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
need: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
// Setup testing inputs.
|
||||||
|
inputs := tc.setupInputs()
|
||||||
|
|
||||||
|
// Initialize an input set, which adds the testing
|
||||||
|
// inputs.
|
||||||
|
set := &BudgetInputSet{inputs: inputs}
|
||||||
|
|
||||||
|
result := set.NeedWalletInput()
|
||||||
|
require.Equal(t, tc.need, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAddWalletInputReturnErr tests the three possible errors returned from
|
||||||
|
// AddWalletInputs:
|
||||||
|
// - error from ListUnspentWitnessFromDefaultAccount.
|
||||||
|
// - error from createWalletTxInput.
|
||||||
|
// - error when wallet doesn't have utxos.
|
||||||
|
func TestAddWalletInputReturnErr(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
wallet := &MockWallet{}
|
||||||
|
defer wallet.AssertExpectations(t)
|
||||||
|
|
||||||
|
// Initialize an empty input set.
|
||||||
|
set := &BudgetInputSet{}
|
||||||
|
|
||||||
|
// Specify the min and max confs used in
|
||||||
|
// ListUnspentWitnessFromDefaultAccount.
|
||||||
|
min, max := int32(1), int32(math.MaxInt32)
|
||||||
|
|
||||||
|
// Mock the wallet to return an error.
|
||||||
|
dummyErr := errors.New("dummy error")
|
||||||
|
wallet.On("ListUnspentWitnessFromDefaultAccount",
|
||||||
|
min, max).Return(nil, dummyErr).Once()
|
||||||
|
|
||||||
|
// Check that the error is returned from
|
||||||
|
// ListUnspentWitnessFromDefaultAccount.
|
||||||
|
err := set.AddWalletInputs(wallet)
|
||||||
|
require.ErrorIs(t, err, dummyErr)
|
||||||
|
|
||||||
|
// Create an utxo with unknown address type to trigger an error.
|
||||||
|
utxo := &lnwallet.Utxo{
|
||||||
|
AddressType: lnwallet.UnknownAddressType,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock the wallet to return the above utxo.
|
||||||
|
wallet.On("ListUnspentWitnessFromDefaultAccount",
|
||||||
|
min, max).Return([]*lnwallet.Utxo{utxo}, nil).Once()
|
||||||
|
|
||||||
|
// Check that the error is returned from createWalletTxInput.
|
||||||
|
err = set.AddWalletInputs(wallet)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// Mock the wallet to return empty utxos.
|
||||||
|
wallet.On("ListUnspentWitnessFromDefaultAccount",
|
||||||
|
min, max).Return([]*lnwallet.Utxo{}, nil).Once()
|
||||||
|
|
||||||
|
// Check that the error is returned from not having wallet inputs.
|
||||||
|
err = set.AddWalletInputs(wallet)
|
||||||
|
require.ErrorIs(t, err, ErrNotEnoughInputs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAddWalletInputNotEnoughInputs checks that when there are not enough
|
||||||
|
// wallet utxos, an error is returned and the budget set is reset to its
|
||||||
|
// initial state.
|
||||||
|
func TestAddWalletInputNotEnoughInputs(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
wallet := &MockWallet{}
|
||||||
|
defer wallet.AssertExpectations(t)
|
||||||
|
|
||||||
|
// Specify the min and max confs used in
|
||||||
|
// ListUnspentWitnessFromDefaultAccount.
|
||||||
|
min, max := int32(1), int32(math.MaxInt32)
|
||||||
|
|
||||||
|
// Assume the desired budget is 10k satoshis.
|
||||||
|
const budget = 10_000
|
||||||
|
|
||||||
|
// Create a mock input that has required outputs.
|
||||||
|
mockInput := &input.MockInput{}
|
||||||
|
mockInput.On("RequiredTxOut").Return(&wire.TxOut{})
|
||||||
|
defer mockInput.AssertExpectations(t)
|
||||||
|
|
||||||
|
// Create a pending input that requires 10k satoshis.
|
||||||
|
pi := &pendingInput{
|
||||||
|
Input: mockInput,
|
||||||
|
params: Params{Budget: budget},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a wallet utxo that cannot cover the budget.
|
||||||
|
utxo := &lnwallet.Utxo{
|
||||||
|
AddressType: lnwallet.WitnessPubKey,
|
||||||
|
Value: budget - 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock the wallet to return the above utxo.
|
||||||
|
wallet.On("ListUnspentWitnessFromDefaultAccount",
|
||||||
|
min, max).Return([]*lnwallet.Utxo{utxo}, nil).Once()
|
||||||
|
|
||||||
|
// Initialize an input set with the pending input.
|
||||||
|
set := BudgetInputSet{inputs: []*pendingInput{pi}}
|
||||||
|
|
||||||
|
// Add wallet inputs to the input set, which should give us an error as
|
||||||
|
// the wallet cannot cover the budget.
|
||||||
|
err := set.AddWalletInputs(wallet)
|
||||||
|
require.ErrorIs(t, err, ErrNotEnoughInputs)
|
||||||
|
|
||||||
|
// Check that the budget set is reverted to its initial state.
|
||||||
|
require.Len(t, set.inputs, 1)
|
||||||
|
require.Equal(t, pi, set.inputs[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAddWalletInputSuccess checks that when there are enough wallet utxos,
|
||||||
|
// they are added to the input set.
|
||||||
|
func TestAddWalletInputSuccess(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
wallet := &MockWallet{}
|
||||||
|
defer wallet.AssertExpectations(t)
|
||||||
|
|
||||||
|
// Specify the min and max confs used in
|
||||||
|
// ListUnspentWitnessFromDefaultAccount.
|
||||||
|
min, max := int32(1), int32(math.MaxInt32)
|
||||||
|
|
||||||
|
// Assume the desired budget is 10k satoshis.
|
||||||
|
const budget = 10_000
|
||||||
|
|
||||||
|
// Create a mock input that has required outputs.
|
||||||
|
mockInput := &input.MockInput{}
|
||||||
|
mockInput.On("RequiredTxOut").Return(&wire.TxOut{})
|
||||||
|
defer mockInput.AssertExpectations(t)
|
||||||
|
|
||||||
|
// Create a pending input that requires 10k satoshis.
|
||||||
|
deadline := int32(1000)
|
||||||
|
pi := &pendingInput{
|
||||||
|
Input: mockInput,
|
||||||
|
params: Params{
|
||||||
|
Budget: budget,
|
||||||
|
DeadlineHeight: fn.Some(deadline),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock methods used in loggings.
|
||||||
|
//
|
||||||
|
// NOTE: these methods are not functional as they are only used for
|
||||||
|
// loggings in debug or trace mode so we use arbitrary values.
|
||||||
|
mockInput.On("OutPoint").Return(&wire.OutPoint{Hash: chainhash.Hash{1}})
|
||||||
|
mockInput.On("WitnessType").Return(input.CommitmentAnchor)
|
||||||
|
|
||||||
|
// Create a wallet utxo that cannot cover the budget.
|
||||||
|
utxo := &lnwallet.Utxo{
|
||||||
|
AddressType: lnwallet.WitnessPubKey,
|
||||||
|
Value: budget - 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock the wallet to return the two utxos which can cover the budget.
|
||||||
|
wallet.On("ListUnspentWitnessFromDefaultAccount",
|
||||||
|
min, max).Return([]*lnwallet.Utxo{utxo, utxo}, nil).Once()
|
||||||
|
|
||||||
|
// Initialize an input set with the pending input.
|
||||||
|
set, err := NewBudgetInputSet([]pendingInput{*pi})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Add wallet inputs to the input set, which should give us an error as
|
||||||
|
// the wallet cannot cover the budget.
|
||||||
|
err = set.AddWalletInputs(wallet)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Check that the budget set is updated.
|
||||||
|
require.Len(t, set.inputs, 3)
|
||||||
|
|
||||||
|
// The first input is the pending input.
|
||||||
|
require.Equal(t, pi, set.inputs[0])
|
||||||
|
|
||||||
|
// The second and third inputs are wallet inputs that have
|
||||||
|
// DeadlineHeight set.
|
||||||
|
input2Deadline := set.inputs[1].params.DeadlineHeight
|
||||||
|
require.Equal(t, deadline, input2Deadline.UnsafeFromSome())
|
||||||
|
input3Deadline := set.inputs[2].params.DeadlineHeight
|
||||||
|
require.Equal(t, deadline, input3Deadline.UnsafeFromSome())
|
||||||
|
|
||||||
|
// Finally, check the interface methods.
|
||||||
|
require.EqualValues(t, budget, set.Budget())
|
||||||
|
require.Equal(t, deadline, set.DeadlineHeight().UnsafeFromSome())
|
||||||
|
// Weak check, a strong check is to open the slice and check each item.
|
||||||
|
require.Len(t, set.inputs, 3)
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user