github.com/decred/dcrlnd@v0.7.6/contractcourt/commit_sweep_resolver.go (about) 1 package contractcourt 2 3 import ( 4 "encoding/binary" 5 "fmt" 6 "io" 7 "math" 8 "sync" 9 10 "github.com/decred/dcrd/chaincfg/chainhash" 11 "github.com/decred/dcrd/dcrutil/v4" 12 "github.com/decred/dcrd/txscript/v4" 13 "github.com/decred/dcrd/wire" 14 "github.com/decred/dcrlnd/chainntnfs" 15 "github.com/decred/dcrlnd/channeldb" 16 "github.com/decred/dcrlnd/input" 17 "github.com/decred/dcrlnd/lnwallet" 18 "github.com/decred/dcrlnd/sweep" 19 ) 20 21 const ( 22 // commitOutputConfTarget is the default confirmation target we'll use 23 // for sweeps of commit outputs that belong to us. 24 commitOutputConfTarget = 6 25 ) 26 27 // commitSweepResolver is a resolver that will attempt to sweep the commitment 28 // output paying to us, in the case that the remote party broadcasts their 29 // version of the commitment transaction. We can sweep this output immediately, 30 // as it doesn't have a time-lock delay. 31 type commitSweepResolver struct { 32 // commitResolution contains all data required to successfully sweep 33 // this HTLC on-chain. 34 commitResolution lnwallet.CommitOutputResolution 35 36 // resolved reflects if the contract has been fully resolved or not. 37 resolved bool 38 39 // broadcastHeight is the height that the original contract was 40 // broadcast to the main-chain at. We'll use this value to bound any 41 // historical queries to the chain for spends/confirmations. 42 broadcastHeight uint32 43 44 // chanPoint is the channel point of the original contract. 45 chanPoint wire.OutPoint 46 47 // channelInitiator denotes whether the party responsible for resolving 48 // the contract initiated the channel. 49 channelInitiator bool 50 51 // leaseExpiry denotes the additional waiting period the contract must 52 // hold until it can be resolved. This waiting period is known as the 53 // expiration of a script-enforced leased channel and only applies to 54 // the channel initiator. 55 // 56 // NOTE: This value should only be set when the contract belongs to a 57 // leased channel. 58 leaseExpiry uint32 59 60 // currentReport stores the current state of the resolver for reporting 61 // over the rpc interface. 62 currentReport ContractReport 63 64 // reportLock prevents concurrent access to the resolver report. 65 reportLock sync.Mutex 66 67 contractResolverKit 68 } 69 70 // newCommitSweepResolver instantiates a new direct commit output resolver. 71 func newCommitSweepResolver(res lnwallet.CommitOutputResolution, 72 broadcastHeight uint32, chanPoint wire.OutPoint, 73 resCfg ResolverConfig) *commitSweepResolver { 74 75 r := &commitSweepResolver{ 76 contractResolverKit: *newContractResolverKit(resCfg), 77 commitResolution: res, 78 broadcastHeight: broadcastHeight, 79 chanPoint: chanPoint, 80 } 81 82 r.initLogger(r) 83 r.initReport() 84 85 return r 86 } 87 88 // ResolverKey returns an identifier which should be globally unique for this 89 // particular resolver within the chain the original contract resides within. 90 func (c *commitSweepResolver) ResolverKey() []byte { 91 key := newResolverID(c.commitResolution.SelfOutPoint) 92 return key[:] 93 } 94 95 // waitForHeight registers for block notifications and waits for the provided 96 // block height to be reached. 97 func waitForHeight(waitHeight uint32, notifier chainntnfs.ChainNotifier, 98 quit <-chan struct{}) error { 99 100 // Register for block epochs. After registration, the current height 101 // will be sent on the channel immediately. 102 blockEpochs, err := notifier.RegisterBlockEpochNtfn(nil) 103 if err != nil { 104 return err 105 } 106 defer blockEpochs.Cancel() 107 108 for { 109 select { 110 case newBlock, ok := <-blockEpochs.Epochs: 111 if !ok { 112 return errResolverShuttingDown 113 } 114 height := newBlock.Height 115 if height >= int32(waitHeight) { 116 return nil 117 } 118 119 case <-quit: 120 return errResolverShuttingDown 121 } 122 } 123 } 124 125 // waitForSpend waits for the given outpoint to be spent, and returns the 126 // details of the spending tx. 127 func waitForSpend(op *wire.OutPoint, pkScript []byte, heightHint uint32, 128 notifier chainntnfs.ChainNotifier, quit <-chan struct{}) ( 129 *chainntnfs.SpendDetail, error) { 130 131 spendNtfn, err := notifier.RegisterSpendNtfn( 132 op, pkScript, heightHint, 133 ) 134 if err != nil { 135 return nil, err 136 } 137 138 select { 139 case spendDetail, ok := <-spendNtfn.Spend: 140 if !ok { 141 return nil, errResolverShuttingDown 142 } 143 144 return spendDetail, nil 145 146 case <-quit: 147 return nil, errResolverShuttingDown 148 } 149 } 150 151 // getCommitTxConfHeight waits for confirmation of the commitment tx and returns 152 // the confirmation height. 153 func (c *commitSweepResolver) getCommitTxConfHeight() (uint32, error) { 154 txID := c.commitResolution.SelfOutPoint.Hash 155 signDesc := c.commitResolution.SelfOutputSignDesc 156 pkScript := signDesc.Output.PkScript 157 const confDepth = 1 158 confChan, err := c.Notifier.RegisterConfirmationsNtfn( 159 &txID, pkScript, confDepth, c.broadcastHeight, 160 ) 161 if err != nil { 162 return 0, err 163 } 164 defer confChan.Cancel() 165 166 select { 167 case txConfirmation, ok := <-confChan.Confirmed: 168 if !ok { 169 return 0, fmt.Errorf("cannot get confirmation "+ 170 "for commit tx %v", txID) 171 } 172 173 return txConfirmation.BlockHeight, nil 174 175 case <-c.quit: 176 return 0, errResolverShuttingDown 177 } 178 } 179 180 // Resolve instructs the contract resolver to resolve the output on-chain. Once 181 // the output has been *fully* resolved, the function should return immediately 182 // with a nil ContractResolver value for the first return value. In the case 183 // that the contract requires further resolution, then another resolve is 184 // returned. 185 // 186 // NOTE: This function MUST be run as a goroutine. 187 func (c *commitSweepResolver) Resolve() (ContractResolver, error) { 188 // If we're already resolved, then we can exit early. 189 if c.resolved { 190 return nil, nil 191 } 192 193 confHeight, err := c.getCommitTxConfHeight() 194 if err != nil { 195 return nil, err 196 } 197 198 // Wait up until the CSV expires, unless we also have a CLTV that 199 // expires after. 200 unlockHeight := confHeight + c.commitResolution.MaturityDelay 201 if c.hasCLTV() { 202 unlockHeight = uint32(math.Max( 203 float64(unlockHeight), float64(c.leaseExpiry), 204 )) 205 } 206 207 c.log.Debugf("commit conf_height=%v, unlock_height=%v", 208 confHeight, unlockHeight) 209 210 // Update report now that we learned the confirmation height. 211 c.reportLock.Lock() 212 c.currentReport.MaturityHeight = unlockHeight 213 c.reportLock.Unlock() 214 215 // If there is a csv/cltv lock, we'll wait for that. 216 if c.commitResolution.MaturityDelay > 0 || c.hasCLTV() { 217 // Determine what height we should wait until for the locks to 218 // expire. 219 var waitHeight uint32 220 switch { 221 // If we have both a csv and cltv lock, we'll need to look at 222 // both and see which expires later. 223 case c.commitResolution.MaturityDelay > 0 && c.hasCLTV(): 224 c.log.Debugf("waiting for CSV and CLTV lock to expire "+ 225 "at height %v", unlockHeight) 226 // If the CSV expires after the CLTV, or there is no 227 // CLTV, then we can broadcast a sweep a block before. 228 // Otherwise, we need to broadcast at our expected 229 // unlock height. 230 waitHeight = uint32(math.Max( 231 float64(unlockHeight-1), float64(c.leaseExpiry), 232 )) 233 234 // If we only have a csv lock, wait for the height before the 235 // lock expires as the spend path should be unlocked by then. 236 case c.commitResolution.MaturityDelay > 0: 237 c.log.Debugf("waiting for CSV lock to expire at "+ 238 "height %v", unlockHeight) 239 waitHeight = unlockHeight - 1 240 } 241 242 err := waitForHeight(waitHeight, c.Notifier, c.quit) 243 if err != nil { 244 return nil, err 245 } 246 } 247 248 // The output is on our local commitment if the script starts with 249 // OP_IF for the revocation clause. On the remote commitment it will 250 // either be a regular P2WKH or a simple sig spend with a CSV delay. 251 isLocalCommitTx := c.commitResolution.SelfOutputSignDesc.WitnessScript[0] == txscript.OP_IF 252 isDelayedOutput := c.commitResolution.MaturityDelay != 0 253 254 c.log.Debugf("isDelayedOutput=%v, isLocalCommitTx=%v", isDelayedOutput, 255 isLocalCommitTx) 256 257 // There're three types of commitments, those that have tweaks 258 // for the remote key (us in this case), those that don't, and a third 259 // where there is no tweak and the output is delayed. On the local 260 // commitment our output will always be delayed. We'll rely on the 261 // presence of the commitment tweak to to discern which type of 262 // commitment this is. 263 var witnessType input.WitnessType 264 switch { 265 266 // Delayed output to us on our local commitment for a channel lease in 267 // which we are the initiator. 268 case isLocalCommitTx && c.hasCLTV(): 269 witnessType = input.LeaseCommitmentTimeLock 270 271 // Delayed output to us on our local commitment. 272 case isLocalCommitTx: 273 witnessType = input.CommitmentTimeLock 274 275 // A confirmed output to us on the remote commitment for a channel lease 276 // in which we are the initiator. 277 case isDelayedOutput && c.hasCLTV(): 278 witnessType = input.LeaseCommitmentToRemoteConfirmed 279 280 // A confirmed output to us on the remote commitment. 281 case isDelayedOutput: 282 witnessType = input.CommitmentToRemoteConfirmed 283 284 // A non-delayed output on the remote commitment where the key is 285 // tweakless. 286 case c.commitResolution.SelfOutputSignDesc.SingleTweak == nil: 287 witnessType = input.CommitSpendNoDelayTweakless 288 289 // A non-delayed output on the remote commitment where the key is 290 // tweaked. 291 default: 292 witnessType = input.CommitmentNoDelay 293 } 294 295 c.log.Infof("Sweeping with witness type: %v", witnessType) 296 297 // We'll craft an input with all the information required for 298 // the sweeper to create a fully valid sweeping transaction to 299 // recover these coins. 300 var inp *input.BaseInput 301 if c.hasCLTV() { 302 inp = input.NewCsvInputWithCltv( 303 &c.commitResolution.SelfOutPoint, witnessType, 304 &c.commitResolution.SelfOutputSignDesc, 305 c.broadcastHeight, c.commitResolution.MaturityDelay, 306 c.leaseExpiry, 307 ) 308 } else { 309 inp = input.NewCsvInput( 310 &c.commitResolution.SelfOutPoint, witnessType, 311 &c.commitResolution.SelfOutputSignDesc, 312 c.broadcastHeight, c.commitResolution.MaturityDelay, 313 ) 314 } 315 316 // With our input constructed, we'll now offer it to the 317 // sweeper. 318 c.log.Infof("sweeping commit output") 319 320 feePref := sweep.FeePreference{ConfTarget: commitOutputConfTarget} 321 resultChan, err := c.Sweeper.SweepInput(inp, sweep.Params{Fee: feePref}) 322 if err != nil { 323 c.log.Errorf("unable to sweep input: %v", err) 324 325 return nil, err 326 } 327 328 var sweepTxID chainhash.Hash 329 330 // Sweeper is going to join this input with other inputs if 331 // possible and publish the sweep tx. When the sweep tx 332 // confirms, it signals us through the result channel with the 333 // outcome. Wait for this to happen. 334 outcome := channeldb.ResolverOutcomeClaimed 335 select { 336 case sweepResult := <-resultChan: 337 switch sweepResult.Err { 338 case sweep.ErrRemoteSpend: 339 // If the remote party was able to sweep this output 340 // it's likely what we sent was actually a revoked 341 // commitment. Report the error and continue to wrap up 342 // the contract. 343 c.log.Warnf("local commitment output was swept by "+ 344 "remote party via %v", sweepResult.Tx.TxHash()) 345 outcome = channeldb.ResolverOutcomeUnclaimed 346 case nil: 347 // No errors, therefore continue processing. 348 c.log.Infof("commit tx fully resolved by sweep tx: %v", 349 sweepResult.Tx.TxHash()) 350 default: 351 // Unknown errors. 352 c.log.Errorf("unable to sweep input: %v", 353 sweepResult.Err) 354 355 return nil, sweepResult.Err 356 } 357 358 sweepTxID = sweepResult.Tx.TxHash() 359 360 case <-c.quit: 361 return nil, errResolverShuttingDown 362 } 363 364 // Funds have been swept and balance is no longer in limbo. 365 c.reportLock.Lock() 366 if outcome == channeldb.ResolverOutcomeClaimed { 367 // We only record the balance as recovered if it actually came 368 // back to us. 369 c.currentReport.RecoveredBalance = c.currentReport.LimboBalance 370 } 371 c.currentReport.LimboBalance = 0 372 c.reportLock.Unlock() 373 report := c.currentReport.resolverReport( 374 &sweepTxID, channeldb.ResolverTypeCommit, outcome, 375 ) 376 c.resolved = true 377 378 // Checkpoint the resolver with a closure that will write the outcome 379 // of the resolver and its sweep transaction to disk. 380 return nil, c.Checkpoint(c, report) 381 } 382 383 // Stop signals the resolver to cancel any current resolution processes, and 384 // suspend. 385 // 386 // NOTE: Part of the ContractResolver interface. 387 func (c *commitSweepResolver) Stop() { 388 close(c.quit) 389 } 390 391 // IsResolved returns true if the stored state in the resolve is fully 392 // resolved. In this case the target output can be forgotten. 393 // 394 // NOTE: Part of the ContractResolver interface. 395 func (c *commitSweepResolver) IsResolved() bool { 396 return c.resolved 397 } 398 399 // SupplementState allows the user of a ContractResolver to supplement it with 400 // state required for the proper resolution of a contract. 401 // 402 // NOTE: Part of the ContractResolver interface. 403 func (c *commitSweepResolver) SupplementState(state *channeldb.OpenChannel) { 404 if state.ChanType.HasLeaseExpiration() { 405 c.leaseExpiry = state.ThawHeight 406 } 407 c.channelInitiator = state.IsInitiator 408 } 409 410 // hasCLTV denotes whether the resolver must wait for an additional CLTV to 411 // expire before resolving the contract. 412 func (c *commitSweepResolver) hasCLTV() bool { 413 return c.channelInitiator && c.leaseExpiry > 0 414 } 415 416 // Encode writes an encoded version of the ContractResolver into the passed 417 // Writer. 418 // 419 // NOTE: Part of the ContractResolver interface. 420 func (c *commitSweepResolver) Encode(w io.Writer) error { 421 if err := encodeCommitResolution(w, &c.commitResolution); err != nil { 422 return err 423 } 424 425 if err := binary.Write(w, endian, c.resolved); err != nil { 426 return err 427 } 428 if err := binary.Write(w, endian, c.broadcastHeight); err != nil { 429 return err 430 } 431 if _, err := w.Write(c.chanPoint.Hash[:]); err != nil { 432 return err 433 } 434 err := binary.Write(w, endian, c.chanPoint.Index) 435 if err != nil { 436 return err 437 } 438 439 // Previously a sweep tx was serialized at this point. Refactoring 440 // removed this, but keep in mind that this data may still be present in 441 // the database. 442 443 return nil 444 } 445 446 // newCommitSweepResolverFromReader attempts to decode an encoded 447 // ContractResolver from the passed Reader instance, returning an active 448 // ContractResolver instance. 449 func newCommitSweepResolverFromReader(r io.Reader, resCfg ResolverConfig) ( 450 *commitSweepResolver, error) { 451 452 c := &commitSweepResolver{ 453 contractResolverKit: *newContractResolverKit(resCfg), 454 } 455 456 if err := decodeCommitResolution(r, &c.commitResolution); err != nil { 457 return nil, err 458 } 459 460 if err := binary.Read(r, endian, &c.resolved); err != nil { 461 return nil, err 462 } 463 if err := binary.Read(r, endian, &c.broadcastHeight); err != nil { 464 return nil, err 465 } 466 _, err := io.ReadFull(r, c.chanPoint.Hash[:]) 467 if err != nil { 468 return nil, err 469 } 470 err = binary.Read(r, endian, &c.chanPoint.Index) 471 if err != nil { 472 return nil, err 473 } 474 475 // Previously a sweep tx was deserialized at this point. Refactoring 476 // removed this, but keep in mind that this data may still be present in 477 // the database. 478 479 c.initLogger(c) 480 c.initReport() 481 482 return c, nil 483 } 484 485 // report returns a report on the resolution state of the contract. 486 func (c *commitSweepResolver) report() *ContractReport { 487 c.reportLock.Lock() 488 defer c.reportLock.Unlock() 489 490 copy := c.currentReport 491 return © 492 } 493 494 // initReport initializes the pending channels report for this resolver. 495 func (c *commitSweepResolver) initReport() { 496 amt := dcrutil.Amount( 497 c.commitResolution.SelfOutputSignDesc.Output.Value, 498 ) 499 500 // Set the initial report. All fields are filled in, except for the 501 // maturity height which remains 0 until Resolve() is executed. 502 // 503 // TODO(joostjager): Resolvers only activate after the commit tx 504 // confirms. With more refactoring in channel arbitrator, it would be 505 // possible to make the confirmation height part of ResolverConfig and 506 // populate MaturityHeight here. 507 c.currentReport = ContractReport{ 508 Outpoint: c.commitResolution.SelfOutPoint, 509 Type: ReportOutputUnencumbered, 510 Amount: amt, 511 LimboBalance: amt, 512 RecoveredBalance: 0, 513 } 514 } 515 516 // A compile time assertion to ensure commitSweepResolver meets the 517 // ContractResolver interface. 518 var _ reportingContractResolver = (*commitSweepResolver)(nil)