github.com/nebulouslabs/sia@v1.3.7/modules/host/negotiaterevisecontract.go (about) 1 package host 2 3 import ( 4 "errors" 5 "fmt" 6 "net" 7 "time" 8 9 "github.com/NebulousLabs/Sia/crypto" 10 "github.com/NebulousLabs/Sia/encoding" 11 "github.com/NebulousLabs/Sia/modules" 12 "github.com/NebulousLabs/Sia/types" 13 ) 14 15 // managedRevisionIteration handles one iteration of the revision loop. As a 16 // performance optimization, multiple iterations of revisions are allowed to be 17 // made over the same connection. 18 func (h *Host) managedRevisionIteration(conn net.Conn, so *storageObligation, finalIter bool) error { 19 // Send the settings to the renter. The host will keep going even if it is 20 // not accepting contracts, because in this case the contract already 21 // exists. 22 err := h.managedRPCSettings(conn) 23 if err != nil { 24 return extendErr("RPCSettings failed: ", err) 25 } 26 27 // Set the negotiation deadline. 28 conn.SetDeadline(time.Now().Add(modules.NegotiateFileContractRevisionTime)) 29 30 // The renter will either accept or reject the settings + revision 31 // transaction. It may also return a stop response to indicate that it 32 // wishes to terminate the revision loop. 33 err = modules.ReadNegotiationAcceptance(conn) 34 if err == modules.ErrStopResponse { 35 return err // managedRPCReviseContract will catch this and exit gracefully 36 } else if err != nil { 37 return extendErr("renter rejected host settings: ", ErrorCommunication(err.Error())) 38 } 39 40 // Read some variables from the host for use later in the function. 41 h.mu.Lock() 42 settings := h.externalSettings() 43 secretKey := h.secretKey 44 blockHeight := h.blockHeight 45 h.mu.Unlock() 46 47 // The renter is going to send its intended modifications, followed by the 48 // file contract revision that pays for them. 49 var modifications []modules.RevisionAction 50 var revision types.FileContractRevision 51 err = encoding.ReadObject(conn, &modifications, settings.MaxReviseBatchSize) 52 if err != nil { 53 return extendErr("unable to read revision modifications: ", ErrorConnection(err.Error())) 54 } 55 err = encoding.ReadObject(conn, &revision, modules.NegotiateMaxFileContractRevisionSize) 56 if err != nil { 57 return extendErr("unable to read proposed revision: ", ErrorConnection(err.Error())) 58 } 59 60 // First read all of the modifications. Then make the modifications, but 61 // with the ability to reverse them. Then verify the file contract revision 62 // correctly accounts for the changes. 63 var bandwidthRevenue types.Currency // Upload bandwidth. 64 var storageRevenue types.Currency 65 var newCollateral types.Currency 66 var sectorsRemoved []crypto.Hash 67 var sectorsGained []crypto.Hash 68 var gainedSectorData [][]byte 69 err = func() error { 70 for _, modification := range modifications { 71 // Check that the index points to an existing sector root. If the type 72 // is ActionInsert, we permit inserting at the end. 73 if modification.Type == modules.ActionInsert { 74 if modification.SectorIndex > uint64(len(so.SectorRoots)) { 75 return errBadModificationIndex 76 } 77 } else if modification.SectorIndex >= uint64(len(so.SectorRoots)) { 78 return errBadModificationIndex 79 } 80 // Check that the data sent for the sector is not too large. 81 if uint64(len(modification.Data)) > modules.SectorSize { 82 return errLargeSector 83 } 84 85 switch modification.Type { 86 case modules.ActionDelete: 87 // There is no financial information to change, it is enough to 88 // remove the sector. 89 sectorsRemoved = append(sectorsRemoved, so.SectorRoots[modification.SectorIndex]) 90 so.SectorRoots = append(so.SectorRoots[0:modification.SectorIndex], so.SectorRoots[modification.SectorIndex+1:]...) 91 case modules.ActionInsert: 92 // Check that the sector size is correct. 93 if uint64(len(modification.Data)) != modules.SectorSize { 94 return errBadSectorSize 95 } 96 97 // Update finances. 98 blocksRemaining := so.proofDeadline() - blockHeight 99 blockBytesCurrency := types.NewCurrency64(uint64(blocksRemaining)).Mul64(modules.SectorSize) 100 bandwidthRevenue = bandwidthRevenue.Add(settings.UploadBandwidthPrice.Mul64(modules.SectorSize)) 101 storageRevenue = storageRevenue.Add(settings.StoragePrice.Mul(blockBytesCurrency)) 102 newCollateral = newCollateral.Add(settings.Collateral.Mul(blockBytesCurrency)) 103 104 // Insert the sector into the root list. 105 newRoot := crypto.MerkleRoot(modification.Data) 106 sectorsGained = append(sectorsGained, newRoot) 107 gainedSectorData = append(gainedSectorData, modification.Data) 108 so.SectorRoots = append(so.SectorRoots[:modification.SectorIndex], append([]crypto.Hash{newRoot}, so.SectorRoots[modification.SectorIndex:]...)...) 109 case modules.ActionModify: 110 // Check that the offset and length are okay. Length is already 111 // known to be appropriately small, but the offset needs to be 112 // checked for being appropriately small as well otherwise there is 113 // a risk of overflow. 114 if modification.Offset > modules.SectorSize || modification.Offset+uint64(len(modification.Data)) > modules.SectorSize { 115 return errIllegalOffsetAndLength 116 } 117 118 // Get the data for the new sector. 119 sector, err := h.ReadSector(so.SectorRoots[modification.SectorIndex]) 120 if err != nil { 121 return extendErr("could not read sector: ", ErrorInternal(err.Error())) 122 } 123 copy(sector[modification.Offset:], modification.Data) 124 125 // Update finances. 126 bandwidthRevenue = bandwidthRevenue.Add(settings.UploadBandwidthPrice.Mul64(uint64(len(modification.Data)))) 127 128 // Update the sectors removed and gained to indicate that the old 129 // sector has been replaced with a new sector. 130 newRoot := crypto.MerkleRoot(sector) 131 sectorsRemoved = append(sectorsRemoved, so.SectorRoots[modification.SectorIndex]) 132 sectorsGained = append(sectorsGained, newRoot) 133 gainedSectorData = append(gainedSectorData, sector) 134 so.SectorRoots[modification.SectorIndex] = newRoot 135 default: 136 return errUnknownModification 137 } 138 } 139 newRevenue := storageRevenue.Add(bandwidthRevenue) 140 return extendErr("unable to verify updated contract: ", verifyRevision(*so, revision, blockHeight, newRevenue, newCollateral)) 141 }() 142 if err != nil { 143 modules.WriteNegotiationRejection(conn, err) // Error is ignored so that the error type can be preserved in extendErr. 144 return extendErr("rejected proposed modifications: ", err) 145 } 146 // Revision is acceptable, write an acceptance string. 147 err = modules.WriteNegotiationAcceptance(conn) 148 if err != nil { 149 return extendErr("could not accept revision modifications: ", ErrorConnection(err.Error())) 150 } 151 152 // Renter will send a transaction signature for the file contract revision. 153 var renterSig types.TransactionSignature 154 err = encoding.ReadObject(conn, &renterSig, modules.NegotiateMaxTransactionSignatureSize) 155 if err != nil { 156 return extendErr("could not read renter transaction signature: ", ErrorConnection(err.Error())) 157 } 158 // Verify that the signature is valid and get the host's signature. 159 txn, err := createRevisionSignature(revision, renterSig, secretKey, blockHeight) 160 if err != nil { 161 modules.WriteNegotiationRejection(conn, err) // Error is ignored so that the error type can be preserved in extendErr. 162 return extendErr("could not create revision signature: ", err) 163 } 164 165 so.PotentialStorageRevenue = so.PotentialStorageRevenue.Add(storageRevenue) 166 so.RiskedCollateral = so.RiskedCollateral.Add(newCollateral) 167 so.PotentialUploadRevenue = so.PotentialUploadRevenue.Add(bandwidthRevenue) 168 so.RevisionTransactionSet = []types.Transaction{txn} 169 h.mu.Lock() 170 err = h.modifyStorageObligation(*so, sectorsRemoved, sectorsGained, gainedSectorData) 171 h.mu.Unlock() 172 if err != nil { 173 modules.WriteNegotiationRejection(conn, err) // Error is ignored so that the error type can be preserved in extendErr. 174 return extendErr("could not modify storage obligation: ", ErrorInternal(err.Error())) 175 } 176 177 // Host will now send acceptance and its signature to the renter. This 178 // iteration is complete. If the finalIter flag is set, StopResponse will 179 // be sent instead. This indicates to the renter that the host wishes to 180 // terminate the revision loop. 181 if finalIter { 182 err = modules.WriteNegotiationStop(conn) 183 } else { 184 err = modules.WriteNegotiationAcceptance(conn) 185 } 186 if err != nil { 187 return extendErr("iteration signal failed to send: ", ErrorConnection(err.Error())) 188 } 189 err = encoding.WriteObject(conn, txn.TransactionSignatures[1]) 190 if err != nil { 191 return extendErr("failed to write revision signatures: ", ErrorConnection(err.Error())) 192 } 193 return nil 194 } 195 196 // managedRPCReviseContract accepts a request to revise an existing contract. 197 // Revisions can add sectors, delete sectors, and modify existing sectors. 198 func (h *Host) managedRPCReviseContract(conn net.Conn) error { 199 // Set a preliminary deadline for receiving the storage obligation. 200 startTime := time.Now() 201 // Perform the file contract revision exchange, giving the renter the most 202 // recent file contract revision and getting the storage obligation that 203 // will be used to pay for the data. 204 _, so, err := h.managedRPCRecentRevision(conn) 205 if err != nil { 206 return extendErr("failed RPCRecentRevision during RPCReviseContract: ", err) 207 } 208 // The storage obligation is received with a lock on it. Defer a call to 209 // unlock the storage obligation. 210 defer func() { 211 h.managedUnlockStorageObligation(so.id()) 212 }() 213 214 // Begin the revision loop. The host will process revisions until a 215 // timeout is reached, or until the renter sends a StopResponse. 216 for timeoutReached := false; !timeoutReached; { 217 timeoutReached = time.Since(startTime) > iteratedConnectionTime 218 err := h.managedRevisionIteration(conn, &so, timeoutReached) 219 if err == modules.ErrStopResponse { 220 return nil 221 } else if err != nil { 222 return extendErr("revision iteration failed: ", err) 223 } 224 } 225 return nil 226 } 227 228 // verifyRevision checks that the revision pays the host correctly, and that 229 // the revision does not attempt any malicious or unexpected changes. 230 func verifyRevision(so storageObligation, revision types.FileContractRevision, blockHeight types.BlockHeight, expectedExchange, expectedCollateral types.Currency) error { 231 // Check that the revision is well-formed. 232 if len(revision.NewValidProofOutputs) != 2 || len(revision.NewMissedProofOutputs) != 3 { 233 return errBadContractOutputCounts 234 } 235 236 // Check that the time to finalize and submit the file contract revision 237 // has not already passed. 238 if so.expiration()-revisionSubmissionBuffer <= blockHeight { 239 return errLateRevision 240 } 241 242 oldFCR := so.RevisionTransactionSet[len(so.RevisionTransactionSet)-1].FileContractRevisions[0] 243 244 // Host payout addresses shouldn't change 245 if revision.NewValidProofOutputs[1].UnlockHash != oldFCR.NewValidProofOutputs[1].UnlockHash { 246 return errors.New("host payout address changed") 247 } 248 if revision.NewMissedProofOutputs[1].UnlockHash != oldFCR.NewMissedProofOutputs[1].UnlockHash { 249 return errors.New("host payout address changed") 250 } 251 // Make sure the lost collateral still goes to the void 252 if revision.NewMissedProofOutputs[2].UnlockHash != oldFCR.NewMissedProofOutputs[2].UnlockHash { 253 return errors.New("lost collateral address was changed") 254 } 255 256 // Check that all non-volatile fields are the same. 257 if oldFCR.ParentID != revision.ParentID { 258 return errBadContractParent 259 } 260 if oldFCR.UnlockConditions.UnlockHash() != revision.UnlockConditions.UnlockHash() { 261 return errBadUnlockConditions 262 } 263 if oldFCR.NewRevisionNumber >= revision.NewRevisionNumber { 264 return errBadRevisionNumber 265 } 266 if revision.NewFileSize != uint64(len(so.SectorRoots))*modules.SectorSize { 267 return errBadFileSize 268 } 269 if oldFCR.NewWindowStart != revision.NewWindowStart { 270 return errBadWindowStart 271 } 272 if oldFCR.NewWindowEnd != revision.NewWindowEnd { 273 return errBadWindowEnd 274 } 275 if oldFCR.NewUnlockHash != revision.NewUnlockHash { 276 return errBadUnlockHash 277 } 278 279 // Determine the amount that was transferred from the renter. 280 if revision.NewValidProofOutputs[0].Value.Cmp(oldFCR.NewValidProofOutputs[0].Value) > 0 { 281 return extendErr("renter increased its valid proof output: ", errHighRenterValidOutput) 282 } 283 fromRenter := oldFCR.NewValidProofOutputs[0].Value.Sub(revision.NewValidProofOutputs[0].Value) 284 // Verify that enough money was transferred. 285 if fromRenter.Cmp(expectedExchange) < 0 { 286 s := fmt.Sprintf("expected at least %v to be exchanged, but %v was exchanged: ", expectedExchange, fromRenter) 287 return extendErr(s, errHighRenterValidOutput) 288 } 289 290 // Determine the amount of money that was transferred to the host. 291 if oldFCR.NewValidProofOutputs[1].Value.Cmp(revision.NewValidProofOutputs[1].Value) > 0 { 292 return extendErr("host valid proof output was decreased: ", errLowHostValidOutput) 293 } 294 toHost := revision.NewValidProofOutputs[1].Value.Sub(oldFCR.NewValidProofOutputs[1].Value) 295 // Verify that enough money was transferred. 296 if !toHost.Equals(fromRenter) { 297 s := fmt.Sprintf("expected exactly %v to be transferred to the host, but %v was transferred: ", fromRenter, toHost) 298 return extendErr(s, errLowHostValidOutput) 299 } 300 301 // If the renter's valid proof output is larger than the renter's missed 302 // proof output, the renter has incentive to see the host fail. Make sure 303 // that this incentive is not present. 304 if revision.NewValidProofOutputs[0].Value.Cmp(revision.NewMissedProofOutputs[0].Value) > 0 { 305 return extendErr("renter has incentive to see host fail: ", errHighRenterMissedOutput) 306 } 307 308 // Check that the host is not going to be posting more collateral than is 309 // expected. If the new misesd output is greater than the old one, the host 310 // is actually posting negative collateral, which is fine. 311 if revision.NewMissedProofOutputs[1].Value.Cmp(oldFCR.NewMissedProofOutputs[1].Value) <= 0 { 312 collateral := oldFCR.NewMissedProofOutputs[1].Value.Sub(revision.NewMissedProofOutputs[1].Value) 313 if collateral.Cmp(expectedCollateral) > 0 { 314 s := fmt.Sprintf("host expected to post at most %v collateral, but contract has host posting %v: ", expectedCollateral, collateral) 315 return extendErr(s, errLowHostMissedOutput) 316 } 317 } 318 319 // Check that the revision count has increased. 320 if revision.NewRevisionNumber <= oldFCR.NewRevisionNumber { 321 return errBadRevisionNumber 322 } 323 324 // The Merkle root is checked last because it is the most expensive check. 325 log2SectorSize := uint64(0) 326 for 1<<log2SectorSize < (modules.SectorSize / crypto.SegmentSize) { 327 log2SectorSize++ 328 } 329 ct := crypto.NewCachedTree(log2SectorSize) 330 for _, root := range so.SectorRoots { 331 ct.Push(root) 332 } 333 expectedMerkleRoot := ct.Root() 334 if revision.NewFileMerkleRoot != expectedMerkleRoot { 335 return errBadFileMerkleRoot 336 } 337 338 return nil 339 }