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 }