github.com/johnathanhowell/sia@v0.5.1-beta.0.20160524050156-83dcc3d37c94/modules/host/negotiateformcontract.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 // errBadContractUnlockHash is returned when the host receives a file 16 // contract where it does not understand the unlock hash driving the 17 // contract. 18 errBadContractUnlockHash = errors.New("file contract has an unexpected unlock hash") 19 20 // errBadFileSize is returned if a file contract is provided by the renter 21 // which does not have the right file size. 22 errBadFileSize = errors.New("new file contract does not have the right file size") 23 24 // errBadFileMerkleRoot is returned if a file contract is provided by the 25 // renter which does not have the right file size. 26 errBadFileMerkleRoot = errors.New("new file contract does not have the right file Merkle root") 27 28 // errBadPayoutsLen is returned if a new file contract is presented that 29 // has the wrong number of valid or missed proof payouts. 30 errBadPayoutsLen = errors.New("file contract has the wrong number of payouts - there should be two valid and three missed payouts") 31 32 // errBadPayoutsAmounts is returned if a new file contract is presented that 33 // does not pay the correct amount to the host - by default, the payouts 34 // should be paying the contract price. 35 errBadPayoutsAmounts = errors.New("file contract has payouts that do not correctly cover the contract price") 36 37 // errBadPayoutsUnlockHashes is returned if a new file contract is 38 // presented that does not make payments to the correct addresses. 39 errBadPayoutsUnlockHashes = errors.New("file contract has payouts which pay to the wrong unlock hashes for the host") 40 41 // errCollateralBudgetExceeded is returned if the host does not have enough 42 // room in the collateral budget to accept a particular file contract. 43 errCollateralBudgetExceeded = errors.New("host has reached its collateral budget and cannot accept the file contract") 44 45 // errDurationTooLong is returned if the renter proposes a file contract 46 // which is longer than the host's maximum duration. 47 errDurationTooLong = errors.New("file contract has a duration which exceeds the duration permitted by the host") 48 49 // errEmptyFileContractTransactionSet is returned if the renter provides a 50 // nil file contract transaction set during file contract negotiation. 51 errEmptyFileContractTransactionSet = errors.New("file contract transaction set is empty") 52 53 // errLowHostPayout is returned if the host is not getting paid enough in 54 // the file contract to cover the contract price. 55 errLowHostPayout = errors.New("file contract payout does not cover the contract cost") 56 57 // errLowFees is returned if a transaction set provided by the renter does 58 // not have large enough transaction fees to have a reasonalbe chance at 59 // making it onto the blockchain. 60 errLowFees = errors.New("file contract proposal does not have enough transaction fees to be acceptable") 61 62 // errMaxCollateralReached is returned if a file contract is provided which 63 // would require the host to supply more collateral than the host allows 64 // per file contract. 65 errMaxCollateralReached = errors.New("file contract proposal expects the host to pay more than the maximum allowed collateral") 66 67 // errNoFileContract is returned if a transaction set is sent that does not 68 // have a file contract. 69 errNoFileContract = errors.New("transaction set does not have a file contract") 70 71 // errWindowSizeTooSmall is returned if a file contract has a window size 72 // (defined by fc.WindowEnd - fc.WindowStart) which is too small to be 73 // acceptable to the host - the host needs to submit its storage proof to 74 // the blockchain inside of that window. 75 errWindowSizeTooSmall = errors.New("file contract has a storage proof window which is not wide enough to match the host's requirements") 76 77 // errWindowStartTooSoon is returned if the storage proof window for the 78 // file contract opens too soon into the future - the host needs time to 79 // submit the file contract and all revisions to the blockchain before the 80 // storage proof window opens. 81 errWindowStartTooSoon = errors.New("the storage proof window is opening too soon") 82 ) 83 84 // contractCollateral returns the amount of collateral that the host is 85 // expected to add to the file contract based on the payout of the file 86 // contract and based on the host settings. 87 func contractCollateral(settings modules.HostInternalSettings, fc types.FileContract) types.Currency { 88 return fc.ValidProofOutputs[1].Value.Sub(settings.MinimumContractPrice) 89 } 90 91 // managedAddCollateral adds the host's collateral to the file contract 92 // transaction set, returning the new inputs and outputs that get added to the 93 // transaction, as well as any new parents that get added to the transaction 94 // set. The builder that is used to add the collateral is also returned, 95 // because the new transaction has not yet been signed. 96 func (h *Host) managedAddCollateral(settings modules.HostInternalSettings, txnSet []types.Transaction) (builder modules.TransactionBuilder, newParents []types.Transaction, newInputs []types.SiacoinInput, newOutputs []types.SiacoinOutput, err error) { 97 txn := txnSet[len(txnSet)-1] 98 parents := txnSet[:len(txnSet)-1] 99 fc := txn.FileContracts[0] 100 hostPortion := contractCollateral(settings, fc) 101 builder = h.wallet.RegisterTransaction(txn, parents) 102 err = builder.FundSiacoins(hostPortion) 103 if err != nil { 104 builder.Drop() 105 return nil, nil, nil, nil, err 106 } 107 108 // Return which inputs and outputs have been added by the collateral call. 109 newParentIndices, newInputIndices, newOutputIndices, _ := builder.ViewAdded() 110 updatedTxn, updatedParents := builder.View() 111 for _, parentIndex := range newParentIndices { 112 newParents = append(newParents, updatedParents[parentIndex]) 113 } 114 for _, inputIndex := range newInputIndices { 115 newInputs = append(newInputs, updatedTxn.SiacoinInputs[inputIndex]) 116 } 117 for _, outputIndex := range newOutputIndices { 118 newOutputs = append(newOutputs, updatedTxn.SiacoinOutputs[outputIndex]) 119 } 120 return builder, newParents, newInputs, newOutputs, nil 121 } 122 123 // managedFinalizeContract will take a file contract, add the host's 124 // collateral, and then try submitting the file contract to the transaction 125 // pool. If there is no error, the completed transaction set will be returned 126 // to the caller. 127 func (h *Host) managedFinalizeContract(builder modules.TransactionBuilder, renterPK crypto.PublicKey, renterSignatures []types.TransactionSignature, renterRevisionSignature types.TransactionSignature) ([]types.TransactionSignature, types.TransactionSignature, error) { 128 for _, sig := range renterSignatures { 129 builder.AddTransactionSignature(sig) 130 } 131 fullTxnSet, err := builder.Sign(true) 132 if err != nil { 133 builder.Drop() 134 return nil, types.TransactionSignature{}, err 135 } 136 137 // Verify that the signature for the revision from the renter is correct. 138 h.mu.RLock() 139 blockHeight := h.blockHeight 140 hostSPK := h.publicKey 141 hostSK := h.secretKey 142 h.mu.RUnlock() 143 contractTxn := fullTxnSet[len(fullTxnSet)-1] 144 fc := contractTxn.FileContracts[0] 145 noOpRevision := types.FileContractRevision{ 146 ParentID: contractTxn.FileContractID(0), 147 UnlockConditions: types.UnlockConditions{ 148 PublicKeys: []types.SiaPublicKey{ 149 { 150 Algorithm: types.SignatureEd25519, 151 Key: renterPK[:], 152 }, 153 hostSPK, 154 }, 155 SignaturesRequired: 2, 156 }, 157 NewRevisionNumber: fc.RevisionNumber + 1, 158 159 NewFileSize: fc.FileSize, 160 NewFileMerkleRoot: fc.FileMerkleRoot, 161 NewWindowStart: fc.WindowStart, 162 NewWindowEnd: fc.WindowEnd, 163 NewValidProofOutputs: fc.ValidProofOutputs, 164 NewMissedProofOutputs: fc.MissedProofOutputs, 165 NewUnlockHash: fc.UnlockHash, 166 } 167 // createRevisionSignature will also perform validation on the result, 168 // returning an error if the renter. 169 revisionTransaction, err := createRevisionSignature(noOpRevision, renterRevisionSignature, hostSK, blockHeight) 170 if err != nil { 171 return nil, types.TransactionSignature{}, err 172 } 173 174 // Create and add the storage obligation for this file contract. 175 h.mu.Lock() 176 defer h.mu.Unlock() 177 fullTxn, _ := builder.View() 178 hostPortion := contractCollateral(h.settings, fc) 179 so := &storageObligation{ 180 ContractCost: h.settings.MinimumContractPrice, 181 LockedCollateral: hostPortion, 182 183 OriginTransactionSet: fullTxnSet, 184 RevisionTransactionSet: []types.Transaction{revisionTransaction}, 185 } 186 lockErr := h.lockStorageObligation(so) 187 if lockErr != nil { 188 return nil, types.TransactionSignature{}, lockErr 189 } 190 // addStorageObligation will submit the transaction to the transaction 191 // pool, and will only do so if there was not some error in creating the 192 // storage obligation. 193 err = h.addStorageObligation(so) 194 lockErr = h.unlockStorageObligation(so) 195 if lockErr != nil { 196 return nil, types.TransactionSignature{}, lockErr 197 } 198 if err != nil { 199 // AcceptingContracts is set to false in the event of an error, because 200 // it means that the host is having some type of disk error. Under 201 // normal circumstances, adding a storage obligation should not cause 202 // problems unexpectedly. 203 h.log.Println(err) 204 h.settings.AcceptingContracts = false 205 builder.Drop() 206 return nil, types.TransactionSignature{}, err 207 } 208 209 // Get the host's transaction signatures from the builder. 210 var hostTxnSignatures []types.TransactionSignature 211 _, _, _, txnSigIndices := builder.ViewAdded() 212 for _, sigIndex := range txnSigIndices { 213 hostTxnSignatures = append(hostTxnSignatures, fullTxn.TransactionSignatures[sigIndex]) 214 } 215 return hostTxnSignatures, revisionTransaction.TransactionSignatures[1], nil 216 } 217 218 // managedRPCFormContract accepts a file contract from a renter, checks the 219 // file contract for compliance with the host settings, and then commits to the 220 // file contract, creating a storage obligation and submitting the contract to 221 // the blockchain. 222 func (h *Host) managedRPCFormContract(conn net.Conn) error { 223 // Send the host settings to the renter. 224 err := h.managedRPCSettings(conn) 225 if err != nil { 226 return err 227 } 228 // If the host is not accepting contracts, the connection can be closed. 229 // The renter has been given enough information in the host settings to 230 // understand that the connection is going to be closed. 231 h.mu.RLock() 232 settings := h.settings 233 h.mu.RUnlock() 234 if !settings.AcceptingContracts { 235 return nil 236 } 237 238 // Extend the deadline to meet the rest of file contract negotiation. 239 conn.SetDeadline(time.Now().Add(modules.NegotiateFileContractTime)) 240 241 // The renter will either accept or reject the host's settings. 242 err = modules.ReadNegotiationAcceptance(conn) 243 if err != nil { 244 return err 245 } 246 // If the renter sends an acceptance of the settings, it will be followed 247 // by an unsigned transaction containing funding from the renter and a file 248 // contract which matches what the final file contract should look like. 249 // After the file contract, the renter will send a public key which is the 250 // renter's public key in the unlock conditions that protect the file 251 // contract from revision. 252 var txnSet []types.Transaction 253 var renterPK crypto.PublicKey 254 err = encoding.ReadObject(conn, &txnSet, modules.NegotiateMaxFileContractSetLen) 255 if err != nil { 256 return err 257 } 258 err = encoding.ReadObject(conn, &renterPK, modules.NegotiateMaxSiaPubkeySize) 259 if err != nil { 260 return err 261 } 262 263 // The host verifies that the file contract coming over the wire is 264 // acceptable. 265 err = h.managedVerifyNewContract(txnSet, renterPK) 266 if err != nil { 267 // The incoming file contract is not acceptable to the host, indicate 268 // why to the renter. 269 return modules.WriteNegotiationRejection(conn, err) 270 } 271 // The host adds collateral to the transaction. 272 txnBuilder, newParents, newInputs, newOutputs, err := h.managedAddCollateral(settings, txnSet) 273 if err != nil { 274 return modules.WriteNegotiationRejection(conn, err) 275 } 276 // The host indicates acceptance, and then sends any new parent 277 // transactions, inputs and outputs that were added to the transaction. 278 err = modules.WriteNegotiationAcceptance(conn) 279 if err != nil { 280 return err 281 } 282 err = encoding.WriteObject(conn, newParents) 283 if err != nil { 284 return err 285 } 286 err = encoding.WriteObject(conn, newInputs) 287 if err != nil { 288 return err 289 } 290 err = encoding.WriteObject(conn, newOutputs) 291 if err != nil { 292 return err 293 } 294 295 // The renter will now send a negotiation response, followed by transaction 296 // signatures for the file contract transaction in the case of acceptance. 297 // The transaction signatures will be followed by another transaction 298 // siganture, to sign a no-op file contract revision. 299 err = modules.ReadNegotiationAcceptance(conn) 300 if err != nil { 301 return err 302 } 303 var renterTxnSignatures []types.TransactionSignature 304 var renterRevisionSignature types.TransactionSignature 305 err = encoding.ReadObject(conn, &renterTxnSignatures, modules.NegotiateMaxTransactionSignaturesSize) 306 if err != nil { 307 return err 308 } 309 err = encoding.ReadObject(conn, &renterRevisionSignature, modules.NegotiateMaxTransactionSignatureSize) 310 if err != nil { 311 return err 312 } 313 314 // The host adds the renter transaction signatures, then signs the 315 // transaction and submits it to the blockchain, creating a storage 316 // obligation in the process. The host's part is done before anything is 317 // written to the renter, but to give the renter confidence, the host will 318 // send the signatures so that the renter can immediately have the 319 // completed file contract. 320 // 321 // During finalization, the siganture for the revision is also checked, and 322 // signatures for the revision transaction are created. 323 hostTxnSignatures, hostRevisionSignature, err := h.managedFinalizeContract(txnBuilder, renterPK, renterTxnSignatures, renterRevisionSignature) 324 if err != nil { 325 // The incoming file contract is not acceptable to the host, indicate 326 // why to the renter. 327 return modules.WriteNegotiationRejection(conn, err) 328 } 329 err = modules.WriteNegotiationAcceptance(conn) 330 if err != nil { 331 return err 332 } 333 // The host sends the transaction signatures to the renter, followed by the 334 // revision signature. Negotiation is complete. 335 err = encoding.WriteObject(conn, hostTxnSignatures) 336 if err != nil { 337 return err 338 } 339 return encoding.WriteObject(conn, hostRevisionSignature) 340 } 341 342 // managedVerifyNewContract checks that an incoming file contract matches the host's 343 // expectations for a valid contract. 344 func (h *Host) managedVerifyNewContract(txnSet []types.Transaction, renterPK crypto.PublicKey) error { 345 // Check that the transaction set is not empty. 346 if len(txnSet) < 1 { 347 return errEmptyFileContractTransactionSet 348 } 349 // Check that there is a file contract in the txnSet. 350 if len(txnSet[len(txnSet)-1].FileContracts) < 1 { 351 return errNoFileContract 352 } 353 354 h.mu.RLock() 355 blockHeight := h.blockHeight 356 lockedStorageCollateral := h.financialMetrics.LockedStorageCollateral 357 publicKey := h.publicKey 358 settings := h.settings 359 unlockHash := h.unlockHash 360 h.mu.RUnlock() 361 fc := txnSet[len(txnSet)-1].FileContracts[0] 362 363 // A new file contract should have a file size of zero. 364 if fc.FileSize != 0 { 365 return errBadFileSize 366 } 367 if fc.FileMerkleRoot != (crypto.Hash{}) { 368 return errBadFileMerkleRoot 369 } 370 // WindowStart must be at least revisionSubmissionBuffer blocks into the 371 // future. 372 if fc.WindowStart <= blockHeight+revisionSubmissionBuffer { 373 h.log.Debugf("A renter tried to form a contract that had a window start which was too soon. The contract started at %v, the current height is %v, the revisionSubmissionBuffer is %v, and the comparison was %v <= %v\n", fc.WindowStart, blockHeight, revisionSubmissionBuffer, fc.WindowStart, blockHeight+revisionSubmissionBuffer) 374 return errWindowStartTooSoon 375 } 376 // WindowEnd must be at least settings.WindowSize blocks after 377 // WindowStart. 378 if fc.WindowEnd < fc.WindowStart+settings.WindowSize { 379 return errWindowSizeTooSmall 380 } 381 // WindowEnd must not be more than settings.MaxDuration blocks into the 382 // future. 383 if fc.WindowStart > blockHeight+settings.MaxDuration { 384 return errDurationTooLong 385 } 386 387 // ValidProofOutputs shoud have 2 outputs (renter + host) and missed 388 // outputs should have 3 (renter + host + void) 389 if len(fc.ValidProofOutputs) != 2 || len(fc.MissedProofOutputs) != 3 { 390 return errBadPayoutsLen 391 } 392 // The unlock hashes of the valid and missed proof outputs for the host 393 // must match the host's unlock hash. The third missed output should point 394 // to the void. 395 if fc.ValidProofOutputs[1].UnlockHash != unlockHash || fc.MissedProofOutputs[1].UnlockHash != unlockHash || fc.MissedProofOutputs[2].UnlockHash != (types.UnlockHash{}) { 396 return errBadPayoutsUnlockHashes 397 } 398 // Check that the payouts for the valid proof outputs and the missed proof 399 // outputs are the same - this is important because no data has been added 400 // to the file contract yet. 401 if fc.ValidProofOutputs[1].Value.Cmp(fc.MissedProofOutputs[1].Value) != 0 { 402 return errBadPayoutsAmounts 403 } 404 // Check that there's enough payout for the host to cover at least the 405 // contract price. This will prevent negative currency panics when working 406 // with the collateral. 407 if fc.ValidProofOutputs[1].Value.Cmp(settings.MinimumContractPrice) < 0 { 408 return errLowHostPayout 409 } 410 // Check that the collateral does not exceed the maximum amount of 411 // collateral allowed. 412 expectedCollateral := contractCollateral(settings, fc) 413 if expectedCollateral.Cmp(settings.MaxCollateral) > 0 { 414 return errMaxCollateralReached 415 } 416 // Check that the host has enough room in the collateral budget to add this 417 // collateral. 418 if lockedStorageCollateral.Add(expectedCollateral).Cmp(settings.CollateralBudget) > 0 { 419 return errCollateralBudgetExceeded 420 } 421 422 // The unlock hash for the file contract must match the unlock hash that 423 // the host knows how to spend. 424 expectedUH := types.UnlockConditions{ 425 PublicKeys: []types.SiaPublicKey{ 426 { 427 Algorithm: types.SignatureEd25519, 428 Key: renterPK[:], 429 }, 430 publicKey, 431 }, 432 SignaturesRequired: 2, 433 }.UnlockHash() 434 if fc.UnlockHash != expectedUH { 435 return errBadContractUnlockHash 436 } 437 438 // Check that the transaction set has enough fees on it to get into the 439 // blockchain. 440 setFee := modules.CalculateFee(txnSet) 441 minFee, _ := h.tpool.FeeEstimation() 442 if setFee.Cmp(minFee) < 0 { 443 return errLowFees 444 } 445 return nil 446 }