github.com/NebulousLabs/Sia@v1.3.7/modules/host/negotiateformcontract.go (about) 1 package host 2 3 import ( 4 "net" 5 "time" 6 7 "github.com/NebulousLabs/Sia/crypto" 8 "github.com/NebulousLabs/Sia/encoding" 9 "github.com/NebulousLabs/Sia/modules" 10 "github.com/NebulousLabs/Sia/types" 11 ) 12 13 var ( 14 // errCollateralBudgetExceeded is returned if the host does not have enough 15 // room in the collateral budget to accept a particular file contract. 16 errCollateralBudgetExceeded = ErrorInternal("host has reached its collateral budget and cannot accept the file contract") 17 18 // errMaxCollateralReached is returned if a file contract is provided which 19 // would require the host to supply more collateral than the host allows 20 // per file contract. 21 errMaxCollateralReached = ErrorInternal("file contract proposal expects the host to pay more than the maximum allowed collateral") 22 ) 23 24 // contractCollateral returns the amount of collateral that the host is 25 // expected to add to the file contract based on the payout of the file 26 // contract and based on the host settings. 27 func contractCollateral(settings modules.HostExternalSettings, fc types.FileContract) types.Currency { 28 return fc.ValidProofOutputs[1].Value.Sub(settings.ContractPrice) 29 } 30 31 // managedAddCollateral adds the host's collateral to the file contract 32 // transaction set, returning the new inputs and outputs that get added to the 33 // transaction, as well as any new parents that get added to the transaction 34 // set. The builder that is used to add the collateral is also returned, 35 // because the new transaction has not yet been signed. 36 func (h *Host) managedAddCollateral(settings modules.HostExternalSettings, txnSet []types.Transaction) (builder modules.TransactionBuilder, newParents []types.Transaction, newInputs []types.SiacoinInput, newOutputs []types.SiacoinOutput, err error) { 37 txn := txnSet[len(txnSet)-1] 38 parents := txnSet[:len(txnSet)-1] 39 fc := txn.FileContracts[0] 40 hostPortion := contractCollateral(settings, fc) 41 builder, err = h.wallet.RegisterTransaction(txn, parents) 42 if err != nil { 43 return 44 } 45 err = builder.FundSiacoins(hostPortion) 46 if err != nil { 47 builder.Drop() 48 return nil, nil, nil, nil, extendErr("could not add collateral: ", ErrorInternal(err.Error())) 49 } 50 51 // Return which inputs and outputs have been added by the collateral call. 52 newParentIndices, newInputIndices, newOutputIndices, _ := builder.ViewAdded() 53 updatedTxn, updatedParents := builder.View() 54 for _, parentIndex := range newParentIndices { 55 newParents = append(newParents, updatedParents[parentIndex]) 56 } 57 for _, inputIndex := range newInputIndices { 58 newInputs = append(newInputs, updatedTxn.SiacoinInputs[inputIndex]) 59 } 60 for _, outputIndex := range newOutputIndices { 61 newOutputs = append(newOutputs, updatedTxn.SiacoinOutputs[outputIndex]) 62 } 63 return builder, newParents, newInputs, newOutputs, nil 64 } 65 66 // managedRPCFormContract accepts a file contract from a renter, checks the 67 // file contract for compliance with the host settings, and then commits to the 68 // file contract, creating a storage obligation and submitting the contract to 69 // the blockchain. 70 func (h *Host) managedRPCFormContract(conn net.Conn) error { 71 // Send the host settings to the renter. 72 err := h.managedRPCSettings(conn) 73 if err != nil { 74 return extendErr("failed RPCSettings: ", err) 75 } 76 // If the host is not accepting contracts, the connection can be closed. 77 // The renter has been given enough information in the host settings to 78 // understand that the connection is going to be closed. 79 h.mu.Lock() 80 settings := h.externalSettings() 81 h.mu.Unlock() 82 if !settings.AcceptingContracts { 83 h.log.Debugln("Turning down contract because the host is not accepting contracts.") 84 return nil 85 } 86 87 // Extend the deadline to meet the rest of file contract negotiation. 88 conn.SetDeadline(time.Now().Add(modules.NegotiateFileContractTime)) 89 90 // The renter will either accept or reject the host's settings. 91 err = modules.ReadNegotiationAcceptance(conn) 92 if err != nil { 93 return extendErr("renter did not accept settings: ", ErrorCommunication(err.Error())) 94 } 95 // If the renter sends an acceptance of the settings, it will be followed 96 // by an unsigned transaction containing funding from the renter and a file 97 // contract which matches what the final file contract should look like. 98 // After the file contract, the renter will send a public key which is the 99 // renter's public key in the unlock conditions that protect the file 100 // contract from revision. 101 var txnSet []types.Transaction 102 var renterPK crypto.PublicKey 103 err = encoding.ReadObject(conn, &txnSet, modules.NegotiateMaxFileContractSetLen) 104 if err != nil { 105 return extendErr("could not read renter transaction set: ", ErrorConnection(err.Error())) 106 } 107 err = encoding.ReadObject(conn, &renterPK, modules.NegotiateMaxSiaPubkeySize) 108 if err != nil { 109 return extendErr("could not read renter public key: ", ErrorConnection(err.Error())) 110 } 111 112 // The host verifies that the file contract coming over the wire is 113 // acceptable. 114 err = h.managedVerifyNewContract(txnSet, renterPK, settings) 115 if err != nil { 116 // The incoming file contract is not acceptable to the host, indicate 117 // why to the renter. 118 modules.WriteNegotiationRejection(conn, err) // Error ignored to preserve type in extendErr 119 return extendErr("contract verification failed: ", err) 120 } 121 // The host adds collateral to the transaction. 122 txnBuilder, newParents, newInputs, newOutputs, err := h.managedAddCollateral(settings, txnSet) 123 if err != nil { 124 modules.WriteNegotiationRejection(conn, err) // Error ignored to preserve type in extendErr 125 return extendErr("failed to add collateral: ", err) 126 } 127 // The host indicates acceptance, and then sends any new parent 128 // transactions, inputs and outputs that were added to the transaction. 129 err = modules.WriteNegotiationAcceptance(conn) 130 if err != nil { 131 return extendErr("accepting verified contract failed: ", ErrorConnection(err.Error())) 132 } 133 err = encoding.WriteObject(conn, newParents) 134 if err != nil { 135 return extendErr("failed to write new parents: ", ErrorConnection(err.Error())) 136 } 137 err = encoding.WriteObject(conn, newInputs) 138 if err != nil { 139 return extendErr("failed to write new inputs: ", ErrorConnection(err.Error())) 140 } 141 err = encoding.WriteObject(conn, newOutputs) 142 if err != nil { 143 return extendErr("failed to write new outputs: ", ErrorConnection(err.Error())) 144 } 145 146 // The renter will now send a negotiation response, followed by transaction 147 // signatures for the file contract transaction in the case of acceptance. 148 // The transaction signatures will be followed by another transaction 149 // signature, to sign a no-op file contract revision. 150 err = modules.ReadNegotiationAcceptance(conn) 151 if err != nil { 152 return extendErr("renter did not accept updated transactions: ", ErrorCommunication(err.Error())) 153 } 154 var renterTxnSignatures []types.TransactionSignature 155 var renterRevisionSignature types.TransactionSignature 156 err = encoding.ReadObject(conn, &renterTxnSignatures, modules.NegotiateMaxTransactionSignaturesSize) 157 if err != nil { 158 return extendErr("could not read renter transaction signatures: ", ErrorConnection(err.Error())) 159 } 160 err = encoding.ReadObject(conn, &renterRevisionSignature, modules.NegotiateMaxTransactionSignatureSize) 161 if err != nil { 162 return extendErr("could not read renter revision signatures: ", ErrorConnection(err.Error())) 163 } 164 165 // The host adds the renter transaction signatures, then signs the 166 // transaction and submits it to the blockchain, creating a storage 167 // obligation in the process. The host's part is done before anything is 168 // written to the renter, but to give the renter confidence, the host will 169 // send the signatures so that the renter can immediately have the 170 // completed file contract. 171 // 172 // During finalization, the signature for the revision is also checked, and 173 // signatures for the revision transaction are created. 174 h.mu.RLock() 175 hostCollateral := contractCollateral(settings, txnSet[len(txnSet)-1].FileContracts[0]) 176 h.mu.RUnlock() 177 hostTxnSignatures, hostRevisionSignature, newSOID, err := h.managedFinalizeContract(txnBuilder, renterPK, renterTxnSignatures, renterRevisionSignature, nil, hostCollateral, types.ZeroCurrency, types.ZeroCurrency, settings) 178 if err != nil { 179 // The incoming file contract is not acceptable to the host, indicate 180 // why to the renter. 181 modules.WriteNegotiationRejection(conn, err) // Error ignored to preserve type in extendErr 182 return extendErr("contract finalization failed: ", err) 183 } 184 defer h.managedUnlockStorageObligation(newSOID) 185 err = modules.WriteNegotiationAcceptance(conn) 186 if err != nil { 187 return extendErr("failed to write acceptance after contract finalization: ", ErrorConnection(err.Error())) 188 } 189 // The host sends the transaction signatures to the renter, followed by the 190 // revision signature. Negotiation is complete. 191 err = encoding.WriteObject(conn, hostTxnSignatures) 192 if err != nil { 193 return extendErr("failed to write host transaction signatures: ", ErrorConnection(err.Error())) 194 } 195 err = encoding.WriteObject(conn, hostRevisionSignature) 196 if err != nil { 197 return extendErr("failed to write host revision signatures: ", ErrorConnection(err.Error())) 198 } 199 return nil 200 } 201 202 // managedVerifyNewContract checks that an incoming file contract matches the host's 203 // expectations for a valid contract. 204 func (h *Host) managedVerifyNewContract(txnSet []types.Transaction, renterPK crypto.PublicKey, eSettings modules.HostExternalSettings) error { 205 // Check that the transaction set is not empty. 206 if len(txnSet) < 1 { 207 return extendErr("zero-length transaction set: ", errEmptyObject) 208 } 209 // Check that there is a file contract in the txnSet. 210 if len(txnSet[len(txnSet)-1].FileContracts) < 1 { 211 return extendErr("transaction without file contract: ", errEmptyObject) 212 } 213 214 h.mu.RLock() 215 blockHeight := h.blockHeight 216 lockedStorageCollateral := h.financialMetrics.LockedStorageCollateral 217 publicKey := h.publicKey 218 iSettings := h.settings 219 unlockHash := h.unlockHash 220 h.mu.RUnlock() 221 fc := txnSet[len(txnSet)-1].FileContracts[0] 222 223 // A new file contract should have a file size of zero. 224 if fc.FileSize != 0 { 225 return errBadFileSize 226 } 227 if fc.FileMerkleRoot != (crypto.Hash{}) { 228 return errBadFileMerkleRoot 229 } 230 // WindowStart must be at least revisionSubmissionBuffer blocks into the 231 // future. 232 if fc.WindowStart <= blockHeight+revisionSubmissionBuffer { 233 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) 234 return errEarlyWindow 235 } 236 // WindowEnd must be at least settings.WindowSize blocks after 237 // WindowStart. 238 if fc.WindowEnd < fc.WindowStart+eSettings.WindowSize { 239 return errSmallWindow 240 } 241 // WindowStart must not be more than settings.MaxDuration blocks into the 242 // future. 243 if fc.WindowStart > blockHeight+eSettings.MaxDuration { 244 return errLongDuration 245 } 246 247 // ValidProofOutputs shoud have 2 outputs (renter + host) and missed 248 // outputs should have 3 (renter + host + void) 249 if len(fc.ValidProofOutputs) != 2 || len(fc.MissedProofOutputs) != 3 { 250 return errBadContractOutputCounts 251 } 252 // The unlock hashes of the valid and missed proof outputs for the host 253 // must match the host's unlock hash. The third missed output should point 254 // to the void. 255 if fc.ValidProofOutputs[1].UnlockHash != unlockHash || fc.MissedProofOutputs[1].UnlockHash != unlockHash || fc.MissedProofOutputs[2].UnlockHash != (types.UnlockHash{}) { 256 return errBadPayoutUnlockHashes 257 } 258 // Check that the payouts for the valid proof outputs and the missed proof 259 // outputs are the same - this is important because no data has been added 260 // to the file contract yet. 261 if !fc.ValidProofOutputs[1].Value.Equals(fc.MissedProofOutputs[1].Value) { 262 return errMismatchedHostPayouts 263 } 264 // Check that there's enough payout for the host to cover at least the 265 // contract price. This will prevent negative currency panics when working 266 // with the collateral. 267 if fc.ValidProofOutputs[1].Value.Cmp(eSettings.ContractPrice) < 0 { 268 return errLowHostValidOutput 269 } 270 // Check that the collateral does not exceed the maximum amount of 271 // collateral allowed. 272 expectedCollateral := contractCollateral(eSettings, fc) 273 if expectedCollateral.Cmp(eSettings.MaxCollateral) > 0 { 274 return errMaxCollateralReached 275 } 276 // Check that the host has enough room in the collateral budget to add this 277 // collateral. 278 if lockedStorageCollateral.Add(expectedCollateral).Cmp(iSettings.CollateralBudget) > 0 { 279 return errCollateralBudgetExceeded 280 } 281 282 // The unlock hash for the file contract must match the unlock hash that 283 // the host knows how to spend. 284 expectedUH := types.UnlockConditions{ 285 PublicKeys: []types.SiaPublicKey{ 286 types.Ed25519PublicKey(renterPK), 287 publicKey, 288 }, 289 SignaturesRequired: 2, 290 }.UnlockHash() 291 if fc.UnlockHash != expectedUH { 292 return errBadUnlockHash 293 } 294 295 // Check that the transaction set has enough fees on it to get into the 296 // blockchain. 297 setFee := modules.CalculateFee(txnSet) 298 minFee, _ := h.tpool.FeeEstimation() 299 if setFee.Cmp(minFee) < 0 { 300 return errLowTransactionFees 301 } 302 return nil 303 }