github.com/johnathanhowell/sia@v0.5.1-beta.0.20160524050156-83dcc3d37c94/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/NebulousLabs/bolt" 42 ) 43 44 const ( 45 obligationConfused 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 // errObligationLocked is returned when a storage obligation is being put 93 // under lock, but is already locked. 94 errObligationLocked = errors.New("storage obligation is locked, and is unavailable for editing") 95 96 // errObligationUnlocked is returned when a storage obligation is being 97 // removed from lock, but is already unlocked. 98 errObligationUnlocked = errors.New("storage obligation is unlocked, and should not be getting unlocked") 99 100 // errNoBuffer is returned if there is an attempted storage obligation that 101 // needs to have the storage proof submitted in less than 102 // revisionSubmissionBuffer blocks. 103 errNoBuffer = errors.New("file contract rejected because storage proof window is too close") 104 105 // errNoStorageObligation is returned if the requested storage obligation 106 // is not found in the database. 107 errNoStorageObligation = errors.New("storage obligation not found in database") 108 ) 109 110 type storageObligationStatus int 111 112 // storageObligation contains all of the metadata related to a file contract 113 // and the storage contained by the file contract. 114 type storageObligation struct { 115 // Storage obligations are broken up into ordered atomic sectors that are 116 // exactly 4MiB each. By saving the roots of each sector, storage proofs 117 // and modifications to the data can be made inexpensively by making use of 118 // the merkletree.CachedTree. Sectors can be appended, modified, or deleted 119 // and the host can recompute the Merkle root of the whole file without 120 // much computational or I/O expense. 121 SectorRoots []crypto.Hash 122 123 // Variables about the file contract that enforces the storage obligation. 124 // The origin an revision transaction are stored as a set, where the set 125 // contains potentially unconfirmed transactions. 126 ContractCost types.Currency 127 LockedCollateral types.Currency 128 PotentialDownloadRevenue types.Currency 129 PotentialStorageRevenue types.Currency 130 PotentialUploadRevenue types.Currency 131 RiskedCollateral types.Currency 132 TransactionFeesAdded types.Currency 133 134 OriginTransactionSet []types.Transaction 135 RevisionTransactionSet []types.Transaction 136 137 // Variables indicating whether the critical transactions in a storage 138 // obligation have been confirmed on the blockchain. 139 OriginConfirmed bool 140 RevisionConfirmed bool 141 ProofConfirmed bool 142 } 143 144 // getStorageObligation fetches a storage obligation from the database tx. 145 func getStorageObligation(tx *bolt.Tx, soid types.FileContractID) (so storageObligation, err error) { 146 soBytes := tx.Bucket(bucketStorageObligations).Get(soid[:]) 147 if soBytes == nil { 148 return storageObligation{}, errNoStorageObligation 149 } 150 err = json.Unmarshal(soBytes, &so) 151 if err != nil { 152 return storageObligation{}, err 153 } 154 return so, nil 155 } 156 157 // putStorageObligation places a storage obligation into the database, 158 // overwriting the existing storage obligation if there is one. 159 func putStorageObligation(tx *bolt.Tx, so storageObligation) error { 160 soBytes, err := json.Marshal(so) 161 if err != nil { 162 return err 163 } 164 soid := so.id() 165 return tx.Bucket(bucketStorageObligations).Put(soid[:], soBytes) 166 } 167 168 // expiration returns the height at which the storage obligation expires. 169 func (so *storageObligation) expiration() types.BlockHeight { 170 if len(so.RevisionTransactionSet) > 0 { 171 return so.RevisionTransactionSet[len(so.RevisionTransactionSet)-1].FileContractRevisions[0].NewWindowStart 172 } 173 return so.OriginTransactionSet[len(so.OriginTransactionSet)-1].FileContracts[0].WindowStart 174 } 175 176 // fileSize returns the size of the data protected by the obligation. 177 func (so *storageObligation) fileSize() uint64 { 178 if len(so.RevisionTransactionSet) > 0 { 179 return so.RevisionTransactionSet[len(so.RevisionTransactionSet)-1].FileContractRevisions[0].NewFileSize 180 } 181 return so.OriginTransactionSet[len(so.OriginTransactionSet)-1].FileContracts[0].FileSize 182 } 183 184 // id returns the id of the storage obligation, which is definied by the file 185 // contract id of the file contract that governs the storage contract. 186 func (so *storageObligation) id() types.FileContractID { 187 return so.OriginTransactionSet[len(so.OriginTransactionSet)-1].FileContractID(0) 188 } 189 190 // isSane checks that required assumptions about the storage obligation are 191 // correct. 192 func (so *storageObligation) isSane() error { 193 // There should be an origin transaction set. 194 if len(so.OriginTransactionSet) == 0 { 195 build.Critical("origin transaction set is empty") 196 return errInsaneOriginSetSize 197 } 198 199 // The final transaction of the origin transaction set should have one file 200 // contract. 201 final := len(so.OriginTransactionSet) - 1 202 fcCount := len(so.OriginTransactionSet[final].FileContracts) 203 if fcCount != 1 { 204 build.Critical("wrong number of file contracts associated with storage obligation:", fcCount) 205 return errInsaneOriginSetFileContract 206 } 207 208 // The file contract in the final transaction of the origin transaction set 209 // should have two valid proof outputs and two missed proof outputs. 210 lenVPOs := len(so.OriginTransactionSet[final].FileContracts[0].ValidProofOutputs) 211 lenMPOs := len(so.OriginTransactionSet[final].FileContracts[0].MissedProofOutputs) 212 if lenVPOs != 2 || lenMPOs != 2 { 213 build.Critical("file contract has wrong number of VPOs and MPOs, expecting 2 each:", lenVPOs, lenMPOs) 214 return errInsaneFileContractOutputCounts 215 } 216 217 // If there is a revision transaction set, there should be one file 218 // contract revision in the final transaction. 219 if len(so.RevisionTransactionSet) > 0 { 220 final = len(so.OriginTransactionSet) - 1 221 fcrCount := len(so.OriginTransactionSet[final].FileContractRevisions) 222 if fcrCount != 1 { 223 build.Critical("wrong number of file contract revisions in final transaction of revision transaction set:", fcrCount) 224 return errInsaneRevisionSetRevisionCount 225 } 226 227 // The file contract revision in the final transaction of the revision 228 // transaction set should have two valid proof outputs and two missed 229 // proof outputs. 230 lenVPOs = len(so.RevisionTransactionSet[final].FileContractRevisions[0].NewValidProofOutputs) 231 lenMPOs = len(so.RevisionTransactionSet[final].FileContractRevisions[0].NewMissedProofOutputs) 232 if lenVPOs != 2 || lenMPOs != 2 { 233 build.Critical("file contract has wrong number of VPOs and MPOs, expecting 2 each:", lenVPOs, lenMPOs) 234 return errInsaneFileContractRevisionOutputCounts 235 } 236 } 237 return nil 238 } 239 240 // merkleRoot returns the file merkle root of a storage obligation. 241 func (so *storageObligation) merkleRoot() crypto.Hash { 242 if len(so.RevisionTransactionSet) > 0 { 243 return so.RevisionTransactionSet[len(so.RevisionTransactionSet)-1].FileContractRevisions[0].NewFileMerkleRoot 244 } 245 return so.OriginTransactionSet[len(so.OriginTransactionSet)-1].FileContracts[0].FileMerkleRoot 246 } 247 248 // payous returns the set of valid payouts and missed payouts that represent 249 // the latest revision for the storage obligation. 250 func (so *storageObligation) payouts() (valid []types.SiacoinOutput, missed []types.SiacoinOutput) { 251 valid = make([]types.SiacoinOutput, 2) 252 missed = make([]types.SiacoinOutput, 2) 253 if len(so.RevisionTransactionSet) > 0 { 254 copy(valid, so.RevisionTransactionSet[len(so.RevisionTransactionSet)-1].FileContractRevisions[0].NewValidProofOutputs) 255 copy(missed, so.RevisionTransactionSet[len(so.RevisionTransactionSet)-1].FileContractRevisions[0].NewMissedProofOutputs) 256 return 257 } 258 copy(valid, so.OriginTransactionSet[len(so.OriginTransactionSet)-1].FileContracts[0].ValidProofOutputs) 259 copy(missed, so.OriginTransactionSet[len(so.OriginTransactionSet)-1].FileContracts[0].MissedProofOutputs) 260 return 261 } 262 263 // proofDeadline returns the height by which the storage proof must be 264 // submitted. 265 func (so *storageObligation) proofDeadline() types.BlockHeight { 266 if len(so.RevisionTransactionSet) > 0 { 267 return so.RevisionTransactionSet[len(so.RevisionTransactionSet)-1].FileContractRevisions[0].NewWindowEnd 268 } 269 return so.OriginTransactionSet[len(so.OriginTransactionSet)-1].FileContracts[0].WindowEnd 270 } 271 272 // value returns the value of fulfilling the storage obligation to the host. 273 func (so *storageObligation) value() types.Currency { 274 return so.ContractCost.Add(so.PotentialDownloadRevenue).Add(so.PotentialStorageRevenue).Add(so.PotentialUploadRevenue).Add(so.RiskedCollateral) 275 } 276 277 // queueActionItem adds an action item to the host at the input height so that 278 // the host knows to perform maintenance on the associated storage obligation 279 // when that height is reached. 280 func (h *Host) queueActionItem(height types.BlockHeight, id types.FileContractID) error { 281 // Sanity check - action item should be at a higher height than the current 282 // block height. 283 if height <= h.blockHeight { 284 h.log.Critical("action item queued improperly") 285 } 286 return h.db.Update(func(tx *bolt.Tx) error { 287 // Translate the height into a byte slice. 288 heightBytes := make([]byte, 8) 289 binary.BigEndian.PutUint64(heightBytes, uint64(height)) 290 291 // Get the list of action items already at this height and extend it. 292 bai := tx.Bucket(bucketActionItems) 293 existingItems := bai.Get(heightBytes) 294 var extendedItems = make([]byte, len(existingItems), len(existingItems)+len(id[:])) 295 copy(extendedItems, existingItems) 296 extendedItems = append(extendedItems, id[:]...) 297 return bai.Put(heightBytes, extendedItems) 298 }) 299 } 300 301 // addStorageObligation adds a storage obligation to the host. Because this 302 // operation can return errors, the transactions should not be submitted to the 303 // blockchain until after this function has indicated success. All of the 304 // sectors that are present in the storage obligation should already be on 305 // disk, which means that addStorageObligation should be exclusively called 306 // when creating a new, empty file contract or when renewing an existing file 307 // contract. 308 func (h *Host) addStorageObligation(so *storageObligation) error { 309 // Sanity check - obligation should be under lock while being added. 310 soid := so.id() 311 _, exists := h.lockedStorageObligations[soid] 312 if !exists { 313 h.log.Critical("addStorageObligation called with an obligation that is not locked") 314 } 315 // Sanity check - There needs to be enough time left on the file contract 316 // for the host to safely submit the file contract revision. 317 if h.blockHeight+revisionSubmissionBuffer >= so.expiration() { 318 h.log.Critical("submission window was not verified before trying to submit a storage obligation") 319 return errNoBuffer 320 } 321 // Sanity check - the resubmission timeout needs to be smaller than storage 322 // proof window. 323 if so.expiration()+resubmissionTimeout >= so.proofDeadline() { 324 h.log.Critical("host is misconfigured - the storage proof window needs to be long enough to resubmit if needed") 325 return errors.New("fill me in") 326 } 327 328 // Add the storage obligation information to the database. 329 err := h.db.Update(func(tx *bolt.Tx) error { 330 // Sanity check - a storage obligation using the same file contract id 331 // should not already exist. This situation can happen if the 332 // transaction pool ejects a file contract and then a new one is 333 // created. Though the file contract will have the same terms, some 334 // other conditions might cause problems. The check for duplicate file 335 // contract ids should happen during the negotiation phase, and not 336 // during the 'addStorageObligation' phase. 337 bso := tx.Bucket(bucketStorageObligations) 338 soBytes := bso.Get(soid[:]) 339 if soBytes != nil { 340 h.log.Critical("host already has a save storage obligation for this file contract") 341 return errDuplicateStorageObligation 342 } 343 344 // If the storage obligation already has sectors, it means that the 345 // file contract is being renewed, and that the sector should be 346 // re-added with a new expriation height. If there is an error at any 347 // point, all of the sectors should be removed. 348 for i, root := range so.SectorRoots { 349 err := h.AddSector(root, so.expiration(), make([]byte, modules.SectorSize)) 350 if err != nil { 351 // Remove all of the sectors that got added and return an error. 352 for j := 0; j < i; j++ { 353 removeErr := h.RemoveSector(so.SectorRoots[j], so.expiration()) 354 if removeErr != nil { 355 h.log.Println(removeErr) 356 } 357 } 358 return err 359 } 360 } 361 362 // Add the storage obligation to the database. 363 soBytes, err := json.Marshal(*so) 364 if err != nil { 365 return err 366 } 367 return bso.Put(soid[:], soBytes) 368 }) 369 if err != nil { 370 return err 371 } 372 373 // Update the host financial metrics with regards to this storage 374 // obligation. 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 383 // Set an action item that will have the host verify that the file contract 384 // has been submitted to the blockchain, then another to submit the file 385 // contract revision to the blockchain, and another to submit the storage 386 // proof. 387 err0 := h.tpool.AcceptTransactionSet(so.OriginTransactionSet) 388 // The file contract was already submitted to the blockchain, need to check 389 // after the resubmission timeout that it was submitted successfully. 390 err1 := h.queueActionItem(h.blockHeight+resubmissionTimeout, soid) 391 // Queue an action item to submit the file contract revision - if there is 392 // never a file contract revision, the handling of this action item will be 393 // a no-op. 394 err2 := h.queueActionItem(so.expiration()-revisionSubmissionBuffer, soid) 395 // The storage proof should be submitted 396 err3 := h.queueActionItem(so.expiration()+resubmissionTimeout, soid) 397 err = composeErrors(err0, err1, err2, err3) 398 if err != nil { 399 return composeErrors(err, h.removeStorageObligation(so, obligationRejected)) 400 } 401 return nil 402 } 403 404 // lockStorageObligation puts a storage obligation under lock in the host. 405 func (h *Host) lockStorageObligation(so *storageObligation) error { 406 _, exists := h.lockedStorageObligations[so.id()] 407 if exists { 408 return errObligationLocked 409 } 410 h.lockedStorageObligations[so.id()] = struct{}{} 411 return nil 412 } 413 414 // modifyStorageObligation will take an updated storage obligation along with a 415 // list of sector changes and update the database to account for all of it. The 416 // sector modifications are only used to update the sector database, they will 417 // not be used to modify the storage obligation (most importantly, this means 418 // that sectorRoots needs to be updated by the calling function). Virtual 419 // sectors will be removed the number of times that they are listed, to remove 420 // multiple instances of the same virtual sector, the virtural sector will need 421 // to appear in 'sectorsRemoved' multiple times. Same with 'sectorsGained'. 422 func (h *Host) modifyStorageObligation(so *storageObligation, sectorsRemoved []crypto.Hash, sectorsGained []crypto.Hash, gainedSectorData [][]byte) error { 423 // Sanity check - obligation should be under lock while being modified. 424 soid := so.id() 425 _, exists := h.lockedStorageObligations[soid] 426 if !exists { 427 h.log.Critical("modifyStorageObligation called with an obligation that is not locked") 428 } 429 // Sanity check - there needs to be enough time to submit the file contract 430 // revision to the blockchain. 431 if so.expiration()-revisionSubmissionBuffer <= h.blockHeight { 432 h.log.Critical("revision submission window was not verified before trying to modify a storage obligation") 433 return errNoBuffer 434 } 435 // Sanity check - sectorsGained and gainedSectorData need to have the same length. 436 if len(sectorsGained) != len(gainedSectorData) { 437 h.log.Critical("modifying a revision with garbage sector data", len(sectorsGained), len(gainedSectorData)) 438 return errInsaneStorageObligationRevision 439 } 440 // Sanity check - all of the sector data should be modules.SectorSize 441 for _, data := range gainedSectorData { 442 if uint64(len(data)) != modules.SectorSize { 443 h.log.Critical("modifying a revision with garbase sector sizes", len(data)) 444 return errInsaneStorageObligationRevision 445 } 446 } 447 448 // Note, for safe error handling, the operation order should be: add 449 // sectors, update database, remove sectors. If the adding or update fails, 450 // the added sectors should be removed and the storage obligation shoud be 451 // considered invalid. If the removing fails, this is okay, it's ignored 452 // and left to consistency checks and user actions to fix (will reduce host 453 // capacity, but will not inhibit the host's ability to submit storage 454 // proofs) 455 var i int 456 var err error 457 for i = range sectorsGained { 458 err = h.AddSector(sectorsGained[i], so.expiration(), gainedSectorData[i]) 459 if err != nil { 460 break 461 } 462 } 463 if err != nil { 464 // Because there was an error, all of the sectors that got added need 465 // to be reverted. 466 for j := 0; j < i; j++ { 467 // Error is not checked because there's nothing useful that can be 468 // done about an error. 469 _ = h.RemoveSector(sectorsGained[j], so.expiration()) 470 } 471 return err 472 } 473 // Update the database to contain the new storage obligation. 474 var oldSO storageObligation 475 err = h.db.Update(func(tx *bolt.Tx) error { 476 // Get the old storage obligation as a reference to know how to upate 477 // the host financial stats. 478 oldSO, err = getStorageObligation(tx, soid) 479 if err != nil { 480 return err 481 } 482 483 // Store the new storage obligation to replace the old one. 484 return putStorageObligation(tx, *so) 485 }) 486 if err != nil { 487 // Because there was an error, all of the sectors that got added need 488 // to be reverted. 489 for i := range sectorsGained { 490 // Error is not checked because there's nothing useful that can be 491 // done about an error. 492 _ = h.RemoveSector(sectorsGained[i], so.expiration()) 493 } 494 return err 495 } 496 // Call removeSector for all of the sectors that have been removed. 497 for k := range sectorsRemoved { 498 // Error is not checkeed because there's nothing useful that can be 499 // done about an error. Failing to remove a sector is not a terrible 500 // place to be, especially if the host can run consistency checks. 501 _ = h.RemoveSector(sectorsRemoved[k], so.expiration()) 502 } 503 504 // Update the financial information for the storage obligation - remove the 505 // old values. 506 h.financialMetrics.PotentialContractCompensation = h.financialMetrics.PotentialContractCompensation.Sub(oldSO.ContractCost) 507 h.financialMetrics.LockedStorageCollateral = h.financialMetrics.LockedStorageCollateral.Sub(oldSO.LockedCollateral) 508 h.financialMetrics.PotentialStorageRevenue = h.financialMetrics.PotentialStorageRevenue.Sub(oldSO.PotentialStorageRevenue) 509 h.financialMetrics.PotentialDownloadBandwidthRevenue = h.financialMetrics.PotentialDownloadBandwidthRevenue.Sub(oldSO.PotentialDownloadRevenue) 510 h.financialMetrics.PotentialUploadBandwidthRevenue = h.financialMetrics.PotentialUploadBandwidthRevenue.Sub(oldSO.PotentialUploadRevenue) 511 h.financialMetrics.RiskedStorageCollateral = h.financialMetrics.RiskedStorageCollateral.Sub(oldSO.RiskedCollateral) 512 h.financialMetrics.TransactionFeeExpenses = h.financialMetrics.TransactionFeeExpenses.Sub(oldSO.TransactionFeesAdded) 513 514 // Update the financial information for the storage obligation - apply the 515 // new values. 516 h.financialMetrics.PotentialContractCompensation = h.financialMetrics.PotentialContractCompensation.Add(so.ContractCost) 517 h.financialMetrics.LockedStorageCollateral = h.financialMetrics.LockedStorageCollateral.Add(so.LockedCollateral) 518 h.financialMetrics.PotentialStorageRevenue = h.financialMetrics.PotentialStorageRevenue.Add(so.PotentialStorageRevenue) 519 h.financialMetrics.PotentialDownloadBandwidthRevenue = h.financialMetrics.PotentialDownloadBandwidthRevenue.Add(so.PotentialDownloadRevenue) 520 h.financialMetrics.PotentialUploadBandwidthRevenue = h.financialMetrics.PotentialUploadBandwidthRevenue.Add(so.PotentialUploadRevenue) 521 h.financialMetrics.RiskedStorageCollateral = h.financialMetrics.RiskedStorageCollateral.Add(so.RiskedCollateral) 522 h.financialMetrics.TransactionFeeExpenses = h.financialMetrics.TransactionFeeExpenses.Add(so.TransactionFeesAdded) 523 return nil 524 } 525 526 // removeStorageObligation will remove a storage obligation from the host, 527 // either due to failure or success. 528 func (h *Host) removeStorageObligation(so *storageObligation, sos storageObligationStatus) error { 529 // Call removeSector for every sector in the storage obligation. 530 for _, root := range so.SectorRoots { 531 // Error is not checked, we want to call remove on every sector even if 532 // there are problems - disk health information will be updated. 533 _ = h.RemoveSector(root, so.expiration()) 534 } 535 536 // Update the host revenue metrics based on the status of the obligation. 537 if sos == obligationConfused { 538 h.log.Critical("storage obligation confused!") 539 } 540 if sos == obligationRejected { 541 // Remove the obligation statistics as potential risk and income. 542 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)) 543 h.financialMetrics.PotentialContractCompensation = h.financialMetrics.PotentialContractCompensation.Sub(so.ContractCost) 544 h.financialMetrics.LockedStorageCollateral = h.financialMetrics.LockedStorageCollateral.Sub(so.LockedCollateral) 545 h.financialMetrics.PotentialStorageRevenue = h.financialMetrics.PotentialStorageRevenue.Sub(so.PotentialStorageRevenue) 546 h.financialMetrics.PotentialDownloadBandwidthRevenue = h.financialMetrics.PotentialDownloadBandwidthRevenue.Sub(so.PotentialDownloadRevenue) 547 h.financialMetrics.PotentialUploadBandwidthRevenue = h.financialMetrics.PotentialUploadBandwidthRevenue.Sub(so.PotentialUploadRevenue) 548 h.financialMetrics.RiskedStorageCollateral = h.financialMetrics.RiskedStorageCollateral.Sub(so.RiskedCollateral) 549 h.financialMetrics.TransactionFeeExpenses = h.financialMetrics.TransactionFeeExpenses.Sub(so.TransactionFeesAdded) 550 } 551 if sos == obligationSucceeded { 552 // Remove the obligation statistics as potential risk and income. 553 h.log.Printf("Succesfully submitted a storage proof. Revenue is %v.\n", h.financialMetrics.PotentialContractCompensation.Add(h.financialMetrics.PotentialStorageRevenue).Add(h.financialMetrics.PotentialDownloadBandwidthRevenue).Add(h.financialMetrics.PotentialUploadBandwidthRevenue)) 554 h.financialMetrics.PotentialContractCompensation = h.financialMetrics.PotentialContractCompensation.Sub(so.ContractCost) 555 h.financialMetrics.LockedStorageCollateral = h.financialMetrics.LockedStorageCollateral.Sub(so.LockedCollateral) 556 h.financialMetrics.PotentialStorageRevenue = h.financialMetrics.PotentialStorageRevenue.Sub(so.PotentialStorageRevenue) 557 h.financialMetrics.PotentialDownloadBandwidthRevenue = h.financialMetrics.PotentialDownloadBandwidthRevenue.Sub(so.PotentialDownloadRevenue) 558 h.financialMetrics.PotentialUploadBandwidthRevenue = h.financialMetrics.PotentialUploadBandwidthRevenue.Sub(so.PotentialUploadRevenue) 559 h.financialMetrics.RiskedStorageCollateral = h.financialMetrics.RiskedStorageCollateral.Sub(so.RiskedCollateral) 560 561 // Add the obligation statistics as actual income. 562 h.financialMetrics.ContractCompensation = h.financialMetrics.ContractCompensation.Add(so.ContractCost) 563 h.financialMetrics.StorageRevenue = h.financialMetrics.StorageRevenue.Add(so.PotentialStorageRevenue) 564 h.financialMetrics.DownloadBandwidthRevenue = h.financialMetrics.DownloadBandwidthRevenue.Add(so.PotentialDownloadRevenue) 565 h.financialMetrics.UploadBandwidthRevenue = h.financialMetrics.UploadBandwidthRevenue.Add(so.PotentialUploadRevenue) 566 } 567 if sos == obligationFailed { 568 // Remove the obligation statistics as potential risk and income. 569 h.log.Printf("Missed storage proof. Revenue would have been %v.\n", h.financialMetrics.PotentialContractCompensation.Add(h.financialMetrics.PotentialStorageRevenue).Add(h.financialMetrics.PotentialDownloadBandwidthRevenue).Add(h.financialMetrics.PotentialUploadBandwidthRevenue)) 570 h.financialMetrics.PotentialContractCompensation = h.financialMetrics.PotentialContractCompensation.Sub(so.ContractCost) 571 h.financialMetrics.LockedStorageCollateral = h.financialMetrics.LockedStorageCollateral.Sub(so.LockedCollateral) 572 h.financialMetrics.PotentialStorageRevenue = h.financialMetrics.PotentialStorageRevenue.Sub(so.PotentialStorageRevenue) 573 h.financialMetrics.PotentialDownloadBandwidthRevenue = h.financialMetrics.PotentialDownloadBandwidthRevenue.Sub(so.PotentialDownloadRevenue) 574 h.financialMetrics.PotentialUploadBandwidthRevenue = h.financialMetrics.PotentialUploadBandwidthRevenue.Sub(so.PotentialUploadRevenue) 575 h.financialMetrics.RiskedStorageCollateral = h.financialMetrics.RiskedStorageCollateral.Sub(so.RiskedCollateral) 576 577 // Add the obligation statistics as loss. 578 h.financialMetrics.LostStorageCollateral = h.financialMetrics.LostStorageCollateral.Add(so.RiskedCollateral) 579 h.financialMetrics.LostRevenue = h.financialMetrics.LostRevenue.Add(so.ContractCost).Add(so.PotentialStorageRevenue).Add(so.PotentialDownloadRevenue).Add(so.PotentialUploadRevenue) 580 } 581 582 // Delete the storage obligation from the database. 583 return h.db.Update(func(tx *bolt.Tx) error { 584 soid := so.id() 585 return tx.Bucket(bucketStorageObligations).Delete(soid[:]) 586 }) 587 } 588 589 // handleActionItem will look at a storage obligation and determine which 590 // action is necessary for the storage obligation to succeed. 591 func (h *Host) handleActionItem(so *storageObligation) { 592 // Check whether the file contract has been seen. If not, resubmit and 593 // queue another action item. Check for death. (signature should have a 594 // kill height) 595 if !so.OriginConfirmed { 596 // Submit the transaction set again, try to get the transaction 597 // confirmed. 598 err := h.tpool.AcceptTransactionSet(so.OriginTransactionSet) 599 if err != nil { 600 h.log.Debugln("Could not get origin transaction set accepted", err) 601 602 // Check if the transaction is invalid with the current consensus set. 603 // If so, the transaction is highly unlikely to ever be confirmed, and 604 // the storage obligation should be removed. This check should come 605 // after logging the errror so that the function can quit. 606 // 607 // TODO: If the host or tpool is behind consensus, might be difficult 608 // to have certainty about the issue. If some but not all of the 609 // parents are confirmed, might be some difficulty. 610 _, t := err.(modules.ConsensusConflict) 611 if t { 612 h.log.Debugln("Consensus conflict on the origin transaction set") 613 err = h.removeStorageObligation(so, obligationRejected) 614 if err != nil { 615 h.log.Println("Error removing storage obligation:", err) 616 } 617 return 618 } 619 } 620 621 // Queue another action item to check the status of the transaction. 622 err = h.queueActionItem(h.blockHeight+resubmissionTimeout, so.id()) 623 if err != nil { 624 h.log.Println("Error queuing action item:", err) 625 } 626 } 627 628 // Check if the file contract revision is ready for submission. Check for death. 629 if !so.RevisionConfirmed && len(so.RevisionTransactionSet) > 0 && h.blockHeight > so.expiration()-revisionSubmissionBuffer { 630 // Sanity check - there should be a file contract revision. 631 rtsLen := len(so.RevisionTransactionSet) 632 if rtsLen < 1 || len(so.RevisionTransactionSet[rtsLen-1].FileContractRevisions) != 1 { 633 h.log.Critical("transaction revision marked as unconfirmed, yet there is no transaction revision") 634 return 635 } 636 637 // Check if the revision has failed to submit correctly. 638 if h.blockHeight > so.expiration() { 639 // TODO: Check this error. 640 // 641 // TODO: this is not quite right, because a previous revision may 642 // be confirmed, and the origin transaction may be confirmed, which 643 // would confuse the revenue stuff a bit. Might happen frequently 644 // due to the dynamic fee pool. 645 h.log.Debugln("Full time has elapsed, but the revision transaction could not be submitted to consensus") 646 h.removeStorageObligation(so, obligationRejected) 647 return 648 } 649 650 // Queue another action item to check the status of the transaction. 651 err := h.queueActionItem(h.blockHeight+resubmissionTimeout, so.id()) 652 if err != nil { 653 h.log.Println("Error queuing action item:", err) 654 } 655 656 // Add a miner fee to the transaction and submit it to the blockchain. 657 revisionTxnIndex := len(so.RevisionTransactionSet) - 1 658 revisionParents := so.RevisionTransactionSet[:revisionTxnIndex] 659 revisionTxn := so.RevisionTransactionSet[revisionTxnIndex] 660 builder := h.wallet.RegisterTransaction(revisionTxn, revisionParents) 661 feeRecommendation, _ := h.tpool.FeeEstimation() 662 if so.value().Div64(2).Cmp(feeRecommendation) < 0 { 663 // There's no sense submitting the revision if the fee is more than 664 // half of the anticipated revenue - fee market went up 665 // unexpectedly, and the money that the renter paid to cover the 666 // fees is no longer enough. 667 return 668 } 669 txnSize := uint64(len(encoding.MarshalAll(so.RevisionTransactionSet)) + 300) 670 requiredFee := feeRecommendation.Mul64(txnSize) 671 err = builder.FundSiacoins(requiredFee) 672 if err != nil { 673 h.log.Println("Error funding transaction fees", err) 674 } 675 builder.AddMinerFee(requiredFee) 676 if err != nil { 677 h.log.Println("Error adding miner fees", err) 678 } 679 feeAddedRevisionTransactionSet, err := builder.Sign(true) 680 if err != nil { 681 h.log.Println("Error signing transaction", err) 682 } 683 err = h.tpool.AcceptTransactionSet(feeAddedRevisionTransactionSet) 684 if err != nil { 685 h.log.Println("Error submitting transaction to transaction pool", err) 686 } 687 so.TransactionFeesAdded = so.TransactionFeesAdded.Add(requiredFee) 688 // return 689 } 690 691 // Check whether a storage proof is ready to be provided, and whether it 692 // has been accepted. Check for death. 693 if !so.ProofConfirmed && h.blockHeight >= so.expiration()+resubmissionTimeout { 694 // If the window has closed, the host has failed and the obligation can 695 // be removed. 696 if so.proofDeadline() < h.blockHeight || len(so.SectorRoots) == 0 { 697 err := h.removeStorageObligation(so, obligationFailed) 698 if err != nil { 699 h.log.Println("Error removing storage obligation:", err) 700 } 701 return 702 } 703 704 // Get the index of the segment, and the index of the sector containing 705 // the segment. 706 segmentIndex, err := h.cs.StorageProofSegment(so.id()) 707 if err != nil { 708 return 709 } 710 sectorIndex := segmentIndex / (modules.SectorSize / crypto.SegmentSize) 711 // Pull the corresponding sector into memory. 712 sectorRoot := so.SectorRoots[sectorIndex] 713 sectorBytes, err := h.ReadSector(sectorRoot) 714 if err != nil { 715 return 716 } 717 718 // Build the storage proof for just the sector. 719 sectorSegment := segmentIndex % (modules.SectorSize / crypto.SegmentSize) 720 base, cachedHashSet := crypto.MerkleProof(sectorBytes, sectorSegment) 721 722 // Using the sector, build a cached root. 723 log2SectorSize := uint64(0) 724 for 1<<log2SectorSize < (modules.SectorSize / crypto.SegmentSize) { 725 log2SectorSize++ 726 } 727 ct := crypto.NewCachedTree(log2SectorSize) 728 ct.SetIndex(segmentIndex) 729 for _, root := range so.SectorRoots { 730 ct.Push(root) 731 } 732 hashSet := ct.Prove(base, cachedHashSet) 733 sp := types.StorageProof{ 734 ParentID: so.id(), 735 HashSet: hashSet, 736 } 737 copy(sp.Segment[:], base) 738 739 // Create and build the transaction with the storage proof. 740 builder := h.wallet.StartTransaction() 741 feeRecommendation, _ := h.tpool.FeeEstimation() 742 if so.value().Cmp(feeRecommendation) < 0 { 743 // There's no sense submitting the storage proof of the fee is more 744 // than the anticipated revenue. 745 return 746 } 747 txnSize := uint64(len(encoding.Marshal(sp)) + 300) 748 requiredFee := feeRecommendation.Mul64(txnSize) 749 err = builder.FundSiacoins(requiredFee) 750 if err != nil { 751 return 752 } 753 builder.AddMinerFee(requiredFee) 754 builder.AddStorageProof(sp) 755 storageProofSet, err := builder.Sign(true) 756 if err != nil { 757 return 758 } 759 err = h.tpool.AcceptTransactionSet(storageProofSet) 760 if err != nil { 761 return 762 } 763 so.TransactionFeesAdded = so.TransactionFeesAdded.Add(requiredFee) 764 765 // Queue another action item to check whether there the storage proof 766 // got confirmed. 767 err = h.queueActionItem(so.proofDeadline(), so.id()) 768 if err != nil { 769 h.log.Println("Error queuing action item:", err) 770 } 771 } 772 773 // Save the storage obligation to account for any fee changes. 774 err := h.db.Update(func(tx *bolt.Tx) error { 775 soBytes, err := json.Marshal(*so) 776 if err != nil { 777 return err 778 } 779 soid := so.id() 780 return tx.Bucket(bucketStorageObligations).Put(soid[:], soBytes) 781 }) 782 if err != nil { 783 h.log.Println("Error updating the storage obligations", err) 784 } 785 786 // Check if all items have succeeded with the required confirmations. Report 787 // success, delete the obligation. 788 if so.ProofConfirmed && h.blockHeight >= so.proofDeadline() { 789 h.removeStorageObligation(so, obligationSucceeded) 790 } 791 } 792 793 // unlockStorageObligation takes a storage obligation out from under lock in 794 // the host. 795 func (h *Host) unlockStorageObligation(so *storageObligation) error { 796 _, exists := h.lockedStorageObligations[so.id()] 797 if !exists { 798 h.log.Critical(errObligationUnlocked) 799 return errObligationUnlocked 800 } 801 delete(h.lockedStorageObligations, so.id()) 802 return nil 803 }