gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/contractor/watchdog.go (about) 1 package contractor 2 3 import ( 4 "fmt" 5 "sync" 6 7 "gitlab.com/NebulousLabs/errors" 8 9 "gitlab.com/SkynetLabs/skyd/skymodules" 10 "go.sia.tech/siad/crypto" 11 "go.sia.tech/siad/modules" 12 "go.sia.tech/siad/types" 13 ) 14 15 // Key Assumptions: 16 // 17 // Contracts are removed from watchdog state after their storage proof window 18 // has closed. The assumption here is that contracts are generally formed such 19 // that by the time the window has closed, it is both extremely unlikely that 20 // the original file contract transaction or any revisions will be re-orged out 21 // and it is also irrelevant to the renter by that point in time because they 22 // will already have started treating the contract as expired. We also note that 23 // the watchdog does *not* check if storage proofs are re-orged out. If a host 24 // has ever submitted a valid storage proof, then from the renter's point of 25 // view they have fulfilled their obligation for the contract. 26 // 27 // TODOs: 28 // - Perform action when storage proof is found and when missing at the end of 29 // the window. 30 // 31 // - When creating sweep transaction, add parent transactions if the renter's 32 // own dependencies are causing this to be triggered. 33 34 type watchdog struct { 35 // contracts stores all contract metadata used by the watchdog for any 36 // contract it is watching. Any contract that is being watched must have data 37 // in here. 38 contracts map[types.FileContractID]*fileContractStatus 39 40 // archivedContracts are contracts that have expired and are stored for 41 // archival purposes. 42 archivedContracts map[types.FileContractID]skymodules.ContractWatchStatus 43 44 // outputDependencies maps Siacoin outputs to the file contracts that are 45 // dependent on them. When a contract is first submitted to the watchdog to be 46 // monitored, the outputDependencies created for that contract are the 47 // confirmed Siacoin outputs used to create the file contract transaction set. 48 // The inverse mapping can be created on demand for any given file contract 49 // using the corresponding (updated)formationTxnSet. This map is used to check 50 // for double-spends on inputs used to form a contract. 51 outputDependencies map[types.SiacoinOutputID]map[types.FileContractID]struct{} 52 53 // The watchdog uses the renewWindow to compute the first blockheight (start 54 // of storage proof window minus renewWindow) at which the watchdog will 55 // broadcast the most recent revision if it hasn't seen it on chain yet. 56 renewWindow types.BlockHeight 57 blockHeight types.BlockHeight 58 59 staticContractor *Contractor 60 staticDeps modules.Dependencies 61 staticTPool transactionPool 62 63 mu sync.Mutex 64 } 65 66 // fileContractStatus holds all the metadata the watchdog needs to monitor a file 67 // contract. 68 type fileContractStatus struct { 69 // formationSweepHeight is the blockheight by which the watchdog expects to 70 // find the contract on-chain. Up until this height, if contract is not yet 71 // found the watchdog will rebbroadcast the formationTxnSet. After this height 72 // the watchdog will attempt to sweep its inputs and abandon this contract 73 formationSweepHeight types.BlockHeight 74 contractFound bool 75 revisionFound uint64 // store the revision number found 76 storageProofFound types.BlockHeight // store the blockheight at which the proof was found. 77 78 // While watching for contract formation, the watchdog may periodically 79 // rebroadcast the initial file contract transaction and unconfirmed parent 80 // transactions. Any transactions in the original txn set that have been found 81 // on-chain are removed from this set. If a Siacoin output that this file 82 // contract depends on is re-orged out, then the transaction that creates that 83 // output is added to the set. 84 formationTxnSet []types.Transaction 85 86 // parentOutputs stores SiacoinOutputID of outputs which this file contract is 87 // dependent on, i.e. the parent outputs of the formationTxnSet. It is 88 // initialized with the parent outputs from the formationTxnSet but may grow 89 // and shrink as transactions are added or pruned from the formationTxnSet. 90 parentOutputs map[types.SiacoinOutputID]struct{} 91 92 // In case its been too long since the contract was supposed to form and the 93 // initial set has yet to appear on-chain, the watchdog is also prepared to 94 // double spend the inputs used by the contractor to create the contract with 95 // a higher fee-rate if necessary. It does so by extending the sweepTxn. 96 sweepTxn types.Transaction 97 sweepParents []types.Transaction 98 99 // Store the storage proof window start and end heights. 100 windowStart types.BlockHeight 101 windowEnd types.BlockHeight 102 } 103 104 // monitorContractArgs defines the arguments passed to callMonitorContract. 105 type monitorContractArgs struct { 106 // recovered indicates that the contract has been recovered, and that it 107 // doesn't need to be monitored for formation. 108 recovered bool 109 110 fcID types.FileContractID 111 revisionTxn types.Transaction 112 formationTxnSet []types.Transaction 113 sweepTxn types.Transaction 114 sweepParents []types.Transaction 115 blockHeight types.BlockHeight 116 } 117 118 // newWatchdog creates a new watchdog. 119 func newWatchdog(contractor *Contractor) *watchdog { 120 renewWindow := contractor.Allowance().RenewWindow 121 return &watchdog{ 122 contracts: make(map[types.FileContractID]*fileContractStatus), 123 archivedContracts: make(map[types.FileContractID]skymodules.ContractWatchStatus), 124 outputDependencies: make(map[types.SiacoinOutputID]map[types.FileContractID]struct{}), 125 126 renewWindow: renewWindow, 127 blockHeight: contractor.blockHeight, 128 129 staticTPool: contractor.staticTPool, 130 staticDeps: contractor.staticDeps, 131 staticContractor: contractor, 132 } 133 } 134 135 // ContractStatus returns the status of a contract in the watchdog. 136 func (c *Contractor) ContractStatus(fcID types.FileContractID) (skymodules.ContractWatchStatus, bool) { 137 if err := c.staticTG.Add(); err != nil { 138 return skymodules.ContractWatchStatus{}, false 139 } 140 defer c.staticTG.Done() 141 return c.staticWatchdog.managedContractStatus(fcID) 142 } 143 144 // callAllowanceUpdated informs the watchdog of an allowance change. 145 func (w *watchdog) callAllowanceUpdated(a skymodules.Allowance) { 146 w.mu.Lock() 147 defer w.mu.Unlock() 148 149 // Set the new renewWindow. 150 w.renewWindow = a.RenewWindow 151 } 152 153 // callMonitorContract tells the watchdog to monitor the blockchain for data 154 // relevant to the given contract. 155 func (w *watchdog) callMonitorContract(args monitorContractArgs) error { 156 w.mu.Lock() 157 defer w.mu.Unlock() 158 w.staticContractor.staticLog.Debugf("callMonitorContract for contract: %v at height %v (Watchdog height: %v)", args.fcID, args.blockHeight, w.blockHeight) 159 160 if _, ok := w.contracts[args.fcID]; ok { 161 w.staticContractor.staticLog.Println("watchdog asked to watch contract it already knowns: ", args.fcID) 162 return errAlreadyWatchingContract 163 } 164 165 if len(args.revisionTxn.FileContractRevisions) == 0 { 166 w.staticContractor.staticLog.Println("No revisions in revisiontxn", args) 167 return errors.New("no revision in monitor contract args") 168 } 169 170 // Sanity check on non-recovery monitoring. 171 if !args.recovered { 172 saneInputs := len(args.formationTxnSet) != 0 173 saneInputs = saneInputs && len(args.sweepTxn.SiacoinInputs) != 0 174 saneInputs = saneInputs && args.blockHeight != 0 175 if !saneInputs { 176 w.staticContractor.staticLog.Critical("Bad args given for contract: ", args) 177 return errors.New("bad args for non recovered contract") 178 } 179 } 180 181 fileContractStatus := &fileContractStatus{ 182 // this height is meaningless for a recovered contract, but will be set to 183 // something reasonable if the formation transaction is reorged out. 184 formationSweepHeight: args.blockHeight + waitTime, 185 formationTxnSet: args.formationTxnSet, 186 contractFound: args.recovered, 187 parentOutputs: make(map[types.SiacoinOutputID]struct{}), 188 sweepTxn: args.sweepTxn, 189 sweepParents: args.sweepParents, 190 windowStart: args.revisionTxn.FileContractRevisions[0].NewWindowStart, 191 windowEnd: args.revisionTxn.FileContractRevisions[0].NewWindowEnd, 192 } 193 w.contracts[args.fcID] = fileContractStatus 194 195 // Watch the parent outputs of this set. 196 outputDependencies := getParentOutputIDs(args.formationTxnSet) 197 for _, oid := range outputDependencies { 198 w.addOutputDependency(oid, args.fcID) 199 } 200 201 w.staticContractor.staticLog.Debugln("Monitoring contract: ", args.fcID) 202 return nil 203 } 204 205 // callScanConsensusChange scans applied and reverted blocks, updating the 206 // watchdog's state with all information relevant to monitored contracts. 207 func (w *watchdog) callScanConsensusChange(cc modules.ConsensusChange) { 208 w.mu.Lock() 209 defer w.mu.Unlock() 210 for _, block := range cc.RevertedBlocks { 211 if block.ID() != types.GenesisID { 212 w.blockHeight-- 213 } 214 w.scanRevertedBlock(block) 215 } 216 217 for _, block := range cc.AppliedBlocks { 218 if block.ID() != types.GenesisID { 219 w.blockHeight++ 220 } 221 w.scanAppliedBlock(block) 222 } 223 } 224 225 // sendTxnSet broadcasts a transaction set and logs errors that are not 226 // duplicate transaction errors. (This is because the watchdog may be 227 // overzealous in sending out transactions). 228 func (w *watchdog) sendTxnSet(txnSet []types.Transaction, reason string) { 229 w.staticContractor.staticLog.Debugln("Sending txn set to tpool", reason) 230 if w.staticDeps.Disrupt("DisableWatchdogBroadcast") { 231 w.staticContractor.staticLog.Debugln("(Watchdog Broadcast Disrupted)") 232 return 233 } 234 235 // Send the transaction set in a go-routine to avoid deadlock when this 236 // sendTxnSet is called within ProcessConsensusChange. 237 go func() { 238 err := w.staticContractor.staticTG.Add() 239 if err != nil { 240 return 241 } 242 defer w.staticContractor.staticTG.Done() 243 244 err = w.staticTPool.AcceptTransactionSet(txnSet) 245 if err != nil && !errors.Contains(err, modules.ErrDuplicateTransactionSet) { 246 w.staticContractor.staticLog.Println("watchdog send transaction error: "+reason, err) 247 } 248 }() 249 } 250 251 // archiveContract archives the file contract. Include a non-zero double spend 252 // height if the reason for archival is that the contract was double-spent. 253 func (w *watchdog) archiveContract(fcID types.FileContractID, doubleSpendHeight types.BlockHeight) { 254 w.staticContractor.staticLog.Println("Archiving contract: ", fcID) 255 contractData, ok := w.contracts[fcID] 256 if !ok { 257 return 258 } 259 for oid := range contractData.parentOutputs { 260 w.removeOutputDependency(oid, fcID) 261 } 262 w.archivedContracts[fcID] = skymodules.ContractWatchStatus{ 263 Archived: true, 264 FormationSweepHeight: contractData.formationSweepHeight, 265 ContractFound: contractData.contractFound, 266 LatestRevisionFound: contractData.revisionFound, 267 StorageProofFoundAtHeight: contractData.storageProofFound, 268 DoubleSpendHeight: doubleSpendHeight, 269 WindowStart: contractData.windowStart, 270 WindowEnd: contractData.windowEnd, 271 } 272 delete(w.contracts, fcID) 273 } 274 275 // addOutputDependency marks the contract with fcID as dependent on this Siacoin 276 // output. 277 func (w *watchdog) addOutputDependency(outputID types.SiacoinOutputID, fcID types.FileContractID) { 278 dependentFCs, ok := w.outputDependencies[outputID] 279 if !ok { 280 dependentFCs = make(map[types.FileContractID]struct{}) 281 } 282 dependentFCs[fcID] = struct{}{} 283 w.outputDependencies[outputID] = dependentFCs 284 285 // Add the dependencies into the contract metadata also. 286 contractData := w.contracts[fcID] 287 contractData.parentOutputs[outputID] = struct{}{} 288 } 289 290 // removeOutputDependency removes the given SiacoinOutput from the dependencies 291 // of this file contract. 292 func (w *watchdog) removeOutputDependency(outputID types.SiacoinOutputID, fcID types.FileContractID) { 293 dependentFCs, ok := w.outputDependencies[outputID] 294 if !ok { 295 w.staticContractor.staticLog.Debugf("unable to remove output dependency: outputID not found in outputDependencies: outputID: %s", crypto.Hash(outputID).String()) 296 return 297 } 298 299 _, foundContract := dependentFCs[fcID] 300 if !foundContract { 301 w.staticContractor.staticLog.Debugf("unable to remove output dependency: FileContract not marked in outputDependencies: fcID: %s, outputID: %s", crypto.Hash(fcID).String(), crypto.Hash(outputID).String()) 302 return 303 } 304 305 if len(dependentFCs) == 1 { 306 // If this is the only file contract dependent on that output, delete the 307 // whole set. 308 delete(w.outputDependencies, outputID) 309 } else { 310 delete(dependentFCs, fcID) 311 } 312 313 if contractData, ok := w.contracts[fcID]; ok { 314 delete(contractData.parentOutputs, outputID) 315 } 316 } 317 318 // getParentOutputIDs returns the IDs of the parent SiacoinOutputs used in the 319 // transaction set. That is, it returns the SiacoinOutputs that this transaction 320 // set is dependent on. 321 func getParentOutputIDs(txnSet []types.Transaction) []types.SiacoinOutputID { 322 // Create a map of created and spent outputs. The parent outputs are those 323 // that are spent but not created in this set. 324 createdOutputs := make(map[types.SiacoinOutputID]bool) 325 spentOutputs := make(map[types.SiacoinOutputID]bool) 326 for _, txn := range txnSet { 327 for _, scInput := range txn.SiacoinInputs { 328 spentOutputs[scInput.ParentID] = true 329 } 330 for i := range txn.SiacoinOutputs { 331 createdOutputs[txn.SiacoinOutputID(uint64(i))] = true 332 } 333 } 334 335 // Remove all intermediary outputs that were created in the set from the set 336 // of spentOutputs. 337 parentOutputs := make([]types.SiacoinOutputID, 0) 338 for id := range spentOutputs { 339 if !createdOutputs[id] { 340 parentOutputs = append(parentOutputs, id) 341 } 342 } 343 344 return parentOutputs 345 } 346 347 // removeTxnFromSet is a helper function used to create a standalone-valid 348 // transaction set by removing confirmed transactions from a transaction set. If 349 // the transaction meant to be removed is not present in the set and error is 350 // returned, otherwise a new transaction set is returned that no longer contains 351 // that transaction. 352 func removeTxnFromSet(txn types.Transaction, txnSet []types.Transaction) ([]types.Transaction, error) { 353 txnID := txn.ID() 354 355 for i, txnFromSet := range txnSet { 356 if txnFromSet.ID() == txnID { 357 // Create the new set without the txn. 358 newSet := append(txnSet[:i], txnSet[i+1:]...) 359 return newSet, nil 360 } 361 } 362 363 // Since this function is called when some parent inputs of the txnSet are 364 // spent, this error indicates that the txn given double-spends a txn from 365 // the set. 366 return nil, errTxnNotInSet 367 } 368 369 // scanAppliedBlock updates the watchdog's state with data from a newly 370 // connected block. It checks for contracts, revisions, and proofs of monitored 371 // contracts and also for double-spends of any outputs which monitored contracts 372 // depend on. 373 func (w *watchdog) scanAppliedBlock(block types.Block) { 374 w.staticContractor.staticLog.Debugln("Watchdog scanning applied block at height: ", w.blockHeight) 375 376 for _, txn := range block.Transactions { 377 for i := range txn.FileContracts { 378 fcID := txn.FileContractID(uint64(i)) 379 if contractData, ok := w.contracts[fcID]; ok { 380 contractData.contractFound = true 381 w.staticContractor.staticLog.Debugln("Found contract: ", fcID) 382 } 383 } 384 385 for _, rev := range txn.FileContractRevisions { 386 if contractData, ok := w.contracts[rev.ParentID]; ok { 387 contractData.revisionFound = rev.NewRevisionNumber 388 w.staticContractor.staticLog.Debugln("Found revision for: ", rev.ParentID, rev.NewRevisionNumber) 389 } 390 } 391 392 for _, storageProof := range txn.StorageProofs { 393 if contractData, ok := w.contracts[storageProof.ParentID]; ok { 394 contractData.storageProofFound = w.blockHeight 395 w.staticContractor.staticLog.Debugln("Found storage proof: ", storageProof.ParentID) 396 } 397 } 398 399 // Check the transaction for spends of any inputs a monitored file contract 400 // depends on. 401 w.findDependencySpends(txn) 402 } 403 } 404 405 // findDependencySpends checks the transactions from a newly applied block to 406 // see if it spends outputs which monitored contracts are dependent on. If so, 407 // the function prunes the formationTxnSet for that contract and updates its 408 // dependencies. 409 func (w *watchdog) findDependencySpends(txn types.Transaction) { 410 // This transaction could be double-spending inputs used across multiple 411 // file contracts. Keep track of those here. 412 inputsSpent := make(map[types.FileContractID]struct{}) 413 spendsMonitoredOutput := false 414 for _, scInput := range txn.SiacoinInputs { 415 // Check if the output spent here is a parent of any contract being 416 // monitored. 417 fcIDs, watching := w.outputDependencies[scInput.ParentID] 418 if !watching { 419 continue 420 } 421 422 for fcID := range fcIDs { 423 // If we found the contract already, then this output must be spent in the 424 // formation txn set. Otherwise we must check if this transaction 425 // double-spends any inputs for the formation transaction set. 426 _, ok := w.contracts[fcID] 427 if !ok { 428 w.staticContractor.staticLog.Critical("Found dependency on un-monitored formation") 429 continue 430 } 431 spendsMonitoredOutput = true 432 inputsSpent[fcID] = struct{}{} 433 } 434 } 435 436 if !spendsMonitoredOutput { 437 return 438 } 439 440 // Try removing this transaction from the formation transaction set, and 441 // check if the pruned formation transaction set is still considered valid. 442 for fcID := range inputsSpent { 443 // Some transactions from this contract's formation set may have already 444 // been pruned. If so, use the most recent set. 445 contractData := w.contracts[fcID] 446 txnSet := contractData.formationTxnSet 447 448 // Try removing this transaction from the set. 449 prunedFormationTxnSet, err := removeTxnFromSet(txn, txnSet) 450 if err != nil { 451 w.staticContractor.staticLog.Println("Error removing txn from set, inputs were double-spent:", err, fcID, len(txnSet), txn.ID()) 452 453 // Signal to the contractor that this contract's inputs were 454 // double-spent and that it should be removed. 455 w.archiveContract(fcID, w.blockHeight) 456 go w.staticContractor.callNotifyDoubleSpend(fcID, w.blockHeight) 457 continue 458 } 459 460 w.staticContractor.staticLog.Debugln("Removed transaction from set for: ", fcID, len(prunedFormationTxnSet), txn.ID()) 461 contractData.formationTxnSet = prunedFormationTxnSet 462 463 // Get the new set of parent output IDs. 464 newDepOutputs := getParentOutputIDs(prunedFormationTxnSet) 465 466 // Remove outputs no longer needed as dependencies 467 for oid := range contractData.parentOutputs { 468 isStillADependency := false 469 for _, newDep := range newDepOutputs { 470 if oid == newDep { 471 isStillADependency = true 472 break 473 } 474 } 475 if !isStillADependency { 476 w.removeOutputDependency(oid, fcID) 477 } 478 } 479 480 // Add any new dependencies. 481 for _, oid := range newDepOutputs { 482 if _, ok := contractData.parentOutputs[oid]; !ok { 483 w.addOutputDependency(oid, fcID) 484 } 485 } 486 } 487 } 488 489 // scanRevertedBlock updates the watchdog's state with data from a newly 490 // reverted block. It checks for the removal of contracts, revisions, and proofs 491 // of monitored contracts and also for the creation of any new dependencies for 492 // monitored formation transaction sets. 493 func (w *watchdog) scanRevertedBlock(block types.Block) { 494 w.staticContractor.staticLog.Debugln("Watchdog scanning reverted block at height: ", w.blockHeight) 495 496 outputsCreatedInBlock := make(map[types.SiacoinOutputID]*types.Transaction) 497 for i := 0; i < len(block.Transactions); i++ { 498 txn := &block.Transactions[i] 499 for i := range txn.SiacoinOutputs { 500 outputsCreatedInBlock[txn.SiacoinOutputID(uint64(i))] = txn 501 } 502 503 for i := range txn.FileContracts { 504 fcID := txn.FileContractID(uint64(i)) 505 // After a blockchain reorg, it's possible that a contract that used to be on 506 // the active chain is no longer in the new active chain. To make sure all 507 // active contracts are actually committed to on-chain, the watchdog keeps track 508 // of any contracts removed during a reorg. If they have not re-appeared and the 509 // contractor is synced then the watchdog must re-broadcast the file contract's 510 // transaction. 511 contractData, ok := w.contracts[fcID] 512 if !ok { 513 continue 514 } 515 516 w.staticContractor.staticLog.Println("Contract formation txn reverted: ", fcID) 517 contractData.contractFound = false 518 519 // Set watchheight to max(current watch height, current height + leeway) 520 if contractData.formationSweepHeight < w.blockHeight+reorgLeeway { 521 contractData.formationSweepHeight = w.blockHeight + reorgLeeway 522 } 523 524 // Sanity check: if the contract was previously confirmed, it should have 525 // been removed from the formationTxnSet. 526 if len(contractData.formationTxnSet) != 0 { 527 w.staticContractor.staticLog.Critical("found reverted contract with non-empty formationTxnSet in watchdog", fcID) 528 } 529 530 // Re-add the file contract transaction to the formationTxnSet. 531 contractData.formationTxnSet = []types.Transaction{*txn} 532 outputDependencies := getParentOutputIDs(contractData.formationTxnSet) 533 for _, oid := range outputDependencies { 534 w.addOutputDependency(oid, fcID) 535 } 536 } 537 538 for _, rev := range txn.FileContractRevisions { 539 if contractData, ok := w.contracts[rev.ParentID]; ok { 540 w.staticContractor.staticLog.Debugln("Revision for monitored contract reverted: ", rev.ParentID, rev.NewRevisionNumber) 541 contractData.revisionFound = 0 // There are no zero revisions. 542 } 543 } 544 545 for _, storageProof := range txn.StorageProofs { 546 if contractData, ok := w.contracts[storageProof.ParentID]; ok { 547 w.staticContractor.staticLog.Debugln("Storage proof for monitored contract reverted: ", storageProof.ParentID) 548 contractData.storageProofFound = 0 549 } 550 } 551 } 552 w.updateDependenciesFromRevertedBlock(outputsCreatedInBlock) 553 } 554 555 // updateDependenciesFromRevertedBlock checks all created outputs in a reverted 556 // block to see if any monitored formation transactions are dependent on them. 557 // If so, the watchdog adds the reverted transaction creating that output as a 558 // dependency for that file contract. 559 func (w *watchdog) updateDependenciesFromRevertedBlock(createdOutputs map[types.SiacoinOutputID]*types.Transaction) { 560 // Create a queue of outputs to check. 561 outputQueue := make([]types.SiacoinOutputID, 0) 562 outputsInQueue := make(map[types.SiacoinOutputID]struct{}) 563 564 // Helper function that adds outputs spent in this transaction to the queue, 565 // if they are not already in it. 566 addParentOutputsToQueue := func(txn *types.Transaction) { 567 for _, scInput := range txn.SiacoinInputs { 568 _, outputCreatedInBlock := createdOutputs[scInput.ParentID] 569 _, inQueue := outputsInQueue[scInput.ParentID] 570 if !inQueue && outputCreatedInBlock { 571 outputQueue = append(outputQueue, scInput.ParentID) 572 outputsInQueue[scInput.ParentID] = struct{}{} 573 } 574 } 575 } 576 577 // Populate the queue first by checking all outputs once. 578 for outputID, txn := range createdOutputs { 579 // Check if any file contracts being monitored by the watchdog have this 580 // output as a dependency. 581 dependentFCs, watching := w.outputDependencies[outputID] 582 if !watching { 583 continue 584 } 585 // Add the new dependencies to file contracts dependent on this output. 586 for fcID := range dependentFCs { 587 w.staticContractor.staticLog.Debugln("Adding dependency to file contract:", fcID, txn.ID()) 588 w.addDependencyToContractFormationSet(fcID, *txn) 589 } 590 // Queue up the parent outputs so that we can check if they are adding new 591 // dependencies as well. 592 addParentOutputsToQueue(txn) 593 } 594 595 // Go through all the new dependencies in the queue. 596 var outputID types.SiacoinOutputID 597 for len(outputQueue) > 0 { 598 // Pop on output ID off the queue. 599 outputID, outputQueue = outputQueue[0], outputQueue[1:] 600 txn := createdOutputs[outputID] 601 602 // Check if any file contracts being monitored by the watchdog have this 603 // output as a dependency. 604 dependentFCs, watching := w.outputDependencies[outputID] 605 if !watching { 606 continue 607 } 608 // Add the new dependencies to file contracts dependent on this output. 609 for fcID := range dependentFCs { 610 w.staticContractor.staticLog.Debugln("Adding dependency to file contract:", fcID, txn.ID()) 611 w.addDependencyToContractFormationSet(fcID, *txn) 612 } 613 // Queue up the parent outputs so that we can check if they are adding new 614 // dependencies as well. 615 addParentOutputsToQueue(txn) 616 617 // Remove from outputsInQueue map at end of function, in order to not re-add 618 // the same output. 619 delete(outputsInQueue, outputID) 620 } 621 } 622 623 // addDependencyToContractFormationSet adds a tranasaction to a contract's 624 // formationTransactionSet, if it is not already in that set. It also adds 625 // the outputs spent in that transaction as dependencies for this file contract. 626 func (w *watchdog) addDependencyToContractFormationSet(fcID types.FileContractID, txn types.Transaction) { 627 contractData := w.contracts[fcID] 628 txnSet := contractData.formationTxnSet 629 630 txnID := txn.ID() 631 for _, existingTxn := range contractData.formationTxnSet { 632 // Don't add duplicate transactions. 633 if txnID == existingTxn.ID() { 634 return 635 } 636 } 637 638 // Add this transaction as a new dependency. 639 // NOTE: Dependencies must be prepended to maintain correct ordering. 640 txnSet = append([]types.Transaction{txn}, txnSet...) 641 contractData.formationTxnSet = txnSet 642 643 // Add outputs as dependencies to this file contract. 644 for _, scInput := range txn.SiacoinInputs { 645 w.addOutputDependency(scInput.ParentID, fcID) 646 } 647 } 648 649 // callCheckContracts checks if the watchdog needs to take any actions for 650 // any contracts its watching at this blockHeight. For newly formed contracts, 651 // it checks if a contract has been seen on-chain yet, if not the watchdog will 652 // re-broadcast the initial transaction. If enough time has elapsed the watchdog 653 // will double-spend the inputs used to create that file contract. 654 // 655 // The watchdog also checks if the latest revision for a file contract has been 656 // posted yet. If not, the watchdog will also re-broadcast that transaction. 657 // 658 // Finally, the watchdog checks if hosts' storage proofs made it on chain within 659 // their expiration window, and notifies the contractor of the storage proof 660 // status. 661 func (w *watchdog) callCheckContracts() { 662 w.mu.Lock() 663 defer w.mu.Unlock() 664 w.staticContractor.staticLog.Debugln("Watchdog checking contracts at height:", w.blockHeight) 665 666 for fcID, contractData := range w.contracts { 667 if !contractData.contractFound { 668 w.checkUnconfirmedContract(fcID, contractData) 669 } 670 671 if (w.blockHeight >= contractData.windowStart-w.renewWindow) && (contractData.revisionFound != 0) { 672 // Check if the most recent revision has appeared on-chain. If not send it 673 // ourselves. Called in a go-routine because the contractor may be in 674 // maintenance which can cause a deadlock because this function Acquires a 675 // lock using the contractset. 676 w.staticContractor.staticLog.Debugln("Checking revision for monitored contract: ", fcID) 677 go func(fcid types.FileContractID, bh types.BlockHeight) { 678 err := w.staticContractor.staticTG.Add() 679 if err != nil { 680 return 681 } 682 defer w.staticContractor.staticTG.Done() 683 w.threadedCheckMonitorRevision(fcid, bh) 684 }(fcID, w.blockHeight) 685 } 686 687 if w.blockHeight >= contractData.windowEnd { 688 if contractData.storageProofFound == 0 { 689 // TODO: penalize host / send signal back to watchee 690 w.staticContractor.staticLog.Debugln("didn't find proof", fcID) 691 } else { 692 // TODO: ++ host / send signal back to watchee 693 w.staticContractor.staticLog.Debugln("did find proof", fcID) 694 } 695 w.archiveContract(fcID, 0) 696 } 697 } 698 } 699 700 // checkUnconfirmedContract re-broadcasts the file contract formation 701 // transaction or sweeps the inputs used by the renter, depending on whether or 702 // not the transaction set has too many added dependencies or if the 703 // formationSweepHeight has been reached. 704 func (w *watchdog) checkUnconfirmedContract(fcID types.FileContractID, contractData *fileContractStatus) { 705 // Check that the size of the formationTxnSet has not gone beyond the 706 // standardness limit. If it has, then we won't be able to propagate it 707 // anymore. 708 var setSize int 709 for _, txn := range contractData.formationTxnSet { 710 setSize += txn.MarshalSiaSize() 711 } 712 if setSize > modules.TransactionSetSizeLimit { 713 w.staticContractor.staticLog.Println("UpdatedFormationTxnSet beyond set size limit", fcID) 714 } 715 716 if (w.blockHeight >= contractData.formationSweepHeight) || (setSize > modules.TransactionSetSizeLimit) { 717 w.staticContractor.staticLog.Println("Sweeping inputs: ", w.blockHeight, contractData.formationSweepHeight) 718 // TODO: Add parent transactions if the renter's own dependencies are 719 // causing this to be triggered. 720 w.sweepContractInputs(fcID, contractData) 721 } else { 722 // Try to broadcast the transaction set again. 723 debugStr := fmt.Sprintf("sending formation txn for contract with id: %s at h=%d wh=%d", fcID.String(), w.blockHeight, contractData.formationSweepHeight) 724 w.staticContractor.staticLog.Debugln(debugStr) 725 w.sendTxnSet(contractData.formationTxnSet, debugStr) 726 } 727 } 728 729 // threadedCheckMonitorRevision checks if the given FileContract has it latest 730 // revision posted on-chain. If not, the watchdog broadcasts the latest revision 731 // transaction itself. 732 func (w *watchdog) threadedCheckMonitorRevision(fcID types.FileContractID, height types.BlockHeight) { 733 // Get the highest revision number seen by the watchdog for this FC. 734 var revNumFound uint64 735 w.mu.Lock() 736 if contractData, ok := w.contracts[fcID]; ok { 737 revNumFound = contractData.revisionFound 738 } 739 w.mu.Unlock() 740 741 // Get the last revision transaction from the contractset / oldcontracts. 742 var lastRevisionTxn types.Transaction 743 contract, ok := w.staticContractor.staticContracts.Acquire(fcID) 744 if ok { 745 lastRevisionTxn = contract.Metadata().Transaction 746 w.staticContractor.staticContracts.Return(contract) 747 } else { 748 w.staticContractor.staticLog.Debugln("Unable to Acquire monitored contract from contractset", fcID) 749 // Try old contracts. If the contract was renewed already it won't be in the 750 // contractset. 751 w.staticContractor.mu.RLock() 752 contract, ok := w.staticContractor.oldContracts[fcID] 753 if !ok { 754 w.staticContractor.staticLog.Debugln("Unable to Acquire monitored contract from oldContracts", fcID) 755 w.staticContractor.mu.RUnlock() 756 return 757 } 758 lastRevisionTxn = contract.Transaction 759 w.staticContractor.mu.RUnlock() 760 } 761 762 lastRevNum := lastRevisionTxn.FileContractRevisions[0].NewRevisionNumber 763 if lastRevNum > revNumFound { 764 // NOTE: fee-bumping via CPFP (the watchdog will do this every block 765 // until it sees the revision or the window has closed.) 766 debugStr := fmt.Sprintf("sending revision txn for contract with id: %s revNum: %d", fcID.String(), lastRevNum) 767 w.staticContractor.staticLog.Debugln(debugStr) 768 w.mu.Lock() 769 w.sendTxnSet([]types.Transaction{lastRevisionTxn}, debugStr) 770 w.mu.Unlock() 771 } 772 } 773 774 // sweepContractInputs spends the inputs used initially by the contractor 775 // for creating a file contract, and sends them to an address owned by 776 // this wallet. This is done only if a file contract has not appeared on-chain 777 // in time. 778 // TODO: this function fails if the wallet is locked. since an alert is already 779 // broadcast to the user, it might be useful to retry a sweep once the wallet 780 // is unlocked. 781 func (w *watchdog) sweepContractInputs(fcID types.FileContractID, contractData *fileContractStatus) { 782 sweepBuilder, err := w.staticContractor.staticWallet.RegisterTransaction(contractData.sweepTxn, contractData.sweepParents) 783 if err != nil { 784 w.staticContractor.staticLog.Println("Unable to register sweep transaction") 785 return 786 } 787 markedInputs := sweepBuilder.MarkWalletInputs() 788 if !markedInputs { 789 w.staticContractor.staticLog.Println("sweepBuilder did not mark any owned inputs") 790 } 791 792 // Get the size of the transaction set so far for fee calculation. 793 setSize := contractData.sweepTxn.MarshalSiaSize() 794 for _, sweepParent := range contractData.sweepParents { 795 setSize += sweepParent.MarshalSiaSize() 796 } 797 798 // Estimate a transaction fee and add it to the txn. 799 _, maxFee := w.staticTPool.FeeEstimation() 800 txnFee := maxFee.Mul64(uint64(setSize)) // Estimated transaction size in bytes 801 sweepBuilder.AddMinerFee(txnFee) 802 803 txn, _ := sweepBuilder.View() 804 // There can be refund outputs, but the last output is the one that is used to 805 // sweep. 806 numOuts := len(txn.SiacoinOutputs) 807 if numOuts == 0 { 808 w.staticContractor.staticLog.Println("expected at least 1 output in sweepTxn", len(txn.SiacoinOutputs)) 809 return 810 } 811 replacementOutput := types.SiacoinOutput{ 812 Value: txn.SiacoinOutputs[numOuts-1].Value.Sub(txnFee), 813 UnlockHash: txn.SiacoinOutputs[numOuts-1].UnlockHash, 814 } 815 err = sweepBuilder.ReplaceSiacoinOutput(uint64(numOuts-1), replacementOutput) 816 if err != nil { 817 w.staticContractor.staticLog.Println("error replacing output in sweep") 818 return 819 } 820 821 signedTxnSet, err := sweepBuilder.Sign(true) 822 if err != nil { 823 w.staticContractor.staticLog.Println("unable to sign sweep txn", fcID) 824 return 825 } 826 827 debugStr := fmt.Sprintf("SweepTxn for contract with id: %s", fcID.String()) 828 w.sendTxnSet(signedTxnSet, debugStr) 829 } 830 831 // managedContractStatus returns the status of a contract in the watchdog if it 832 // exists. 833 func (w *watchdog) managedContractStatus(fcID types.FileContractID) (skymodules.ContractWatchStatus, bool) { 834 w.mu.Lock() 835 defer w.mu.Unlock() 836 837 contractStatus, ok := w.archivedContracts[fcID] 838 if ok { 839 return contractStatus, true 840 } 841 842 contractData, ok := w.contracts[fcID] 843 if !ok { 844 return skymodules.ContractWatchStatus{}, false 845 } 846 847 return skymodules.ContractWatchStatus{ 848 Archived: false, 849 FormationSweepHeight: contractData.formationSweepHeight, 850 ContractFound: contractData.contractFound, 851 LatestRevisionFound: contractData.revisionFound, 852 StorageProofFoundAtHeight: contractData.storageProofFound, 853 WindowStart: contractData.windowStart, 854 WindowEnd: contractData.windowEnd, 855 }, true 856 } 857 858 // threadedSendMostRecentRevision sends the most recent revision transaction out. 859 // Should be called whenever a contract is no longer going to be used. 860 func (w *watchdog) threadedSendMostRecentRevision(metadata skymodules.RenterContract) { 861 if err := w.staticContractor.staticTG.Add(); err != nil { 862 return 863 } 864 defer w.staticContractor.staticTG.Done() 865 fcID := metadata.ID 866 lastRevisionTxn := metadata.Transaction 867 lastRevNum := lastRevisionTxn.FileContractRevisions[0].NewRevisionNumber 868 869 debugStr := fmt.Sprintf("sending most recent revision txn for contract with id: %s revNum: %d", fcID.String(), lastRevNum) 870 w.mu.Lock() 871 w.sendTxnSet([]types.Transaction{lastRevisionTxn}, debugStr) 872 w.mu.Unlock() 873 }