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