github.com/decred/dcrlnd@v0.7.6/sweep/tx_input_set_test.go (about)

     1  package sweep
     2  
     3  import (
     4  	"testing"
     5  
     6  	"github.com/decred/dcrd/dcrutil/v4"
     7  	"github.com/decred/dcrd/wire"
     8  	"github.com/decred/dcrlnd/input"
     9  	"github.com/decred/dcrlnd/lnwallet"
    10  	"github.com/stretchr/testify/require"
    11  )
    12  
    13  // TestTxInputSet tests adding various sized inputs to the set.
    14  func TestTxInputSet(t *testing.T) {
    15  	const (
    16  		feeRate   = 5e4
    17  		maxInputs = 10
    18  	)
    19  	set := newTxInputSet(nil, feeRate, maxInputs)
    20  
    21  	// Create a 10001 atom input. The fee to sweep this input to a P2PKH
    22  	// output is 10850 atoms. That means that this input yields -849 atoms
    23  	// and we expect it not to be added.
    24  	if set.add(createP2PKHInput(10001), constraintsRegular) {
    25  		t.Fatal("expected add of negatively yielding input to fail")
    26  	}
    27  
    28  	// A 15000 atom input should be accepted into the set, because it
    29  	// yields positively.
    30  	if !set.add(createP2PKHInput(15001), constraintsRegular) {
    31  		t.Fatal("expected add of positively yielding input to succeed")
    32  	}
    33  
    34  	fee := set.sizeEstimate(true).fee()
    35  	require.Equal(t, dcrutil.Amount(10850), fee)
    36  
    37  	// The tx output should now be 15000-10850 = 4151 atoms. The dust limit
    38  	// isn't reached yet.
    39  	wantOutputValue := dcrutil.Amount(4151)
    40  	if set.totalOutput() != wantOutputValue {
    41  		t.Fatalf("unexpected output value. want=%d got=%d", wantOutputValue, set.totalOutput())
    42  	}
    43  	if set.enoughInput() {
    44  		t.Fatal("expected dust limit not yet to be reached")
    45  	}
    46  
    47  	// Add a 13703 atoms input. This increases the tx fee to 19150 atoms.
    48  	// The tx output should now be 13703+15001 - 19150 = 9554 atoms.
    49  	if !set.add(createP2PKHInput(13703), constraintsRegular) {
    50  		t.Fatal("expected add of positively yielding input to succeed")
    51  	}
    52  	wantOutputValue = 9554
    53  	if set.totalOutput() != wantOutputValue {
    54  		t.Fatalf("unexpected output value. want=%d got=%d", wantOutputValue, set.totalOutput())
    55  	}
    56  	if !set.enoughInput() {
    57  		t.Fatal("expected dust limit to be reached")
    58  	}
    59  }
    60  
    61  // TestTxInputSetFromWallet tests adding a wallet input to a TxInputSet to
    62  // reach the dust limit.
    63  func TestTxInputSetFromWallet(t *testing.T) {
    64  	const (
    65  		feeRate   = 2e4
    66  		maxInputs = 10
    67  	)
    68  
    69  	wallet := &mockWallet{}
    70  	set := newTxInputSet(wallet, feeRate, maxInputs)
    71  
    72  	// Add a 10000 atoms input to the set. It yields positively, but
    73  	// doesn't reach the output dust limit.
    74  	if !set.add(createP2PKHInput(10000), constraintsRegular) {
    75  		t.Fatal("expected add of positively yielding input to succeed")
    76  	}
    77  	if set.enoughInput() {
    78  		t.Fatal("expected dust limit not yet to be reached")
    79  	}
    80  
    81  	// Expect that adding a negative yield input fails.
    82  	if set.add(createP2PKHInput(50), constraintsRegular) {
    83  		t.Fatal("expected negative yield input add to fail")
    84  	}
    85  
    86  	// Force add the negative yield input. It should succeed.
    87  	if !set.add(createP2PKHInput(50), constraintsForce) {
    88  		t.Fatal("expected forced add to succeed")
    89  	}
    90  
    91  	err := set.tryAddWalletInputsIfNeeded()
    92  	if err != nil {
    93  		t.Fatal(err)
    94  	}
    95  
    96  	if !set.enoughInput() {
    97  		t.Fatal("expected dust limit to be reached")
    98  	}
    99  }
   100  
   101  // createP2PKHInput returns a P2PKH test input with the specified amount.
   102  func createP2PKHInput(amt dcrutil.Amount) input.Input {
   103  	input := createTestInput(int64(amt), input.PublicKeyHash)
   104  	return &input
   105  }
   106  
   107  type mockWallet struct {
   108  	Wallet
   109  }
   110  
   111  func (m *mockWallet) ListUnspentWitnessFromDefaultAccount(minConfs, maxConfs int32) (
   112  	[]*lnwallet.Utxo, error) {
   113  
   114  	return []*lnwallet.Utxo{
   115  		{
   116  			AddressType: lnwallet.PubKeyHash,
   117  			Value:       8000,
   118  		},
   119  	}, nil
   120  }
   121  
   122  type reqInput struct {
   123  	input.Input
   124  
   125  	txOut *wire.TxOut
   126  }
   127  
   128  func (r *reqInput) RequiredTxOut() *wire.TxOut {
   129  	return r.txOut
   130  }
   131  
   132  // TestTxInputSetRequiredOutput tests that the tx input set behaves as expected
   133  // when we add inputs that have required tx outs.
   134  func TestTxInputSetRequiredOutput(t *testing.T) {
   135  	const (
   136  		feeRate   = 50000
   137  		maxInputs = 10
   138  	)
   139  	set := newTxInputSet(nil, feeRate, maxInputs)
   140  
   141  	// Attempt to add an input with a required txout below the dust limit.
   142  	// This should fail since we cannot trim such outputs.
   143  	inp := &reqInput{
   144  		Input: createP2PKHInput(10001),
   145  		txOut: &wire.TxOut{
   146  			Value:    10001,
   147  			PkScript: make([]byte, 25),
   148  		},
   149  	}
   150  	require.False(t, set.add(inp, constraintsRegular),
   151  		"expected adding dust required tx out to fail")
   152  
   153  	// Create a 15001 atoms input that also has a required TxOut of 15001 atoms.
   154  	// The fee to sweep this input to a P2PKH output is 10850 sats.
   155  	inp = &reqInput{
   156  		Input: createP2PKHInput(15001),
   157  		txOut: &wire.TxOut{
   158  			Value:    15001,
   159  			PkScript: make([]byte, 25),
   160  		},
   161  	}
   162  	require.True(t, set.add(inp, constraintsRegular), "failed adding input")
   163  
   164  	// The fee needed to pay for this input and output should be 10850 atoms.
   165  	fee := set.sizeEstimate(false).fee()
   166  	require.Equal(t, dcrutil.Amount(10850), fee)
   167  
   168  	// Since the tx set currently pays no fees, we expect the current
   169  	// change to actually be negative, since this is what it would cost us
   170  	// in fees to add a change output.
   171  	feeWithChange := set.sizeEstimate(true).fee()
   172  	if set.changeOutput != -feeWithChange {
   173  		t.Fatalf("expected negative change of %v, had %v",
   174  			-feeWithChange, set.changeOutput)
   175  	}
   176  
   177  	// This should also be reflected by not having enough input.
   178  	require.False(t, set.enoughInput())
   179  
   180  	// Get a weight estimate without change output, and add an additional
   181  	// input to it.
   182  	dummyInput := createP2PKHInput(1000)
   183  	weight := set.sizeEstimate(false)
   184  	require.NoError(t, weight.add(dummyInput))
   185  
   186  	// Now we add a an input that is large enough to pay the fee for the
   187  	// transaction without a change output, but not large enough to afford
   188  	// adding a change output.
   189  	extraInput1 := weight.fee() + 100
   190  	require.True(t, set.add(createP2PKHInput(extraInput1), constraintsRegular),
   191  		"expected add of positively yielding input to succeed")
   192  
   193  	// The change should be negative, since we would have to add a change
   194  	// output, which we cannot yet afford.
   195  	if set.changeOutput >= 0 {
   196  		t.Fatal("expected change to be negaitve")
   197  	}
   198  
   199  	// Even though we cannot afford a change output, the tx set is valid,
   200  	// since we can pay the fees without the change output.
   201  	require.True(t, set.enoughInput())
   202  
   203  	// Get another weight estimate, this time with a change output, and
   204  	// figure out how much we must add to afford a change output.
   205  	weight = set.sizeEstimate(true)
   206  	require.NoError(t, weight.add(dummyInput))
   207  
   208  	// We add what is left to reach this value.
   209  	extraInput2 := weight.fee() - extraInput1 + 100
   210  
   211  	// Add this input, which should result in the change now being 100 sats.
   212  	require.True(t, set.add(createP2PKHInput(extraInput2), constraintsRegular))
   213  
   214  	// The change should be 100, since this is what is left after paying
   215  	// fees in case of a change output.
   216  	change := set.changeOutput
   217  	if change != 100 {
   218  		t.Fatalf("expected change be 100, was %v", change)
   219  	}
   220  
   221  	// Even though the change output is dust, we have enough for fees, and
   222  	// we have an output, so it should be considered enough to craft a
   223  	// valid sweep transaction.
   224  	require.True(t, set.enoughInput())
   225  
   226  	// Finally we add an input that should push the change output above the
   227  	// dust limit.
   228  	weight = set.sizeEstimate(true)
   229  	require.NoError(t, weight.add(dummyInput))
   230  
   231  	// We expect the change to everything that is left after paying the tx
   232  	// fee.
   233  	extraInput3 := weight.fee() - extraInput1 - extraInput2 + 1000
   234  	require.True(t, set.add(createP2PKHInput(extraInput3), constraintsRegular))
   235  
   236  	change = set.changeOutput
   237  	if change != 1000 {
   238  		t.Fatalf("expected change to be %v, had %v", 1000, change)
   239  
   240  	}
   241  	require.True(t, set.enoughInput())
   242  }