mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-03-04 09:48:19 +01: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
3 changed files with 723 additions and 0 deletions
|
@ -52,11 +52,22 @@ type Params struct {
|
|||
|
||||
// Force indicates whether the input should be swept regardless of
|
||||
// whether it is economical to do so.
|
||||
//
|
||||
// TODO(yy): Remove this param once deadline based sweeping is in place.
|
||||
Force bool
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// ParamsUpdate contains a new set of parameters to update a pending sweep with.
|
||||
|
@ -196,6 +207,11 @@ type pendingInput struct {
|
|||
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.
|
||||
//
|
||||
// NOTE: Part of the txInput interface.
|
||||
|
|
|
@ -35,6 +35,14 @@ var (
|
|||
// ErrNotEnoughInputs is returned when there are not enough wallet
|
||||
// inputs to construct a non-dust change output for an input set.
|
||||
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
|
||||
|
@ -542,3 +550,269 @@ func createWalletTxInput(utxo *lnwallet.Utxo) (input.Input, error) {
|
|||
&utxo.OutPoint, witnessType, signDesc, heightHint,
|
||||
), 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
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -237,3 +241,432 @@ func TestTxInputSetRequiredOutput(t *testing.T) {
|
|||
}
|
||||
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…
Add table
Reference in a new issue