Merge pull request #3892 from matheusdtech/fix-lingering-contract

contractcourt: fix lingering contract after local breach
This commit is contained in:
Conner Fromknecht 2020-05-12 11:46:26 -07:00 committed by GitHub
commit b6915d9fbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 91 additions and 20 deletions

@ -239,24 +239,40 @@ func (c *commitSweepResolver) Resolve() (ContractResolver, error) {
// possible and publish the sweep tx. When the sweep tx // possible and publish the sweep tx. When the sweep tx
// confirms, it signals us through the result channel with the // confirms, it signals us through the result channel with the
// outcome. Wait for this to happen. // outcome. Wait for this to happen.
recovered := true
select { select {
case sweepResult := <-resultChan: case sweepResult := <-resultChan:
if sweepResult.Err != nil { switch sweepResult.Err {
case sweep.ErrRemoteSpend:
// If the remote party was able to sweep this output
// it's likely what we sent was actually a revoked
// commitment. Report the error and continue to wrap up
// the contract.
c.log.Warnf("local commitment output was swept by "+
"remote party via %v", sweepResult.Tx.TxHash())
recovered = false
case nil:
// No errors, therefore continue processing.
c.log.Infof("local commitment output fully resolved by "+
"sweep tx: %v", sweepResult.Tx.TxHash())
default:
// Unknown errors.
c.log.Errorf("unable to sweep input: %v", c.log.Errorf("unable to sweep input: %v",
sweepResult.Err) sweepResult.Err)
return nil, sweepResult.Err return nil, sweepResult.Err
} }
c.log.Infof("commit tx fully resolved by sweep tx: %v",
sweepResult.Tx.TxHash())
case <-c.quit: case <-c.quit:
return nil, errResolverShuttingDown return nil, errResolverShuttingDown
} }
// Funds have been swept and balance is no longer in limbo. // Funds have been swept and balance is no longer in limbo.
c.reportLock.Lock() c.reportLock.Lock()
c.currentReport.RecoveredBalance = c.currentReport.LimboBalance if recovered {
// We only record the balance as recovered if it actually came
// back to us.
c.currentReport.RecoveredBalance = c.currentReport.LimboBalance
}
c.currentReport.LimboBalance = 0 c.currentReport.LimboBalance = 0
c.reportLock.Unlock() c.reportLock.Unlock()

@ -1,7 +1,6 @@
package contractcourt package contractcourt
import ( import (
"reflect"
"testing" "testing"
"time" "time"
@ -96,6 +95,7 @@ func (i *commitSweepResolverTestContext) waitForResult() {
type mockSweeper struct { type mockSweeper struct {
sweptInputs chan input.Input sweptInputs chan input.Input
updatedInputs chan wire.OutPoint updatedInputs chan wire.OutPoint
sweepErr error
} }
func newMockSweeper() *mockSweeper { func newMockSweeper() *mockSweeper {
@ -112,7 +112,8 @@ func (s *mockSweeper) SweepInput(input input.Input, params sweep.Params) (
result := make(chan sweep.Result, 1) result := make(chan sweep.Result, 1)
result <- sweep.Result{ result <- sweep.Result{
Tx: &wire.MsgTx{}, Tx: &wire.MsgTx{},
Err: s.sweepErr,
} }
return result, nil return result, nil
} }
@ -167,12 +168,13 @@ func TestCommitSweepResolverNoDelay(t *testing.T) {
ctx.waitForResult() ctx.waitForResult()
} }
// TestCommitSweepResolverDelay tests resolution of a direct commitment output // testCommitSweepResolverDelay tests resolution of a direct commitment output
// that is encumbered by a time lock. // that is encumbered by a time lock. sweepErr indicates whether the local node
func TestCommitSweepResolverDelay(t *testing.T) { // fails to sweep the output.
t.Parallel() func testCommitSweepResolverDelay(t *testing.T, sweepErr error) {
defer timeout(t)() defer timeout(t)()
const sweepProcessInterval = 100 * time.Millisecond
amt := int64(100) amt := int64(100)
outpoint := wire.OutPoint{ outpoint := wire.OutPoint{
Index: 5, Index: 5,
@ -190,14 +192,20 @@ func TestCommitSweepResolverDelay(t *testing.T) {
ctx := newCommitSweepResolverTestContext(t, &res) ctx := newCommitSweepResolverTestContext(t, &res)
// Setup whether we expect the sweeper to receive a sweep error in this
// test case.
ctx.sweeper.sweepErr = sweepErr
report := ctx.resolver.report() report := ctx.resolver.report()
if !reflect.DeepEqual(report, &ContractReport{ expectedReport := ContractReport{
Outpoint: outpoint, Outpoint: outpoint,
Type: ReportOutputUnencumbered, Type: ReportOutputUnencumbered,
Amount: btcutil.Amount(amt), Amount: btcutil.Amount(amt),
LimboBalance: btcutil.Amount(amt), LimboBalance: btcutil.Amount(amt),
}) { }
t.Fatal("unexpected resolver report") if *report != expectedReport {
t.Fatalf("unexpected resolver report. want=%v got=%v",
expectedReport, report)
} }
ctx.resolve() ctx.resolve()
@ -207,7 +215,7 @@ func TestCommitSweepResolverDelay(t *testing.T) {
} }
// Allow resolver to process confirmation. // Allow resolver to process confirmation.
time.Sleep(100 * time.Millisecond) time.Sleep(sweepProcessInterval)
// Expect report to be updated. // Expect report to be updated.
report = ctx.resolver.report() report = ctx.resolver.report()
@ -222,7 +230,7 @@ func TestCommitSweepResolverDelay(t *testing.T) {
select { select {
case <-ctx.sweeper.sweptInputs: case <-ctx.sweeper.sweptInputs:
t.Fatal("no sweep expected") t.Fatal("no sweep expected")
case <-time.After(100 * time.Millisecond): case <-time.After(sweepProcessInterval):
} }
// A new block arrives. The commit tx confirmed at height -1 and the csv // A new block arrives. The commit tx confirmed at height -1 and the csv
@ -233,14 +241,52 @@ func TestCommitSweepResolverDelay(t *testing.T) {
ctx.waitForResult() ctx.waitForResult()
// If this test case generates a sweep error, we don't expect to be
// able to recover anything. This might happen if the local commitment
// output was swept by a justice transaction by the remote party.
expectedRecoveredBalance := btcutil.Amount(amt)
if sweepErr != nil {
expectedRecoveredBalance = 0
}
report = ctx.resolver.report() report = ctx.resolver.report()
if !reflect.DeepEqual(report, &ContractReport{ expectedReport = ContractReport{
Outpoint: outpoint, Outpoint: outpoint,
Type: ReportOutputUnencumbered, Type: ReportOutputUnencumbered,
Amount: btcutil.Amount(amt), Amount: btcutil.Amount(amt),
RecoveredBalance: btcutil.Amount(amt),
MaturityHeight: testInitialBlockHeight + 2, MaturityHeight: testInitialBlockHeight + 2,
}) { RecoveredBalance: expectedRecoveredBalance,
t.Fatal("unexpected resolver report") }
if *report != expectedReport {
t.Fatalf("unexpected resolver report. want=%v got=%v",
expectedReport, report)
}
}
// TestCommitSweepResolverDelay tests resolution of a direct commitment output
// that is encumbered by a time lock.
func TestCommitSweepResolverDelay(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
sweepErr error
}{{
name: "success",
sweepErr: nil,
}, {
name: "remote spend",
sweepErr: sweep.ErrRemoteSpend,
}}
for _, tc := range testCases {
tc := tc
ok := t.Run(tc.name, func(t *testing.T) {
testCommitSweepResolverDelay(t, tc.sweepErr)
})
if !ok {
break
}
} }
} }

@ -7586,6 +7586,15 @@ func testRevokedCloseRetribution(net *lntest.NetworkHarness, t *harnessTest) {
} }
assertNodeNumChannels(t, carol, 0) assertNodeNumChannels(t, carol, 0)
// Mine enough blocks for Bob's channel arbitrator to wrap up the
// references to the breached channel. The chanarb waits for commitment
// tx's confHeight+CSV-1 blocks and since we've already mined one that
// included the justice tx we only need to mine extra DefaultCSV-2
// blocks to unlock it.
mineBlocks(t, net, lntest.DefaultCSV-2, 0)
assertNumPendingChannels(t, net.Bob, 0, 0)
} }
// testRevokedCloseRetributionZeroValueRemoteOutput tests that Dave is able // testRevokedCloseRetributionZeroValueRemoteOutput tests that Dave is able