gitlab.com/jokerrs1/Sia@v1.3.2/modules/host/storageobligations.go (about) 1 package host 2 3 // storageobligations.go is responsible for managing the storage obligations 4 // within the host - making sure that any file contracts, transaction 5 // dependencies, file contract revisions, and storage proofs are making it into 6 // the blockchain in a reasonable time. 7 // 8 // NOTE: Currently, the code partially supports changing the storage proof 9 // window in file contract revisions, however the action item code will not 10 // handle it correctly. Until the action item code is improved (to also handle 11 // byzantine situations where the renter submits prior revisions), the host 12 // should not support changing the storage proof window, especially to further 13 // in the future. 14 15 // TODO: Need to queue the action item for checking on the submission status of 16 // the file contract revision. Also need to make sure that multiple actions are 17 // being taken if needed. 18 19 // TODO: Make sure that the origin tranasction set is not submitted to the 20 // transaction pool before addSO is called - if it is, there will be a 21 // duplicate transaction error, and then the storage obligation will return an 22 // error, which is bad. Well, or perhas we just need to have better logic 23 // handling. 24 25 // TODO: Need to make sure that 'revision confirmed' is actually looking only 26 // at the most recent revision (I think it is...) 27 28 // TODO: Make sure that not too many action items are being created. 29 30 import ( 31 "encoding/binary" 32 "encoding/json" 33 "errors" 34 35 "github.com/NebulousLabs/Sia/build" 36 "github.com/NebulousLabs/Sia/crypto" 37 "github.com/NebulousLabs/Sia/encoding" 38 "github.com/NebulousLabs/Sia/modules" 39 "github.com/NebulousLabs/Sia/types" 40 41 "github.com/coreos/bbolt" 42 ) 43 44 const ( 45 obligationUnresolved storageObligationStatus = iota // Indicatees that an unitialized value was used. 46 obligationRejected // Indicates that the obligation never got started, no revenue gained or lost. 47 obligationSucceeded // Indicates that the obligation was completed, revenues were gained. 48 obligationFailed // Indicates that the obligation failed, revenues and collateral were lost. 49 ) 50 51 var ( 52 // errDuplicateStorageObligation is returned when the storage obligation 53 // database already has a storage obligation with the provided file 54 // contract. This error should only happen in the event of a developer 55 // mistake. 56 errDuplicateStorageObligation = errors.New("storage obligation has a file contract which conflicts with an existing storage obligation") 57 58 // errInsaneFileContractOutputCounts is returned when a file contract has 59 // the wrong number of outputs for either the valid or missed payouts. 60 errInsaneFileContractOutputCounts = errors.New("file contract has incorrect number of outputs for the valid or missed payouts") 61 62 // errInsaneFileContractRevisionOutputCounts is returned when a file 63 // contract has the wrong number of outputs for either the valid or missed 64 // payouts. 65 errInsaneFileContractRevisionOutputCounts = errors.New("file contract revision has incorrect number of outputs for the valid or missed payouts") 66 67 // errInsaneOriginSetFileContract is returned is the final transaction of 68 // the origin transaction set of a storage obligation does not have a file 69 // contract in the final transaction - there should be a file contract 70 // associated with every storage obligation. 71 errInsaneOriginSetFileContract = errors.New("origin transaction set of storage obligation should have one file contract in the final transaction") 72 73 // errInsaneOriginSetSize is returned if the origin transaction set of a 74 // storage obligation is empty - there should be a file contract associated 75 // with every storage obligation. 76 errInsaneOriginSetSize = errors.New("origin transaction set of storage obligation is size zero") 77 78 // errInsaneRevisionSetRevisionCount is returned if the final transaction 79 // in the revision transaction set of a storage obligation has more or less 80 // than one file contract revision. 81 errInsaneRevisionSetRevisionCount = errors.New("revision transaction set of storage obligation should have one file contract revision in the final transaction") 82 83 // errInsaneStorageObligationRevision is returned if there is an attempted 84 // storage obligation revision which does not have sensical inputs. 85 errInsaneStorageObligationRevision = errors.New("revision to storage obligation does not make sense") 86 87 // errInsaneStorageObligationRevisionData is returned if there is an 88 // attempted storage obligation revision which does not have sensical 89 // inputs. 90 errInsaneStorageObligationRevisionData = errors.New("revision to storage obligation has insane data") 91 92 // errNoBuffer is returned if there is an attempted storage obligation that 93 // needs to have the storage proof submitted in less than 94 // revisionSubmissionBuffer blocks. 95 errNoBuffer = errors.New("file contract rejected because storage proof window is too close") 96 97 // errNoStorageObligation is returned if the requested storage obligation 98 // is not found in the database. 99 errNoStorageObligation = errors.New("storage obligation not found in database") 100 101 // errObligationUnlocked is returned when a storage obligation is being 102 // removed from lock, but is already unlocked. 103 errObligationUnlocked = errors.New("storage obligation is unlocked, and should not be getting unlocked") 104 ) 105 106 type storageObligationStatus uint64 107 108 // storageObligation contains all of the metadata related to a file contract 109 // and the storage contained by the file contract. 110 type storageObligation struct { 111 // Storage obligations are broken up into ordered atomic sectors that are 112 // exactly 4MiB each. By saving the roots of each sector, storage proofs 113 // and modifications to the data can be made inexpensively by making use of 114 // the merkletree.CachedTree. Sectors can be appended, modified, or deleted 115 // and the host can recompute the Merkle root of the whole file without 116 // much computational or I/O expense. 117 SectorRoots []crypto.Hash 118 119 // Variables about the file contract that enforces the storage obligation. 120 // The origin an revision transaction are stored as a set, where the set 121 // contains potentially unconfirmed transactions. 122 ContractCost types.Currency 123 LockedCollateral types.Currency 124 PotentialDownloadRevenue types.Currency 125 PotentialStorageRevenue types.Currency 126 PotentialUploadRevenue types.Currency 127 RiskedCollateral types.Currency 128 TransactionFeesAdded types.Currency 129 130 // The negotiation height specifies the block height at which the file 131 // contract was negotiated. If the origin transaction set is not accepted 132 // onto the blockchain quickly enough, the contract is pruned from the 133 // host. The origin and revision transaction set contain the contracts + 134 // revisions as well as all parent transactions. The parents are necessary 135 // because after a restart the transaction pool may be emptied out. 136 NegotiationHeight types.BlockHeight 137 OriginTransactionSet []types.Transaction 138 RevisionTransactionSet []types.Transaction 139 140 // Variables indicating whether the critical transactions in a storage 141 // obligation have been confirmed on the blockchain. 142 OriginConfirmed bool 143 RevisionConstructed bool 144 RevisionConfirmed bool 145 ProofConstructed bool 146 ProofConfirmed bool 147 ObligationStatus storageObligationStatus 148 } 149 150 // getStorageObligation fetches a storage obligation from the database tx. 151 func getStorageObligation(tx *bolt.Tx, soid types.FileContractID) (so storageObligation, err error) { 152 soBytes := tx.Bucket(bucketStorageObligations).Get(soid[:]) 153 if soBytes == nil { 154 return storageObligation{}, errNoStorageObligation 155 } 156 err = json.Unmarshal(soBytes, &so) 157 if err != nil { 158 return storageObligation{}, err 159 } 160 return so, nil 161 } 162 163 // putStorageObligation places a storage obligation into the database, 164 // overwriting the existing storage obligation if there is one. 165 func putStorageObligation(tx *bolt.Tx, so storageObligation) error { 166 soBytes, err := json.Marshal(so) 167 if err != nil { 168 return err 169 } 170 soid := so.id() 171 return tx.Bucket(bucketStorageObligations).Put(soid[:], soBytes) 172 } 173 174 // expiration returns the height at which the storage obligation expires. 175 func (so storageObligation) expiration() types.BlockHeight { 176 if len(so.RevisionTransactionSet) > 0 { 177 return so.RevisionTransactionSet[len(so.RevisionTransactionSet)-1].FileContractRevisions[0].NewWindowStart 178 } 179 return so.OriginTransactionSet[len(so.OriginTransactionSet)-1].FileContracts[0].WindowStart 180 } 181 182 // fileSize returns the size of the data protected by the obligation. 183 func (so storageObligation) fileSize() uint64 { 184 if len(so.RevisionTransactionSet) > 0 { 185 return so.RevisionTransactionSet[len(so.RevisionTransactionSet)-1].FileContractRevisions[0].NewFileSize 186 } 187 return so.OriginTransactionSet[len(so.OriginTransactionSet)-1].FileContracts[0].FileSize 188 } 189 190 // id returns the id of the storage obligation, which is definied by the file 191 // contract id of the file contract that governs the storage contract. 192 func (so storageObligation) id() types.FileContractID { 193 return so.OriginTransactionSet[len(so.OriginTransactionSet)-1].FileContractID(0) 194 } 195 196 // isSane checks that required assumptions about the storage obligation are 197 // correct. 198 func (so storageObligation) isSane() error { 199 // There should be an origin transaction set. 200 if len(so.OriginTransactionSet) == 0 { 201 build.Critical("origin transaction set is empty") 202 return errInsaneOriginSetSize 203 } 204 205 // The final transaction of the origin transaction set should have one file 206 // contract. 207 final := len(so.OriginTransactionSet) - 1 208 fcCount := len(so.OriginTransactionSet[final].FileContracts) 209 if fcCount != 1 { 210 build.Critical("wrong number of file contracts associated with storage obligation:", fcCount) 211 return errInsaneOriginSetFileContract 212 } 213 214 // The file contract in the final transaction of the origin transaction set 215 // should have two valid proof outputs and two missed proof outputs. 216 lenVPOs := len(so.OriginTransactionSet[final].FileContracts[0].ValidProofOutputs) 217 lenMPOs := len(so.OriginTransactionSet[final].FileContracts[0].MissedProofOutputs) 218 if lenVPOs != 2 || lenMPOs != 2 { 219 build.Critical("file contract has wrong number of VPOs and MPOs, expecting 2 each:", lenVPOs, lenMPOs) 220 return errInsaneFileContractOutputCounts 221 } 222 223 // If there is a revision transaction set, there should be one file 224 // contract revision in the final transaction. 225 if len(so.RevisionTransactionSet) > 0 { 226 final = len(so.OriginTransactionSet) - 1 227 fcrCount := len(so.OriginTransactionSet[final].FileContractRevisions) 228 if fcrCount != 1 { 229 build.Critical("wrong number of file contract revisions in final transaction of revision transaction set:", fcrCount) 230 return errInsaneRevisionSetRevisionCount 231 } 232 233 // The file contract revision in the final transaction of the revision 234 // transaction set should have two valid proof outputs and two missed 235 // proof outputs. 236 lenVPOs = len(so.RevisionTransactionSet[final].FileContractRevisions[0].NewValidProofOutputs) 237 lenMPOs = len(so.RevisionTransactionSet[final].FileContractRevisions[0].NewMissedProofOutputs) 238 if lenVPOs != 2 || lenMPOs != 2 { 239 build.Critical("file contract has wrong number of VPOs and MPOs, expecting 2 each:", lenVPOs, lenMPOs) 240 return errInsaneFileContractRevisionOutputCounts 241 } 242 } 243 return nil 244 } 245 246 // merkleRoot returns the file merkle root of a storage obligation. 247 func (so storageObligation) merkleRoot() crypto.Hash { 248 if len(so.RevisionTransactionSet) > 0 { 249 return so.RevisionTransactionSet[len(so.RevisionTransactionSet)-1].FileContractRevisions[0].NewFileMerkleRoot 250 } 251 return so.OriginTransactionSet[len(so.OriginTransactionSet)-1].FileContracts[0].FileMerkleRoot 252 } 253 254 // payous returns the set of valid payouts and missed payouts that represent 255 // the latest revision for the storage obligation. 256 func (so storageObligation) payouts() (valid []types.SiacoinOutput, missed []types.SiacoinOutput) { 257 valid = make([]types.SiacoinOutput, 2) 258 missed = make([]types.SiacoinOutput, 2) 259 if len(so.RevisionTransactionSet) > 0 { 260 copy(valid, so.RevisionTransactionSet[len(so.RevisionTransactionSet)-1].FileContractRevisions[0].NewValidProofOutputs) 261 copy(missed, so.RevisionTransactionSet[len(so.RevisionTransactionSet)-1].FileContractRevisions[0].NewMissedProofOutputs) 262 return 263 } 264 copy(valid, so.OriginTransactionSet[len(so.OriginTransactionSet)-1].FileContracts[0].ValidProofOutputs) 265 copy(missed, so.OriginTransactionSet[len(so.OriginTransactionSet)-1].FileContracts[0].MissedProofOutputs) 266 return 267 } 268 269 // proofDeadline returns the height by which the storage proof must be 270 // submitted. 271 func (so storageObligation) proofDeadline() types.BlockHeight { 272 if len(so.RevisionTransactionSet) > 0 { 273 return so.RevisionTransactionSet[len(so.RevisionTransactionSet)-1].FileContractRevisions[0].NewWindowEnd 274 } 275 return so.OriginTransactionSet[len(so.OriginTransactionSet)-1].FileContracts[0].WindowEnd 276 } 277 278 // value returns the value of fulfilling the storage obligation to the host. 279 func (so storageObligation) value() types.Currency { 280 return so.ContractCost.Add(so.PotentialDownloadRevenue).Add(so.PotentialStorageRevenue).Add(so.PotentialUploadRevenue).Add(so.RiskedCollateral) 281 } 282 283 // queueActionItem adds an action item to the host at the input height so that 284 // the host knows to perform maintenance on the associated storage obligation 285 // when that height is reached. 286 func (h *Host) queueActionItem(height types.BlockHeight, id types.FileContractID) error { 287 // Sanity check - action item should be at a higher height than the current 288 // block height. 289 if height <= h.blockHeight { 290 h.log.Println("action item queued improperly") 291 } 292 return h.db.Update(func(tx *bolt.Tx) error { 293 // Translate the height into a byte slice. 294 heightBytes := make([]byte, 8) 295 binary.BigEndian.PutUint64(heightBytes, uint64(height)) 296 297 // Get the list of action items already at this height and extend it. 298 bai := tx.Bucket(bucketActionItems) 299 existingItems := bai.Get(heightBytes) 300 var extendedItems = make([]byte, len(existingItems), len(existingItems)+len(id[:])) 301 copy(extendedItems, existingItems) 302 extendedItems = append(extendedItems, id[:]...) 303 return bai.Put(heightBytes, extendedItems) 304 }) 305 } 306 307 // managedAddStorageObligation adds a storage obligation to the host. Because 308 // this operation can return errors, the transactions should not be submitted to 309 // the blockchain until after this function has indicated success. All of the 310 // sectors that are present in the storage obligation should already be on disk, 311 // which means that addStorageObligation should be exclusively called when 312 // creating a new, empty file contract or when renewing an existing file 313 // contract. 314 func (h *Host) managedAddStorageObligation(so storageObligation) error { 315 var soid types.FileContractID 316 err := func() error { 317 h.mu.Lock() 318 defer h.mu.Unlock() 319 320 // Sanity check - obligation should be under lock while being added. 321 soid = so.id() 322 _, exists := h.lockedStorageObligations[soid] 323 if !exists { 324 h.log.Critical("addStorageObligation called with an obligation that is not locked") 325 } 326 // Sanity check - There needs to be enough time left on the file contract 327 // for the host to safely submit the file contract revision. 328 if h.blockHeight+revisionSubmissionBuffer >= so.expiration() { 329 h.log.Critical("submission window was not verified before trying to submit a storage obligation") 330 return errNoBuffer 331 } 332 // Sanity check - the resubmission timeout needs to be smaller than storage 333 // proof window. 334 if so.expiration()+resubmissionTimeout >= so.proofDeadline() { 335 h.log.Critical("host is misconfigured - the storage proof window needs to be long enough to resubmit if needed") 336 return errors.New("fill me in") 337 } 338 339 // Add the storage obligation information to the database. 340 err := h.db.Update(func(tx *bolt.Tx) error { 341 // Sanity check - a storage obligation using the same file contract id 342 // should not already exist. This situation can happen if the 343 // transaction pool ejects a file contract and then a new one is 344 // created. Though the file contract will have the same terms, some 345 // other conditions might cause problems. The check for duplicate file 346 // contract ids should happen during the negotiation phase, and not 347 // during the 'addStorageObligation' phase. 348 bso := tx.Bucket(bucketStorageObligations) 349 350 // If the storage obligation already has sectors, it means that the 351 // file contract is being renewed, and that the sector should be 352 // re-added with a new expiration height. If there is an error at any 353 // point, all of the sectors should be removed. 354 if len(so.SectorRoots) != 0 { 355 err := h.AddSectorBatch(so.SectorRoots) 356 if err != nil { 357 return err 358 } 359 } 360 361 // Add the storage obligation to the database. 362 soBytes, err := json.Marshal(so) 363 if err != nil { 364 return err 365 } 366 return bso.Put(soid[:], soBytes) 367 }) 368 if err != nil { 369 return err 370 } 371 372 // Update the host financial metrics with regards to this storage 373 // obligation. 374 h.financialMetrics.ContractCount++ 375 h.financialMetrics.PotentialContractCompensation = h.financialMetrics.PotentialContractCompensation.Add(so.ContractCost) 376 h.financialMetrics.LockedStorageCollateral = h.financialMetrics.LockedStorageCollateral.Add(so.LockedCollateral) 377 h.financialMetrics.PotentialStorageRevenue = h.financialMetrics.PotentialStorageRevenue.Add(so.PotentialStorageRevenue) 378 h.financialMetrics.PotentialDownloadBandwidthRevenue = h.financialMetrics.PotentialDownloadBandwidthRevenue.Add(so.PotentialDownloadRevenue) 379 h.financialMetrics.PotentialUploadBandwidthRevenue = h.financialMetrics.PotentialUploadBandwidthRevenue.Add(so.PotentialUploadRevenue) 380 h.financialMetrics.RiskedStorageCollateral = h.financialMetrics.RiskedStorageCollateral.Add(so.RiskedCollateral) 381 h.financialMetrics.TransactionFeeExpenses = h.financialMetrics.TransactionFeeExpenses.Add(so.TransactionFeesAdded) 382 return nil 383 }() 384 if err != nil { 385 return err 386 } 387 388 // Check that the transaction is fully valid and submit it to the 389 // transaction pool. 390 err = h.tpool.AcceptTransactionSet(so.OriginTransactionSet) 391 if err != nil { 392 h.log.Println("Failed to add storage obligation, transaction set was not accepted:", err) 393 return err 394 } 395 396 // Queue the action items. 397 h.mu.Lock() 398 defer h.mu.Unlock() 399 400 // The file contract was already submitted to the blockchain, need to check 401 // after the resubmission timeout that it was submitted successfully. 402 err1 := h.queueActionItem(h.blockHeight+resubmissionTimeout, soid) 403 err2 := h.queueActionItem(h.blockHeight+resubmissionTimeout*2, soid) // Paranoia 404 // Queue an action item to submit the file contract revision - if there is 405 // never a file contract revision, the handling of this action item will be 406 // a no-op. 407 err3 := h.queueActionItem(so.expiration()-revisionSubmissionBuffer, soid) 408 err4 := h.queueActionItem(so.expiration()-revisionSubmissionBuffer+resubmissionTimeout, soid) // Paranoia 409 // The storage proof should be submitted 410 err5 := h.queueActionItem(so.expiration()+resubmissionTimeout, soid) 411 err6 := h.queueActionItem(so.expiration()+resubmissionTimeout*2, soid) // Paranoia 412 err = composeErrors(err1, err2, err3, err4, err5, err6) 413 if err != nil { 414 h.log.Println("Error with transaction set, redacting obligation, id", so.id()) 415 return composeErrors(err, h.removeStorageObligation(so, obligationRejected)) 416 } 417 return nil 418 } 419 420 // modifyStorageObligation will take an updated storage obligation along with a 421 // list of sector changes and update the database to account for all of it. The 422 // sector modifications are only used to update the sector database, they will 423 // not be used to modify the storage obligation (most importantly, this means 424 // that sectorRoots needs to be updated by the calling function). Virtual 425 // sectors will be removed the number of times that they are listed, to remove 426 // multiple instances of the same virtual sector, the virtural sector will need 427 // to appear in 'sectorsRemoved' multiple times. Same with 'sectorsGained'. 428 func (h *Host) modifyStorageObligation(so storageObligation, sectorsRemoved []crypto.Hash, sectorsGained []crypto.Hash, gainedSectorData [][]byte) error { 429 // Sanity check - obligation should be under lock while being modified. 430 soid := so.id() 431 _, exists := h.lockedStorageObligations[soid] 432 if !exists { 433 h.log.Critical("modifyStorageObligation called with an obligation that is not locked") 434 } 435 // Sanity check - there needs to be enough time to submit the file contract 436 // revision to the blockchain. 437 if so.expiration()-revisionSubmissionBuffer <= h.blockHeight { 438 return errNoBuffer 439 } 440 // Sanity check - sectorsGained and gainedSectorData need to have the same length. 441 if len(sectorsGained) != len(gainedSectorData) { 442 h.log.Critical("modifying a revision with garbage sector data", len(sectorsGained), len(gainedSectorData)) 443 return errInsaneStorageObligationRevision 444 } 445 // Sanity check - all of the sector data should be modules.SectorSize 446 for _, data := range gainedSectorData { 447 if uint64(len(data)) != modules.SectorSize { 448 h.log.Critical("modifying a revision with garbase sector sizes", len(data)) 449 return errInsaneStorageObligationRevision 450 } 451 } 452 453 // Note, for safe error handling, the operation order should be: add 454 // sectors, update database, remove sectors. If the adding or update fails, 455 // the added sectors should be removed and the storage obligation shoud be 456 // considered invalid. If the removing fails, this is okay, it's ignored 457 // and left to consistency checks and user actions to fix (will reduce host 458 // capacity, but will not inhibit the host's ability to submit storage 459 // proofs) 460 var i int 461 var err error 462 for i = range sectorsGained { 463 err = h.AddSector(sectorsGained[i], gainedSectorData[i]) 464 if err != nil { 465 break 466 } 467 } 468 if err != nil { 469 // Because there was an error, all of the sectors that got added need 470 // to be reverted. 471 for j := 0; j < i; j++ { 472 // Error is not checked because there's nothing useful that can be 473 // done about an error. 474 _ = h.RemoveSector(sectorsGained[j]) 475 } 476 return err 477 } 478 // Update the database to contain the new storage obligation. 479 var oldSO storageObligation 480 err = h.db.Update(func(tx *bolt.Tx) error { 481 // Get the old storage obligation as a reference to know how to upate 482 // the host financial stats. 483 oldSO, err = getStorageObligation(tx, soid) 484 if err != nil { 485 return err 486 } 487 488 // Store the new storage obligation to replace the old one. 489 return putStorageObligation(tx, so) 490 }) 491 if err != nil { 492 // Because there was an error, all of the sectors that got added need 493 // to be reverted. 494 for i := range sectorsGained { 495 // Error is not checked because there's nothing useful that can be 496 // done about an error. 497 _ = h.RemoveSector(sectorsGained[i]) 498 } 499 return err 500 } 501 // Call removeSector for all of the sectors that have been removed. 502 for k := range sectorsRemoved { 503 // Error is not checkeed because there's nothing useful that can be 504 // done about an error. Failing to remove a sector is not a terrible 505 // place to be, especially if the host can run consistency checks. 506 _ = h.RemoveSector(sectorsRemoved[k]) 507 } 508 509 // Update the financial information for the storage obligation - apply the 510 // new values. 511 h.financialMetrics.PotentialContractCompensation = h.financialMetrics.PotentialContractCompensation.Add(so.ContractCost) 512 h.financialMetrics.LockedStorageCollateral = h.financialMetrics.LockedStorageCollateral.Add(so.LockedCollateral) 513 h.financialMetrics.PotentialStorageRevenue = h.financialMetrics.PotentialStorageRevenue.Add(so.PotentialStorageRevenue) 514 h.financialMetrics.PotentialDownloadBandwidthRevenue = h.financialMetrics.PotentialDownloadBandwidthRevenue.Add(so.PotentialDownloadRevenue) 515 h.financialMetrics.PotentialUploadBandwidthRevenue = h.financialMetrics.PotentialUploadBandwidthRevenue.Add(so.PotentialUploadRevenue) 516 h.financialMetrics.RiskedStorageCollateral = h.financialMetrics.RiskedStorageCollateral.Add(so.RiskedCollateral) 517 h.financialMetrics.TransactionFeeExpenses = h.financialMetrics.TransactionFeeExpenses.Add(so.TransactionFeesAdded) 518 519 // Update the financial information for the storage obligation - remove the 520 // old values. 521 h.financialMetrics.PotentialContractCompensation = h.financialMetrics.PotentialContractCompensation.Sub(oldSO.ContractCost) 522 h.financialMetrics.LockedStorageCollateral = h.financialMetrics.LockedStorageCollateral.Sub(oldSO.LockedCollateral) 523 h.financialMetrics.PotentialStorageRevenue = h.financialMetrics.PotentialStorageRevenue.Sub(oldSO.PotentialStorageRevenue) 524 h.financialMetrics.PotentialDownloadBandwidthRevenue = h.financialMetrics.PotentialDownloadBandwidthRevenue.Sub(oldSO.PotentialDownloadRevenue) 525 h.financialMetrics.PotentialUploadBandwidthRevenue = h.financialMetrics.PotentialUploadBandwidthRevenue.Sub(oldSO.PotentialUploadRevenue) 526 h.financialMetrics.RiskedStorageCollateral = h.financialMetrics.RiskedStorageCollateral.Sub(oldSO.RiskedCollateral) 527 h.financialMetrics.TransactionFeeExpenses = h.financialMetrics.TransactionFeeExpenses.Sub(oldSO.TransactionFeesAdded) 528 return nil 529 } 530 531 // removeStorageObligation will remove a storage obligation from the host, 532 // either due to failure or success. 533 func (h *Host) removeStorageObligation(so storageObligation, sos storageObligationStatus) error { 534 // Error is not checked, we want to call remove on every sector even if 535 // there are problems - disk health information will be updated. 536 _ = h.RemoveSectorBatch(so.SectorRoots) 537 538 // Update the host revenue metrics based on the status of the obligation. 539 if sos == obligationUnresolved { 540 h.log.Critical("storage obligation 'unresolved' during call to removeStorageObligation, id", so.id()) 541 } 542 if sos == obligationRejected { 543 if h.financialMetrics.TransactionFeeExpenses.Cmp(so.TransactionFeesAdded) >= 0 { 544 h.financialMetrics.TransactionFeeExpenses = h.financialMetrics.TransactionFeeExpenses.Sub(so.TransactionFeesAdded) 545 546 // Remove the obligation statistics as potential risk and income. 547 h.log.Printf("Rejecting storage obligation expiring at block %v, current height is %v. Potential revenue is %v.\n", so.expiration(), h.blockHeight, h.financialMetrics.PotentialContractCompensation.Add(h.financialMetrics.PotentialStorageRevenue).Add(h.financialMetrics.PotentialDownloadBandwidthRevenue).Add(h.financialMetrics.PotentialUploadBandwidthRevenue)) 548 h.financialMetrics.PotentialContractCompensation = h.financialMetrics.PotentialContractCompensation.Sub(so.ContractCost) 549 h.financialMetrics.LockedStorageCollateral = h.financialMetrics.LockedStorageCollateral.Sub(so.LockedCollateral) 550 h.financialMetrics.PotentialStorageRevenue = h.financialMetrics.PotentialStorageRevenue.Sub(so.PotentialStorageRevenue) 551 h.financialMetrics.PotentialDownloadBandwidthRevenue = h.financialMetrics.PotentialDownloadBandwidthRevenue.Sub(so.PotentialDownloadRevenue) 552 h.financialMetrics.PotentialUploadBandwidthRevenue = h.financialMetrics.PotentialUploadBandwidthRevenue.Sub(so.PotentialUploadRevenue) 553 h.financialMetrics.RiskedStorageCollateral = h.financialMetrics.RiskedStorageCollateral.Sub(so.RiskedCollateral) 554 } 555 } 556 if sos == obligationSucceeded { 557 // Remove the obligation statistics as potential risk and income. 558 h.log.Printf("Successfully submitted a storage proof. Revenue is %v.\n", so.ContractCost.Add(so.PotentialStorageRevenue).Add(so.PotentialDownloadRevenue).Add(so.PotentialUploadRevenue)) 559 h.financialMetrics.PotentialContractCompensation = h.financialMetrics.PotentialContractCompensation.Sub(so.ContractCost) 560 h.financialMetrics.LockedStorageCollateral = h.financialMetrics.LockedStorageCollateral.Sub(so.LockedCollateral) 561 h.financialMetrics.PotentialStorageRevenue = h.financialMetrics.PotentialStorageRevenue.Sub(so.PotentialStorageRevenue) 562 h.financialMetrics.PotentialDownloadBandwidthRevenue = h.financialMetrics.PotentialDownloadBandwidthRevenue.Sub(so.PotentialDownloadRevenue) 563 h.financialMetrics.PotentialUploadBandwidthRevenue = h.financialMetrics.PotentialUploadBandwidthRevenue.Sub(so.PotentialUploadRevenue) 564 h.financialMetrics.RiskedStorageCollateral = h.financialMetrics.RiskedStorageCollateral.Sub(so.RiskedCollateral) 565 566 // Add the obligation statistics as actual income. 567 h.financialMetrics.ContractCompensation = h.financialMetrics.ContractCompensation.Add(so.ContractCost) 568 h.financialMetrics.StorageRevenue = h.financialMetrics.StorageRevenue.Add(so.PotentialStorageRevenue) 569 h.financialMetrics.DownloadBandwidthRevenue = h.financialMetrics.DownloadBandwidthRevenue.Add(so.PotentialDownloadRevenue) 570 h.financialMetrics.UploadBandwidthRevenue = h.financialMetrics.UploadBandwidthRevenue.Add(so.PotentialUploadRevenue) 571 } 572 if sos == obligationFailed { 573 // Remove the obligation statistics as potential risk and income. 574 h.log.Printf("Missed storage proof. Revenue would have been %v.\n", so.ContractCost.Add(so.PotentialStorageRevenue).Add(so.PotentialDownloadRevenue).Add(so.PotentialUploadRevenue)) 575 h.financialMetrics.PotentialContractCompensation = h.financialMetrics.PotentialContractCompensation.Sub(so.ContractCost) 576 h.financialMetrics.LockedStorageCollateral = h.financialMetrics.LockedStorageCollateral.Sub(so.LockedCollateral) 577 h.financialMetrics.PotentialStorageRevenue = h.financialMetrics.PotentialStorageRevenue.Sub(so.PotentialStorageRevenue) 578 h.financialMetrics.PotentialDownloadBandwidthRevenue = h.financialMetrics.PotentialDownloadBandwidthRevenue.Sub(so.PotentialDownloadRevenue) 579 h.financialMetrics.PotentialUploadBandwidthRevenue = h.financialMetrics.PotentialUploadBandwidthRevenue.Sub(so.PotentialUploadRevenue) 580 h.financialMetrics.RiskedStorageCollateral = h.financialMetrics.RiskedStorageCollateral.Sub(so.RiskedCollateral) 581 582 // Add the obligation statistics as loss. 583 h.financialMetrics.LostStorageCollateral = h.financialMetrics.LostStorageCollateral.Add(so.RiskedCollateral) 584 h.financialMetrics.LostRevenue = h.financialMetrics.LostRevenue.Add(so.ContractCost).Add(so.PotentialStorageRevenue).Add(so.PotentialDownloadRevenue).Add(so.PotentialUploadRevenue) 585 } 586 587 // Update the storage obligation to be finalized but still in-database. The 588 // obligation status is updated so that the user can see how the obligation 589 // ended up, and the sector roots are removed because they are large 590 // objects with little purpose once storage proofs are no longer needed. 591 h.financialMetrics.ContractCount-- 592 so.ObligationStatus = sos 593 so.SectorRoots = nil 594 return h.db.Update(func(tx *bolt.Tx) error { 595 return putStorageObligation(tx, so) 596 }) 597 } 598 599 // threadedHandleActionItem will look at a storage obligation and determine 600 // which action is necessary for the storage obligation to succeed. 601 func (h *Host) threadedHandleActionItem(soid types.FileContractID) { 602 err := h.tg.Add() 603 if err != nil { 604 return 605 } 606 defer h.tg.Done() 607 608 // Lock the storage obligation in question. 609 h.managedLockStorageObligation(soid) 610 defer func() { 611 h.managedUnlockStorageObligation(soid) 612 }() 613 614 // Fetch the storage obligation associated with the storage obligation id. 615 var so storageObligation 616 h.mu.RLock() 617 blockHeight := h.blockHeight 618 err = h.db.View(func(tx *bolt.Tx) error { 619 so, err = getStorageObligation(tx, soid) 620 return err 621 }) 622 h.mu.RUnlock() 623 if err != nil { 624 h.log.Println("Could not get storage obligation:", err) 625 return 626 } 627 628 // Check whether the storage obligation has already been completed. 629 if so.ObligationStatus != obligationUnresolved { 630 // Storage obligation has already been completed, skip action item. 631 return 632 } 633 634 // Check whether the file contract has been seen. If not, resubmit and 635 // queue another action item. Check for death. (signature should have a 636 // kill height) 637 if !so.OriginConfirmed { 638 // Submit the transaction set again, try to get the transaction 639 // confirmed. 640 err := h.tpool.AcceptTransactionSet(so.OriginTransactionSet) 641 if err != nil { 642 h.log.Debugln("Could not get origin transaction set accepted", err) 643 644 // Check if the transaction is invalid with the current consensus set. 645 // If so, the transaction is highly unlikely to ever be confirmed, and 646 // the storage obligation should be removed. This check should come 647 // after logging the errror so that the function can quit. 648 // 649 // TODO: If the host or tpool is behind consensus, might be difficult 650 // to have certainty about the issue. If some but not all of the 651 // parents are confirmed, might be some difficulty. 652 _, t := err.(modules.ConsensusConflict) 653 if t { 654 h.log.Println("Consensus conflict on the origin transaction set, id", so.id()) 655 h.mu.Lock() 656 err = h.removeStorageObligation(so, obligationRejected) 657 h.mu.Unlock() 658 if err != nil { 659 h.log.Println("Error removing storage obligation:", err) 660 } 661 return 662 } 663 } 664 665 // Queue another action item to check the status of the transaction. 666 h.mu.Lock() 667 err = h.queueActionItem(h.blockHeight+resubmissionTimeout, so.id()) 668 h.mu.Unlock() 669 if err != nil { 670 h.log.Println("Error queuing action item:", err) 671 } 672 } 673 674 // Check if the file contract revision is ready for submission. Check for death. 675 if !so.RevisionConfirmed && len(so.RevisionTransactionSet) > 0 && blockHeight >= so.expiration()-revisionSubmissionBuffer { 676 // Sanity check - there should be a file contract revision. 677 rtsLen := len(so.RevisionTransactionSet) 678 if rtsLen < 1 || len(so.RevisionTransactionSet[rtsLen-1].FileContractRevisions) != 1 { 679 h.log.Critical("transaction revision marked as unconfirmed, yet there is no transaction revision") 680 return 681 } 682 683 // Check if the revision has failed to submit correctly. 684 if blockHeight > so.expiration() { 685 // TODO: Check this error. 686 // 687 // TODO: this is not quite right, because a previous revision may 688 // be confirmed, and the origin transaction may be confirmed, which 689 // would confuse the revenue stuff a bit. Might happen frequently 690 // due to the dynamic fee pool. 691 h.log.Println("Full time has elapsed, but the revision transaction could not be submitted to consensus, id", so.id()) 692 h.mu.Lock() 693 h.removeStorageObligation(so, obligationRejected) 694 h.mu.Unlock() 695 return 696 } 697 698 // Queue another action item to check the status of the transaction. 699 h.mu.Lock() 700 err := h.queueActionItem(blockHeight+resubmissionTimeout, so.id()) 701 h.mu.Unlock() 702 if err != nil { 703 h.log.Println("Error queuing action item:", err) 704 } 705 706 // Add a miner fee to the transaction and submit it to the blockchain. 707 revisionTxnIndex := len(so.RevisionTransactionSet) - 1 708 revisionParents := so.RevisionTransactionSet[:revisionTxnIndex] 709 revisionTxn := so.RevisionTransactionSet[revisionTxnIndex] 710 builder := h.wallet.RegisterTransaction(revisionTxn, revisionParents) 711 _, feeRecommendation := h.tpool.FeeEstimation() 712 if so.value().Div64(2).Cmp(feeRecommendation) < 0 { 713 // There's no sense submitting the revision if the fee is more than 714 // half of the anticipated revenue - fee market went up 715 // unexpectedly, and the money that the renter paid to cover the 716 // fees is no longer enough. 717 return 718 } 719 txnSize := uint64(len(encoding.MarshalAll(so.RevisionTransactionSet)) + 300) 720 requiredFee := feeRecommendation.Mul64(txnSize) 721 err = builder.FundSiacoins(requiredFee) 722 if err != nil { 723 h.log.Println("Error funding transaction fees", err) 724 } 725 builder.AddMinerFee(requiredFee) 726 if err != nil { 727 h.log.Println("Error adding miner fees", err) 728 } 729 feeAddedRevisionTransactionSet, err := builder.Sign(true) 730 if err != nil { 731 h.log.Println("Error signing transaction", err) 732 } 733 err = h.tpool.AcceptTransactionSet(feeAddedRevisionTransactionSet) 734 if err != nil { 735 h.log.Println("Error submitting transaction to transaction pool", err) 736 } 737 so.TransactionFeesAdded = so.TransactionFeesAdded.Add(requiredFee) 738 // return 739 } 740 741 // Check whether a storage proof is ready to be provided, and whether it 742 // has been accepted. Check for death. 743 if !so.ProofConfirmed && blockHeight >= so.expiration()+resubmissionTimeout { 744 h.log.Debugln("Host is attempting a storage proof for", so.id()) 745 746 // If the window has closed, the host has failed and the obligation can 747 // be removed. 748 if so.proofDeadline() < blockHeight || len(so.SectorRoots) == 0 { 749 h.log.Debugln("storage proof not confirmed by deadline, id", so.id()) 750 h.mu.Lock() 751 err := h.removeStorageObligation(so, obligationFailed) 752 h.mu.Unlock() 753 if err != nil { 754 h.log.Println("Error removing storage obligation:", err) 755 } 756 return 757 } 758 759 // Get the index of the segment, and the index of the sector containing 760 // the segment. 761 segmentIndex, err := h.cs.StorageProofSegment(so.id()) 762 if err != nil { 763 h.log.Debugln("Host got an error when fetching a storage proof segment:", err) 764 return 765 } 766 sectorIndex := segmentIndex / (modules.SectorSize / crypto.SegmentSize) 767 // Pull the corresponding sector into memory. 768 sectorRoot := so.SectorRoots[sectorIndex] 769 sectorBytes, err := h.ReadSector(sectorRoot) 770 if err != nil { 771 h.log.Debugln(err) 772 return 773 } 774 775 // Build the storage proof for just the sector. 776 sectorSegment := segmentIndex % (modules.SectorSize / crypto.SegmentSize) 777 base, cachedHashSet := crypto.MerkleProof(sectorBytes, sectorSegment) 778 779 // Using the sector, build a cached root. 780 log2SectorSize := uint64(0) 781 for 1<<log2SectorSize < (modules.SectorSize / crypto.SegmentSize) { 782 log2SectorSize++ 783 } 784 ct := crypto.NewCachedTree(log2SectorSize) 785 ct.SetIndex(segmentIndex) 786 for _, root := range so.SectorRoots { 787 ct.Push(root) 788 } 789 hashSet := ct.Prove(base, cachedHashSet) 790 sp := types.StorageProof{ 791 ParentID: so.id(), 792 HashSet: hashSet, 793 } 794 copy(sp.Segment[:], base) 795 796 // Create and build the transaction with the storage proof. 797 builder := h.wallet.StartTransaction() 798 _, feeRecommendation := h.tpool.FeeEstimation() 799 if so.value().Cmp(feeRecommendation) < 0 { 800 // There's no sense submitting the storage proof if the fee is more 801 // than the anticipated revenue. 802 h.log.Debugln("Host not submitting storage proof due to a value that does not sufficiently exceed the fee cost") 803 return 804 } 805 txnSize := uint64(len(encoding.Marshal(sp)) + 300) 806 requiredFee := feeRecommendation.Mul64(txnSize) 807 err = builder.FundSiacoins(requiredFee) 808 if err != nil { 809 h.log.Println("Host error when funding a storage proof transaction fee:", err) 810 return 811 } 812 builder.AddMinerFee(requiredFee) 813 builder.AddStorageProof(sp) 814 storageProofSet, err := builder.Sign(true) 815 if err != nil { 816 h.log.Println("Host error when signing the storage proof transaction:", err) 817 return 818 } 819 err = h.tpool.AcceptTransactionSet(storageProofSet) 820 if err != nil { 821 h.log.Println("Host unable to submit storage proof transaction to transaction pool:", err) 822 return 823 } 824 so.TransactionFeesAdded = so.TransactionFeesAdded.Add(requiredFee) 825 826 // Queue another action item to check whether the storage proof 827 // got confirmed. 828 h.mu.Lock() 829 err = h.queueActionItem(so.proofDeadline(), so.id()) 830 h.mu.Unlock() 831 if err != nil { 832 h.log.Println("Error queuing action item:", err) 833 } 834 } 835 836 // Save the storage obligation to account for any fee changes. 837 err = h.db.Update(func(tx *bolt.Tx) error { 838 soBytes, err := json.Marshal(so) 839 if err != nil { 840 return err 841 } 842 return tx.Bucket(bucketStorageObligations).Put(soid[:], soBytes) 843 }) 844 if err != nil { 845 h.log.Println("Error updating the storage obligations", err) 846 } 847 848 // Check if all items have succeeded with the required confirmations. Report 849 // success, delete the obligation. 850 if so.ProofConfirmed && blockHeight >= so.proofDeadline() { 851 h.log.Println("file contract complete, id", so.id()) 852 h.mu.Lock() 853 h.removeStorageObligation(so, obligationSucceeded) 854 h.mu.Unlock() 855 } 856 } 857 858 // StorageObligations fetches the set of storage obligations in the host and 859 // returns metadata on them. 860 func (h *Host) StorageObligations() (sos []modules.StorageObligation) { 861 h.mu.RLock() 862 defer h.mu.RUnlock() 863 864 err := h.db.View(func(tx *bolt.Tx) error { 865 b := tx.Bucket(bucketStorageObligations) 866 err := b.ForEach(func(idBytes, soBytes []byte) error { 867 var so storageObligation 868 err := json.Unmarshal(soBytes, &so) 869 if err != nil { 870 return build.ExtendErr("unable to unmarshal storage obligation:", err) 871 } 872 mso := modules.StorageObligation{ 873 NegotiationHeight: so.NegotiationHeight, 874 875 OriginConfirmed: so.OriginConfirmed, 876 RevisionConstructed: so.RevisionConstructed, 877 RevisionConfirmed: so.RevisionConfirmed, 878 ProofConstructed: so.ProofConstructed, 879 ProofConfirmed: so.ProofConfirmed, 880 ObligationStatus: uint64(so.ObligationStatus), 881 } 882 sos = append(sos, mso) 883 return nil 884 }) 885 if err != nil { 886 return build.ExtendErr("ForEach failed to get next storage obligation:", err) 887 } 888 return nil 889 }) 890 if err != nil { 891 h.log.Println(build.ExtendErr("database failed to provide storage obligations:", err)) 892 } 893 894 return sos 895 }