github.com/johnathanhowell/sia@v0.5.1-beta.0.20160524050156-83dcc3d37c94/modules/host/negotiaterevisecontract.go (about) 1 package host 2 3 import ( 4 "errors" 5 "net" 6 "time" 7 8 "github.com/NebulousLabs/Sia/crypto" 9 "github.com/NebulousLabs/Sia/encoding" 10 "github.com/NebulousLabs/Sia/modules" 11 "github.com/NebulousLabs/Sia/types" 12 ) 13 14 var ( 15 // errBadModificationIndex is returned if the renter requests a change on a 16 // sector root that is not in the file contract. 17 errBadModificationIndex = errors.New("renter has made a modification that points to a nonexistent sector") 18 19 // badSectorSize is returned if the renter provides a sector to be inserted 20 // that is the wrong size. 21 errBadSectorSize = errors.New("renter has provided an incorrectly sized sector") 22 23 // errIllegalOffsetAndLength is returned if the renter tries perform a 24 // modify operation that uses a troublesome combination of offset and 25 // length. 26 errIllegalOffsetAndLength = errors.New("renter is trying to do a modify with an illegal offset and length") 27 28 // errLargeSector is returned if the renter sends a RevisionAction that has 29 // data which creates a sector that is larger than what the host uses. 30 errLargeSector = errors.New("renter has sent a sector that exceeds the host's sector size") 31 32 // errLateRevision is returned if the renter is attempting to revise a 33 // revision after the revision deadline. The host needs time to submit the 34 // final revision to the blockchain to guarantee payment, and therefore 35 // will not accept revisions once the window start is too close. 36 errLateRevision = errors.New("renter is attempting to revise a revision which the host has closed") 37 38 // errReviseBadCollateralDeduction is returned if a proposed file contrct 39 // revision does not correctly deduct value from the host's missed proof 40 // output - which is the host's collateral pool. 41 errReviseBadCollateralDeduction = errors.New("proposed file contract revision does not correctly deduct from the host's collateral pool") 42 43 // errReviseBadFileMerkleRoot is returned if the renter sends a file 44 // contract revision with a Merkle root that does not match the changes 45 // presented by the revision request. 46 errReviseBadFileMerkleRoot = errors.New("proposed file contract revision has an incorrect Merkle merkle root") 47 48 // errReviseBadHostValidOutput is returned if a proposed file contract 49 // revision does not correctly add value to the host's valid proof outputs. 50 errReviseBadHostValidOutput = errors.New("proposed file contract revision does not correctly add to the host's valid proof output") 51 52 // errReviseBadNewFileSize is returned if a proposed file contract revision 53 // does not have a file size which matches the revisions that have been 54 // made. 55 errReviseBadNewFileSize = errors.New("propsed file contract revision has a bad filesize") 56 57 // errReviseBadNewWindowEnd is returned if a proposed file contract 58 // revision does not have a window end which matches the original file 59 // contract revision. 60 errReviseBadNewWindowEnd = errors.New("proposed file contract revision has a bad window end") 61 62 // errReviseBadNewWindowStart is returned if a proposed file contract 63 // revision does not have a window start which matches the window start of 64 // the original file contract. 65 errReviseBadNewWindowStart = errors.New("propsed file contract revision has a bad window start") 66 67 // errReviseBadParent is returned when a file contract revision is 68 // presented which has a parent id that doesn't match the file contract 69 // which is supposed to be getting revised. 70 errReviseBadParent = errors.New("proposed file contract revision has the wrong parent id") 71 72 // errReviseBadRenterValidOutput is returned if a propsed file contract 73 // revision does not corectly deduct value from the renter's valid proof 74 // output. 75 errReviseBadRenterValidOutput = errors.New("proposed file contract revision does not correctly deduct from the renter's valid proof output") 76 77 // errReviseBadRenterMissedOutput is returned if a proposed file contract 78 // revision does not correctly deduct value from the renter's missed proof 79 // output. 80 errReviseBadRenterMissedOutput = errors.New("proposed file contract revision does not correctly deduct from the renter's missed proof output") 81 82 // errReviseBadRevisionNumber is returned if a proposed file contract 83 // revision does not have a revision number which is strictly greater than 84 // the most recent revision number for the file contract being modified. 85 errReviseBadRevisionNumber = errors.New("proposed file contract revision did not correctly increase the revision number") 86 87 // errReviseBadUnlockConditions is returned when a file contract revision 88 // has unlock conditions that do not match the file contract being revised. 89 errReviseBadUnlockConditions = errors.New("propsed file contract revision appears to have the wrong unlock conditions") 90 91 // errRevisionBadUnlockHash is returned if a proposed file contract 92 // revision does not have an unlock hash which matches the unlock hash of 93 // the previous file contract revision. 94 errReviseBadUnlockHash = errors.New("proposed file contract revision has a bad new unlock hash") 95 96 // errReviseBadVoidOutput is returned if a proposed file contract revision 97 // does not correct add value to the void output to compensate for revenue 98 // from the renter. 99 errReviseBadVoidOutput = errors.New("proposed file contract revision does not correctly add to the host's void outputs") 100 101 // errUnknownModification is returned if the host receives a modification 102 // action from the renter that it does not understand. 103 errUnknownModification = errors.New("renter is attempting an action that the host is not aware of") 104 ) 105 106 // managedRevisionIteration handles one iteration of the revision loop. As a 107 // performance optimization, multiple iterations of revisions are allowed to be 108 // made over the same connection. 109 func (h *Host) managedRevisionIteration(conn net.Conn, so *storageObligation, finalIter bool) error { 110 // Send the settings to the renter. The host will keep going even if it is 111 // not accepting contracts, because in this case the contract already 112 // exists. 113 err := h.managedRPCSettings(conn) 114 if err != nil { 115 return err 116 } 117 118 // Set the negotiation deadline. 119 conn.SetDeadline(time.Now().Add(modules.NegotiateFileContractRevisionTime)) 120 121 // The renter will either accept or reject the settings + revision 122 // transaction. It may also return a stop response to indicate that it 123 // wishes to terminate the revision loop. 124 err = modules.ReadNegotiationAcceptance(conn) 125 if err != nil { 126 return err 127 } 128 129 // Read some variables from the host for use later in the function. 130 h.mu.RLock() 131 settings := h.settings 132 secretKey := h.secretKey 133 blockHeight := h.blockHeight 134 h.mu.RUnlock() 135 136 // The renter is going to send its intended modifications, followed by the 137 // file contract revision that pays for them. 138 var modifications []modules.RevisionAction 139 var revision types.FileContractRevision 140 err = encoding.ReadObject(conn, &modifications, settings.MaxReviseBatchSize) 141 if err != nil { 142 return err 143 } 144 err = encoding.ReadObject(conn, &revision, modules.NegotiateMaxFileContractRevisionSize) 145 if err != nil { 146 return err 147 } 148 149 // First read all of the modifications. Then make the modifications, but 150 // with the ability to reverse them. Then verify the file contract revision 151 // correctly accounts for the changes. 152 var bandwidthRevenue types.Currency // Upload bandwidth. 153 var storageRevenue types.Currency 154 var newCollateral types.Currency 155 var sectorsRemoved []crypto.Hash 156 var sectorsGained []crypto.Hash 157 var gainedSectorData [][]byte 158 err = func() error { 159 for _, modification := range modifications { 160 // Check that the index points to an existing sector root. If the type 161 // is ActionInsert, we permit inserting at the end. 162 if modification.Type == modules.ActionInsert { 163 if modification.SectorIndex > uint64(len(so.SectorRoots)) { 164 return errBadModificationIndex 165 } 166 } else if modification.SectorIndex >= uint64(len(so.SectorRoots)) { 167 return errBadModificationIndex 168 } 169 // Check that the data sent for the sector is not too large. 170 if uint64(len(modification.Data)) > modules.SectorSize { 171 return errLargeSector 172 } 173 174 switch modification.Type { 175 case modules.ActionDelete: 176 // There is no financial information to change, it is enough to 177 // remove the sector. 178 sectorsRemoved = append(sectorsRemoved, so.SectorRoots[modification.SectorIndex]) 179 so.SectorRoots = append(so.SectorRoots[0:modification.SectorIndex], so.SectorRoots[modification.SectorIndex+1:]...) 180 case modules.ActionInsert: 181 // Check that the sector size is correct. 182 if uint64(len(modification.Data)) != modules.SectorSize { 183 return errBadSectorSize 184 } 185 186 // Update finances. 187 blocksRemaining := so.proofDeadline() - blockHeight 188 blockBytesCurrency := types.NewCurrency64(uint64(blocksRemaining)).Mul64(modules.SectorSize) 189 bandwidthRevenue = bandwidthRevenue.Add(settings.MinimumUploadBandwidthPrice.Mul64(modules.SectorSize)) 190 storageRevenue = storageRevenue.Add(settings.MinimumStoragePrice.Mul(blockBytesCurrency)) 191 newCollateral = newCollateral.Add(settings.Collateral.Mul(blockBytesCurrency)) 192 193 // Insert the sector into the root list. 194 newRoot := crypto.MerkleRoot(modification.Data) 195 sectorsGained = append(sectorsGained, newRoot) 196 gainedSectorData = append(gainedSectorData, modification.Data) 197 so.SectorRoots = append(so.SectorRoots[:modification.SectorIndex], append([]crypto.Hash{newRoot}, so.SectorRoots[modification.SectorIndex:]...)...) 198 case modules.ActionModify: 199 // Check that the offset and length are okay. Length is already 200 // known to be appropriately small, but the offset needs to be 201 // checked for being appropriately small as well otherwise there is 202 // a risk of overflow. 203 if modification.Offset > modules.SectorSize || modification.Offset+uint64(len(modification.Data)) > modules.SectorSize { 204 return errIllegalOffsetAndLength 205 } 206 207 // Get the data for the new sector. 208 sector, err := h.ReadSector(so.SectorRoots[modification.SectorIndex]) 209 if err != nil { 210 return err 211 } 212 copy(sector[modification.Offset:], modification.Data) 213 214 // Update finances. 215 bandwidthRevenue = bandwidthRevenue.Add(settings.MinimumUploadBandwidthPrice.Mul64(uint64(len(modification.Data)))) 216 217 // Update the sectors removed and gained to indicate that the old 218 // sector has been replaced with a new sector. 219 newRoot := crypto.MerkleRoot(sector) 220 sectorsRemoved = append(sectorsRemoved, so.SectorRoots[modification.SectorIndex]) 221 sectorsGained = append(sectorsGained, newRoot) 222 gainedSectorData = append(gainedSectorData, sector) 223 so.SectorRoots[modification.SectorIndex] = newRoot 224 default: 225 return errUnknownModification 226 } 227 } 228 newRevenue := storageRevenue.Add(bandwidthRevenue) 229 return verifyRevision(so, revision, blockHeight, newRevenue, newCollateral) 230 }() 231 if err != nil { 232 return modules.WriteNegotiationRejection(conn, err) 233 } 234 // Revision is acceptable, write an acceptance string. 235 err = modules.WriteNegotiationAcceptance(conn) 236 if err != nil { 237 return err 238 } 239 240 // Renter will send a transaction signature for the file contract revision. 241 var renterSig types.TransactionSignature 242 err = encoding.ReadObject(conn, &renterSig, modules.NegotiateMaxTransactionSignatureSize) 243 if err != nil { 244 return err 245 } 246 // Verify that the signature is valid and get the host's signature. 247 txn, err := createRevisionSignature(revision, renterSig, secretKey, blockHeight) 248 if err != nil { 249 return modules.WriteNegotiationRejection(conn, err) 250 } 251 252 so.PotentialStorageRevenue = so.PotentialStorageRevenue.Add(storageRevenue) 253 so.RiskedCollateral = so.RiskedCollateral.Add(newCollateral) 254 so.PotentialUploadRevenue = so.PotentialUploadRevenue.Add(bandwidthRevenue) 255 so.RevisionTransactionSet = []types.Transaction{txn} 256 err = h.modifyStorageObligation(so, sectorsRemoved, sectorsGained, gainedSectorData) 257 if err != nil { 258 return modules.WriteNegotiationRejection(conn, err) 259 } 260 261 // Host will now send acceptance and its signature to the renter. This 262 // iteration is complete. If the finalIter flag is set, StopResponse will 263 // be sent instead. This indicates to the renter that the host wishes to 264 // terminate the revision loop. 265 if finalIter { 266 err = modules.WriteNegotiationStop(conn) 267 } else { 268 err = modules.WriteNegotiationAcceptance(conn) 269 } 270 if err != nil { 271 return err 272 } 273 return encoding.WriteObject(conn, txn.TransactionSignatures[1]) 274 } 275 276 // managedRPCReviseContract accepts a request to revise an existing contract. 277 // Revisions can add sectors, delete sectors, and modify existing sectors. 278 func (h *Host) managedRPCReviseContract(conn net.Conn) error { 279 // Set a preliminary deadline for receiving the storage obligation. 280 startTime := time.Now() 281 // Perform the file contract revision exchange, giving the renter the most 282 // recent file contract revision and getting the storage obligation that 283 // will be used to pay for the data. 284 _, so, err := h.managedRPCRecentRevision(conn) 285 if err != nil { 286 return err 287 } 288 289 // Lock the storage obligation during the revision. 290 h.mu.Lock() 291 err = h.lockStorageObligation(so) 292 h.mu.Unlock() 293 if err != nil { 294 return err 295 } 296 defer func() { 297 h.mu.Lock() 298 err = h.unlockStorageObligation(so) 299 h.mu.Unlock() 300 if err != nil { 301 h.log.Critical(err) 302 } 303 }() 304 305 // Begin the revision loop. The host will process revisions until a 306 // timeout is reached, or until the renter sends a StopResponse. 307 for timeoutReached := false; !timeoutReached; { 308 timeoutReached = time.Since(startTime) > iteratedConnectionTime 309 err := h.managedRevisionIteration(conn, so, timeoutReached) 310 if err == modules.ErrStopResponse { 311 return nil 312 } else if err != nil { 313 return err 314 } 315 } 316 return nil 317 } 318 319 // verifyRevision checks that the revision pays the host correctly, and that 320 // the revision does not attempt any malicious or unexpected changes. 321 func verifyRevision(so *storageObligation, revision types.FileContractRevision, blockHeight types.BlockHeight, newRevenue, newCollateral types.Currency) error { 322 // Check that the revision is well-formed. 323 if len(revision.NewValidProofOutputs) != 2 || len(revision.NewMissedProofOutputs) != 3 { 324 return errInsaneFileContractRevisionOutputCounts 325 } 326 327 // Check that the time to finalize and submit the file contract revision 328 // has not already passed. 329 if so.expiration()-revisionSubmissionBuffer <= blockHeight { 330 return errLateRevision 331 } 332 333 oldFCR := so.RevisionTransactionSet[len(so.RevisionTransactionSet)-1].FileContractRevisions[0] 334 335 // Check that all non-volatile fields are the same. 336 if oldFCR.ParentID != revision.ParentID { 337 return errReviseBadParent 338 } 339 if oldFCR.UnlockConditions.UnlockHash() != revision.UnlockConditions.UnlockHash() { 340 return errReviseBadUnlockConditions 341 } 342 if oldFCR.NewRevisionNumber >= revision.NewRevisionNumber { 343 return errReviseBadRevisionNumber 344 } 345 if revision.NewFileSize != uint64(len(so.SectorRoots))*modules.SectorSize { 346 return errReviseBadNewFileSize 347 } 348 if oldFCR.NewWindowStart != revision.NewWindowStart { 349 return errReviseBadNewWindowStart 350 } 351 if oldFCR.NewWindowEnd != revision.NewWindowEnd { 352 return errReviseBadNewWindowEnd 353 } 354 if oldFCR.NewUnlockHash != revision.NewUnlockHash { 355 return errReviseBadUnlockHash 356 } 357 358 // The new revenue comes out of the renter's valid outputs. 359 if revision.NewValidProofOutputs[0].Value.Add(newRevenue).Cmp(oldFCR.NewValidProofOutputs[0].Value) > 0 { 360 return errReviseBadRenterValidOutput 361 } 362 // The new revenue goes into the host's valid outputs. 363 if oldFCR.NewValidProofOutputs[1].Value.Add(newRevenue).Cmp(revision.NewValidProofOutputs[1].Value) < 0 { 364 return errReviseBadHostValidOutput 365 } 366 // The new revenue comes out of the renter's missed outputs. 367 if revision.NewMissedProofOutputs[0].Value.Add(newRevenue).Cmp(oldFCR.NewMissedProofOutputs[0].Value) > 0 { 368 return errReviseBadRenterMissedOutput 369 } 370 // The new collateral comes out of the host's missed outputs. 371 if revision.NewMissedProofOutputs[1].Value.Add(newCollateral).Cmp(oldFCR.NewMissedProofOutputs[1].Value) > 0 { 372 return errReviseBadCollateralDeduction 373 } 374 // The new collateral and new revenue goes into the host's void outputs. 375 if oldFCR.NewMissedProofOutputs[2].Value.Add(newRevenue).Add(newCollateral).Cmp(revision.NewMissedProofOutputs[2].Value) < 0 { 376 return errReviseBadVoidOutput 377 } 378 379 // The Merkle root is checked last because it is the most expensive check. 380 log2SectorSize := uint64(0) 381 for 1<<log2SectorSize < (modules.SectorSize / crypto.SegmentSize) { 382 log2SectorSize++ 383 } 384 ct := crypto.NewCachedTree(log2SectorSize) 385 for _, root := range so.SectorRoots { 386 ct.Push(root) 387 } 388 expectedMerkleRoot := ct.Root() 389 if revision.NewFileMerkleRoot != expectedMerkleRoot { 390 return errReviseBadFileMerkleRoot 391 } 392 393 return nil 394 }