github.com/decred/dcrlnd@v0.7.6/contractcourt/commit_sweep_resolver_test.go (about)

     1  package contractcourt
     2  
     3  import (
     4  	"testing"
     5  	"time"
     6  
     7  	"github.com/decred/dcrd/dcrutil/v4"
     8  	"github.com/decred/dcrd/wire"
     9  	"github.com/decred/dcrlnd/chainntnfs"
    10  	"github.com/decred/dcrlnd/channeldb"
    11  	"github.com/decred/dcrlnd/input"
    12  	"github.com/decred/dcrlnd/kvdb"
    13  	"github.com/decred/dcrlnd/lntest/mock"
    14  	"github.com/decred/dcrlnd/lnwallet"
    15  	"github.com/decred/dcrlnd/lnwallet/chainfee"
    16  	"github.com/decred/dcrlnd/sweep"
    17  )
    18  
    19  type commitSweepResolverTestContext struct {
    20  	resolver           *commitSweepResolver
    21  	notifier           *mock.ChainNotifier
    22  	sweeper            *mockSweeper
    23  	resolverResultChan chan resolveResult
    24  	t                  *testing.T
    25  }
    26  
    27  func newCommitSweepResolverTestContext(t *testing.T,
    28  	resolution *lnwallet.CommitOutputResolution) *commitSweepResolverTestContext {
    29  
    30  	notifier := &mock.ChainNotifier{
    31  		EpochChan: make(chan *chainntnfs.BlockEpoch),
    32  		SpendChan: make(chan *chainntnfs.SpendDetail),
    33  		ConfChan:  make(chan *chainntnfs.TxConfirmation),
    34  	}
    35  
    36  	sweeper := newMockSweeper()
    37  
    38  	checkPointChan := make(chan struct{}, 1)
    39  
    40  	chainCfg := ChannelArbitratorConfig{
    41  		ChainArbitratorConfig: ChainArbitratorConfig{
    42  			Notifier: notifier,
    43  			Sweeper:  sweeper,
    44  		},
    45  		PutResolverReport: func(_ kvdb.RwTx,
    46  			_ *channeldb.ResolverReport) error {
    47  
    48  			return nil
    49  		},
    50  	}
    51  
    52  	cfg := ResolverConfig{
    53  		ChannelArbitratorConfig: chainCfg,
    54  		Checkpoint: func(_ ContractResolver,
    55  			_ ...*channeldb.ResolverReport) error {
    56  
    57  			checkPointChan <- struct{}{}
    58  			return nil
    59  		},
    60  	}
    61  
    62  	resolver := newCommitSweepResolver(
    63  		*resolution, 0, wire.OutPoint{}, cfg,
    64  	)
    65  
    66  	return &commitSweepResolverTestContext{
    67  		resolver: resolver,
    68  		notifier: notifier,
    69  		sweeper:  sweeper,
    70  		t:        t,
    71  	}
    72  }
    73  
    74  func (i *commitSweepResolverTestContext) resolve() {
    75  	// Start resolver.
    76  	i.resolverResultChan = make(chan resolveResult, 1)
    77  	go func() {
    78  		nextResolver, err := i.resolver.Resolve()
    79  		i.resolverResultChan <- resolveResult{
    80  			nextResolver: nextResolver,
    81  			err:          err,
    82  		}
    83  	}()
    84  }
    85  
    86  func (i *commitSweepResolverTestContext) notifyEpoch(height int32) {
    87  	i.notifier.EpochChan <- &chainntnfs.BlockEpoch{
    88  		Height: height,
    89  	}
    90  }
    91  
    92  func (i *commitSweepResolverTestContext) waitForResult() {
    93  	i.t.Helper()
    94  
    95  	result := <-i.resolverResultChan
    96  	if result.err != nil {
    97  		i.t.Fatal(result.err)
    98  	}
    99  
   100  	if result.nextResolver != nil {
   101  		i.t.Fatal("expected no next resolver")
   102  	}
   103  }
   104  
   105  type mockSweeper struct {
   106  	sweptInputs       chan input.Input
   107  	updatedInputs     chan wire.OutPoint
   108  	sweepTx           *wire.MsgTx
   109  	sweepErr          error
   110  	createSweepTxChan chan *wire.MsgTx
   111  
   112  	deadlines []int
   113  }
   114  
   115  func newMockSweeper() *mockSweeper {
   116  	return &mockSweeper{
   117  		sweptInputs:       make(chan input.Input, 3),
   118  		updatedInputs:     make(chan wire.OutPoint),
   119  		sweepTx:           &wire.MsgTx{},
   120  		createSweepTxChan: make(chan *wire.MsgTx),
   121  		deadlines:         []int{},
   122  	}
   123  }
   124  
   125  func (s *mockSweeper) SweepInput(input input.Input, params sweep.Params) (
   126  	chan sweep.Result, error) {
   127  
   128  	s.sweptInputs <- input
   129  
   130  	// Update the deadlines used if it's set.
   131  	if params.Fee.ConfTarget != 0 {
   132  		s.deadlines = append(s.deadlines, int(params.Fee.ConfTarget))
   133  	}
   134  
   135  	result := make(chan sweep.Result, 1)
   136  	result <- sweep.Result{
   137  		Tx:  s.sweepTx,
   138  		Err: s.sweepErr,
   139  	}
   140  	return result, nil
   141  }
   142  
   143  func (s *mockSweeper) CreateSweepTx(inputs []input.Input, feePref sweep.FeePreference,
   144  	currentBlockHeight uint32) (*wire.MsgTx, error) {
   145  
   146  	// We will wait for the test to supply the sweep tx to return.
   147  	sweepTx := <-s.createSweepTxChan
   148  	return sweepTx, nil
   149  }
   150  
   151  func (s *mockSweeper) RelayFeePerKB() chainfee.AtomPerKByte {
   152  	return 1e4
   153  }
   154  
   155  func (s *mockSweeper) UpdateParams(input wire.OutPoint,
   156  	params sweep.ParamsUpdate) (chan sweep.Result, error) {
   157  
   158  	s.updatedInputs <- input
   159  
   160  	result := make(chan sweep.Result, 1)
   161  	result <- sweep.Result{
   162  		Tx: s.sweepTx,
   163  	}
   164  	return result, nil
   165  }
   166  
   167  var _ UtxoSweeper = &mockSweeper{}
   168  
   169  // TestCommitSweepResolverNoDelay tests resolution of a direct commitment output
   170  // unencumbered by a time lock.
   171  func TestCommitSweepResolverNoDelay(t *testing.T) {
   172  	t.Parallel()
   173  	defer timeout(t)()
   174  
   175  	res := lnwallet.CommitOutputResolution{
   176  		SelfOutputSignDesc: input.SignDescriptor{
   177  			Output: &wire.TxOut{
   178  				Value: 100,
   179  			},
   180  			WitnessScript: []byte{0},
   181  		},
   182  	}
   183  
   184  	ctx := newCommitSweepResolverTestContext(t, &res)
   185  
   186  	// Replace our checkpoint with one which will push reports into a
   187  	// channel for us to consume. We replace this function on the resolver
   188  	// itself because it is created by the test context.
   189  	reportChan := make(chan *channeldb.ResolverReport)
   190  	ctx.resolver.Checkpoint = func(_ ContractResolver,
   191  		reports ...*channeldb.ResolverReport) error {
   192  
   193  		// Send all of our reports into the channel.
   194  		for _, report := range reports {
   195  			reportChan <- report
   196  		}
   197  
   198  		return nil
   199  	}
   200  
   201  	ctx.resolve()
   202  
   203  	spendTx := &wire.MsgTx{}
   204  	spendHash := spendTx.TxHash()
   205  	ctx.notifier.ConfChan <- &chainntnfs.TxConfirmation{
   206  		Tx: spendTx,
   207  	}
   208  
   209  	// No csv delay, so the input should be swept immediately.
   210  	<-ctx.sweeper.sweptInputs
   211  
   212  	amt := dcrutil.Amount(res.SelfOutputSignDesc.Output.Value)
   213  	expectedReport := &channeldb.ResolverReport{
   214  		OutPoint:        wire.OutPoint{},
   215  		Amount:          amt,
   216  		ResolverType:    channeldb.ResolverTypeCommit,
   217  		ResolverOutcome: channeldb.ResolverOutcomeClaimed,
   218  		SpendTxID:       &spendHash,
   219  	}
   220  
   221  	assertResolverReport(t, reportChan, expectedReport)
   222  
   223  	ctx.waitForResult()
   224  }
   225  
   226  // testCommitSweepResolverDelay tests resolution of a direct commitment output
   227  // that is encumbered by a time lock. sweepErr indicates whether the local node
   228  // fails to sweep the output.
   229  func testCommitSweepResolverDelay(t *testing.T, sweepErr error) {
   230  	defer timeout(t)()
   231  
   232  	const sweepProcessInterval = 100 * time.Millisecond
   233  	amt := int64(100)
   234  	outpoint := wire.OutPoint{
   235  		Index: 5,
   236  	}
   237  	res := lnwallet.CommitOutputResolution{
   238  		SelfOutputSignDesc: input.SignDescriptor{
   239  			Output: &wire.TxOut{
   240  				Value: amt,
   241  			},
   242  			WitnessScript: []byte{0},
   243  		},
   244  		MaturityDelay: 3,
   245  		SelfOutPoint:  outpoint,
   246  	}
   247  
   248  	ctx := newCommitSweepResolverTestContext(t, &res)
   249  
   250  	// Replace our checkpoint with one which will push reports into a
   251  	// channel for us to consume. We replace this function on the resolver
   252  	// itself because it is created by the test context.
   253  	reportChan := make(chan *channeldb.ResolverReport)
   254  	ctx.resolver.Checkpoint = func(_ ContractResolver,
   255  		reports ...*channeldb.ResolverReport) error {
   256  
   257  		// Send all of our reports into the channel.
   258  		for _, report := range reports {
   259  			reportChan <- report
   260  		}
   261  
   262  		return nil
   263  	}
   264  
   265  	// Setup whether we expect the sweeper to receive a sweep error in this
   266  	// test case.
   267  	ctx.sweeper.sweepErr = sweepErr
   268  
   269  	report := ctx.resolver.report()
   270  	expectedReport := ContractReport{
   271  		Outpoint:     outpoint,
   272  		Type:         ReportOutputUnencumbered,
   273  		Amount:       dcrutil.Amount(amt),
   274  		LimboBalance: dcrutil.Amount(amt),
   275  	}
   276  	if *report != expectedReport {
   277  		t.Fatalf("unexpected resolver report. want=%v got=%v",
   278  			expectedReport, report)
   279  	}
   280  
   281  	ctx.resolve()
   282  
   283  	ctx.notifier.ConfChan <- &chainntnfs.TxConfirmation{
   284  		BlockHeight: testInitialBlockHeight - 1,
   285  	}
   286  
   287  	// Allow resolver to process confirmation.
   288  	time.Sleep(sweepProcessInterval)
   289  
   290  	// Expect report to be updated.
   291  	report = ctx.resolver.report()
   292  	if report.MaturityHeight != testInitialBlockHeight+2 {
   293  		t.Fatal("report maturity height incorrect")
   294  	}
   295  
   296  	// Notify initial block height. The csv lock is still in effect, so we
   297  	// don't expect any sweep to happen yet.
   298  	ctx.notifyEpoch(testInitialBlockHeight)
   299  
   300  	select {
   301  	case <-ctx.sweeper.sweptInputs:
   302  		t.Fatal("no sweep expected")
   303  	case <-time.After(sweepProcessInterval):
   304  	}
   305  
   306  	// A new block arrives. The commit tx confirmed at height -1 and the csv
   307  	// is 3, so a spend will be valid in the first block after height +1.
   308  	ctx.notifyEpoch(testInitialBlockHeight + 1)
   309  
   310  	<-ctx.sweeper.sweptInputs
   311  
   312  	// Set the resolution report outcome based on whether our sweep
   313  	// succeeded.
   314  	outcome := channeldb.ResolverOutcomeClaimed
   315  	if sweepErr != nil {
   316  		outcome = channeldb.ResolverOutcomeUnclaimed
   317  	}
   318  	sweepTx := ctx.sweeper.sweepTx.TxHash()
   319  
   320  	assertResolverReport(t, reportChan, &channeldb.ResolverReport{
   321  		OutPoint:        outpoint,
   322  		ResolverType:    channeldb.ResolverTypeCommit,
   323  		ResolverOutcome: outcome,
   324  		Amount:          dcrutil.Amount(amt),
   325  		SpendTxID:       &sweepTx,
   326  	})
   327  
   328  	ctx.waitForResult()
   329  
   330  	// If this test case generates a sweep error, we don't expect to be
   331  	// able to recover anything. This might happen if the local commitment
   332  	// output was swept by a justice transaction by the remote party.
   333  	expectedRecoveredBalance := dcrutil.Amount(amt)
   334  	if sweepErr != nil {
   335  		expectedRecoveredBalance = 0
   336  	}
   337  
   338  	report = ctx.resolver.report()
   339  	expectedReport = ContractReport{
   340  		Outpoint:         outpoint,
   341  		Type:             ReportOutputUnencumbered,
   342  		Amount:           dcrutil.Amount(amt),
   343  		MaturityHeight:   testInitialBlockHeight + 2,
   344  		RecoveredBalance: expectedRecoveredBalance,
   345  	}
   346  	if *report != expectedReport {
   347  		t.Fatalf("unexpected resolver report. want=%v got=%v",
   348  			expectedReport, report)
   349  	}
   350  
   351  }
   352  
   353  // TestCommitSweepResolverDelay tests resolution of a direct commitment output
   354  // that is encumbered by a time lock.
   355  func TestCommitSweepResolverDelay(t *testing.T) {
   356  	t.Parallel()
   357  
   358  	testCases := []struct {
   359  		name     string
   360  		sweepErr error
   361  	}{{
   362  		name:     "success",
   363  		sweepErr: nil,
   364  	}, {
   365  		name:     "remote spend",
   366  		sweepErr: sweep.ErrRemoteSpend,
   367  	}}
   368  
   369  	for _, tc := range testCases {
   370  		tc := tc
   371  		ok := t.Run(tc.name, func(t *testing.T) {
   372  			testCommitSweepResolverDelay(t, tc.sweepErr)
   373  		})
   374  		if !ok {
   375  			break
   376  		}
   377  	}
   378  }