diff --git a/contractcourt/commit_sweep_resolver_test.go b/contractcourt/commit_sweep_resolver_test.go index 279cc6e8..da1fca11 100644 --- a/contractcourt/commit_sweep_resolver_test.go +++ b/contractcourt/commit_sweep_resolver_test.go @@ -96,6 +96,7 @@ func (i *commitSweepResolverTestContext) waitForResult() { type mockSweeper struct { sweptInputs chan input.Input updatedInputs chan wire.OutPoint + sweepErr error } func newMockSweeper() *mockSweeper { @@ -112,7 +113,8 @@ func (s *mockSweeper) SweepInput(input input.Input, params sweep.Params) ( result := make(chan sweep.Result, 1) result <- sweep.Result{ - Tx: &wire.MsgTx{}, + Tx: &wire.MsgTx{}, + Err: s.sweepErr, } return result, nil } @@ -244,3 +246,92 @@ func TestCommitSweepResolverDelay(t *testing.T) { t.Fatal("unexpected resolver report") } } + +// TestCommitSweepResolverLocalBreach tests resolution when the local node +// publishes a breached output (one that is swept by the remote party). +func TestCommitSweepResolverLocalBreach(t *testing.T) { + t.Parallel() + defer timeout(t)() + + amt := int64(100) + outpoint := wire.OutPoint{ + Index: 5, + } + res := lnwallet.CommitOutputResolution{ + SelfOutputSignDesc: input.SignDescriptor{ + Output: &wire.TxOut{ + Value: amt, + }, + WitnessScript: []byte{0}, + }, + MaturityDelay: 3, + SelfOutPoint: outpoint, + } + + ctx := newCommitSweepResolverTestContext(t, &res) + + // Setup the sweeper so that it returns an error of the output being + // already swept. + ctx.sweeper.sweepErr = sweep.ErrRemoteSpend + + report := ctx.resolver.report() + expectedReport := ContractReport{ + Outpoint: outpoint, + Type: ReportOutputUnencumbered, + Amount: btcutil.Amount(amt), + LimboBalance: btcutil.Amount(amt), + } + if *report != expectedReport { + t.Fatalf("unexpected resolver report. want=%v got=%v", + expectedReport, report) + } + + ctx.resolve() + + ctx.notifier.confChan <- &chainntnfs.TxConfirmation{ + BlockHeight: testInitialBlockHeight - 1, + } + + // Allow resolver to process confirmation. + time.Sleep(100 * time.Millisecond) + + // Expect report to be updated. + report = ctx.resolver.report() + if report.MaturityHeight != testInitialBlockHeight+2 { + t.Fatal("report maturity height incorrect") + } + + // Notify initial block height. The csv lock is still in effect, so we + // don't expect any sweep to happen yet. + ctx.notifyEpoch(testInitialBlockHeight) + + select { + case <-ctx.sweeper.sweptInputs: + t.Fatal("no sweep expected") + case <-time.After(100 * time.Millisecond): + } + + // A new block arrives. The commit tx confirmed at height -1 and the csv + // is 3, so a spend will be valid in the first block after height +1. + ctx.notifyEpoch(testInitialBlockHeight + 1) + + <-ctx.sweeper.sweptInputs + + ctx.waitForResult() + + report = ctx.resolver.report() + expectedReport = ContractReport{ + Outpoint: outpoint, + Type: ReportOutputUnencumbered, + Amount: btcutil.Amount(amt), + MaturityHeight: testInitialBlockHeight + 2, + + // RecoveredBalance is zero due to the output having been swept + // by the remote party. + RecoveredBalance: 0, + } + if *report != expectedReport { + t.Fatalf("unexpected resolver report. want=%v got=%v", + expectedReport, report) + } +}