github.com/fozzysec/SiaPrime@v0.0.0-20190612043147-66c8e8d11fe3/modules/host/negotiaterenewcontract.go (about) 1 package host 2 3 import ( 4 "errors" 5 "net" 6 "time" 7 8 "SiaPrime/crypto" 9 "SiaPrime/encoding" 10 "SiaPrime/modules" 11 "SiaPrime/types" 12 ) 13 14 var ( 15 // errRenewDoesNotExtend is returned if a file contract renewal is 16 // presented which does not extend the existing file contract. 17 errRenewDoesNotExtend = errors.New("file contract renewal does not extend the existing file contract") 18 ) 19 20 // renewBaseCollateral returns the base collateral on the storage in the file 21 // contract, using the host's external settings and the starting file contract. 22 func renewBaseCollateral(so storageObligation, settings modules.HostExternalSettings, fc types.FileContract) types.Currency { 23 if fc.WindowEnd <= so.proofDeadline() { 24 return types.NewCurrency64(0) 25 } 26 timeExtension := fc.WindowEnd - so.proofDeadline() 27 return settings.Collateral.Mul64(fc.FileSize).Mul64(uint64(timeExtension)) 28 } 29 30 // renewBasePrice returns the base cost of the storage in the file contract, 31 // using the host external settings and the starting file contract. 32 func renewBasePrice(so storageObligation, settings modules.HostExternalSettings, fc types.FileContract) types.Currency { 33 if fc.WindowEnd <= so.proofDeadline() { 34 return types.NewCurrency64(0) 35 } 36 timeExtension := fc.WindowEnd - so.proofDeadline() 37 return settings.StoragePrice.Mul64(fc.FileSize).Mul64(uint64(timeExtension)) 38 } 39 40 // renewContractCollateral returns the amount of collateral that the host is 41 // expected to add to the file contract based on the file contract and host 42 // settings. 43 func renewContractCollateral(so storageObligation, settings modules.HostExternalSettings, fc types.FileContract) types.Currency { 44 return fc.ValidProofOutputs[1].Value.Sub(settings.ContractPrice).Sub(renewBasePrice(so, settings, fc)) 45 } 46 47 // managedAddRenewCollateral adds the host's collateral to the renewed file 48 // contract. 49 func (h *Host) managedAddRenewCollateral(so storageObligation, settings modules.HostExternalSettings, txnSet []types.Transaction) (builder modules.TransactionBuilder, newParents []types.Transaction, newInputs []types.SiacoinInput, newOutputs []types.SiacoinOutput, err error) { 50 txn := txnSet[len(txnSet)-1] 51 parents := txnSet[:len(txnSet)-1] 52 fc := txn.FileContracts[0] 53 hostPortion := renewContractCollateral(so, settings, fc) 54 builder, err = h.wallet.RegisterTransaction(txn, parents) 55 if err != nil { 56 return 57 } 58 err = builder.FundSiacoins(hostPortion) 59 if err != nil { 60 builder.Drop() 61 return nil, nil, nil, nil, extendErr("could not add collateral: ", ErrorInternal(err.Error())) 62 } 63 64 // Return which inputs and outputs have been added by the collateral call. 65 newParentIndices, newInputIndices, newOutputIndices, _ := builder.ViewAdded() 66 updatedTxn, updatedParents := builder.View() 67 for _, parentIndex := range newParentIndices { 68 newParents = append(newParents, updatedParents[parentIndex]) 69 } 70 for _, inputIndex := range newInputIndices { 71 newInputs = append(newInputs, updatedTxn.SiacoinInputs[inputIndex]) 72 } 73 for _, outputIndex := range newOutputIndices { 74 newOutputs = append(newOutputs, updatedTxn.SiacoinOutputs[outputIndex]) 75 } 76 return builder, newParents, newInputs, newOutputs, nil 77 } 78 79 // managedRenewContract accepts a request to renew a file contract. 80 func (h *Host) managedRPCRenewContract(conn net.Conn) error { 81 // Perform the recent revision protocol to get the file contract being 82 // revised. 83 _, so, err := h.managedRPCRecentRevision(conn) 84 if err != nil { 85 return extendErr("failed RPCRecentRevision during RPCRenewContract: ", err) 86 } 87 // The storage obligation is received with a lock. Defer a call to unlock 88 // the storage obligation. 89 defer func() { 90 h.managedUnlockStorageObligation(so.id()) 91 }() 92 93 // Perform the host settings exchange with the renter. 94 err = h.managedRPCSettings(conn) 95 if err != nil { 96 return extendErr("RPCSettings failed: ", err) 97 } 98 99 // Set the renewal deadline. 100 conn.SetDeadline(time.Now().Add(modules.NegotiateRenewContractTime)) 101 102 // The renter will either accept or reject the host's settings. 103 err = modules.ReadNegotiationAcceptance(conn) 104 if err != nil { 105 return extendErr("renter rejected the host settings: ", ErrorCommunication(err.Error())) 106 } 107 // If the renter sends an acceptance of the settings, it will be followed 108 // by an unsigned transaction containing funding from the renter and a file 109 // contract which matches what the final file contract should look like. 110 // After the file contract, the renter will send a public key which is the 111 // renter's public key in the unlock conditions that protect the file 112 // contract from revision. 113 var txnSet []types.Transaction 114 var renterPK crypto.PublicKey 115 err = encoding.ReadObject(conn, &txnSet, modules.NegotiateMaxFileContractSetLen) 116 if err != nil { 117 return extendErr("unable to read transaction set: ", ErrorConnection(err.Error())) 118 } 119 err = encoding.ReadObject(conn, &renterPK, modules.NegotiateMaxSiaPubkeySize) 120 if err != nil { 121 return extendErr("unable to read renter public key: ", ErrorConnection(err.Error())) 122 } 123 124 h.mu.Lock() 125 settings := h.externalSettings() 126 h.mu.Unlock() 127 128 // Verify that the transaction coming over the wire is a proper renewal. 129 err = h.managedVerifyRenewedContract(so, txnSet, renterPK) 130 if err != nil { 131 modules.WriteNegotiationRejection(conn, err) // Error is ignored to preserve type for extendErr 132 return extendErr("verification of renewal failed: ", err) 133 } 134 txnBuilder, newParents, newInputs, newOutputs, err := h.managedAddRenewCollateral(so, settings, txnSet) 135 if err != nil { 136 modules.WriteNegotiationRejection(conn, err) // Error is ignored to preserve type for extendErr 137 return extendErr("failed to add collateral: ", err) 138 } 139 // The host indicates acceptance, then sends the new parents, inputs, and 140 // outputs to the transaction. 141 err = modules.WriteNegotiationAcceptance(conn) 142 if err != nil { 143 return extendErr("failed to write acceptance: ", ErrorConnection(err.Error())) 144 } 145 err = encoding.WriteObject(conn, newParents) 146 if err != nil { 147 return extendErr("failed to write new parents: ", ErrorConnection(err.Error())) 148 } 149 err = encoding.WriteObject(conn, newInputs) 150 if err != nil { 151 return extendErr("failed to write new inputs: ", ErrorConnection(err.Error())) 152 } 153 err = encoding.WriteObject(conn, newOutputs) 154 if err != nil { 155 return extendErr("failed to write new outputs: ", ErrorConnection(err.Error())) 156 } 157 158 // The renter will send a negotiation response, followed by transaction 159 // signatures for the file contract transaction in the case of acceptance. 160 // The transaction signatures will be followed by another transaction 161 // signature to sign the no-op file contract revision associated with the 162 // new file contract. 163 err = modules.ReadNegotiationAcceptance(conn) 164 if err != nil { 165 return extendErr("renter rejected collateral extension: ", ErrorCommunication(err.Error())) 166 } 167 var renterTxnSignatures []types.TransactionSignature 168 var renterRevisionSignature types.TransactionSignature 169 err = encoding.ReadObject(conn, &renterTxnSignatures, modules.NegotiateMaxTransactionSignatureSize) 170 if err != nil { 171 return extendErr("failed to read renter transaction signatures: ", ErrorConnection(err.Error())) 172 } 173 err = encoding.ReadObject(conn, &renterRevisionSignature, modules.NegotiateMaxTransactionSignatureSize) 174 if err != nil { 175 return extendErr("failed to read renter revision signatures: ", ErrorConnection(err.Error())) 176 } 177 178 // The host adds the renter transaction signatures, then signs the 179 // transaction and submits it to the blockchain, creating a storage 180 // obligation in the process. The host's part is now complete and the 181 // contract is finalized, but to give confidence to the renter the host 182 // will send the signatures so that the renter can immediately have the 183 // completed file contract. 184 // 185 // During finalization the signatures sent by the renter are all checked. 186 h.mu.RLock() 187 fc := txnSet[len(txnSet)-1].FileContracts[0] 188 renewCollateral := renewContractCollateral(so, settings, fc) 189 renewRevenue := renewBasePrice(so, settings, fc) 190 renewRisk := renewBaseCollateral(so, settings, fc) 191 h.mu.RUnlock() 192 hostTxnSignatures, hostRevisionSignature, newSOID, err := h.managedFinalizeContract(txnBuilder, renterPK, renterTxnSignatures, renterRevisionSignature, so.SectorRoots, renewCollateral, renewRevenue, renewRisk, settings) 193 if err != nil { 194 modules.WriteNegotiationRejection(conn, err) // Error is ignored to preserve type for extendErr 195 return extendErr("failed to finalize contract: ", err) 196 } 197 defer h.managedUnlockStorageObligation(newSOID) 198 err = modules.WriteNegotiationAcceptance(conn) 199 if err != nil { 200 return extendErr("failed to write acceptance: ", ErrorConnection(err.Error())) 201 } 202 // The host sends the transaction signatures to the renter, followed by the 203 // revision signature. Negotiation is complete. 204 err = encoding.WriteObject(conn, hostTxnSignatures) 205 if err != nil { 206 return extendErr("failed to write transaction signatures: ", ErrorConnection(err.Error())) 207 } 208 err = encoding.WriteObject(conn, hostRevisionSignature) 209 if err != nil { 210 return extendErr("failed to write revision signature: ", ErrorConnection(err.Error())) 211 } 212 return nil 213 } 214 215 // managedVerifyRenewedContract checks that the contract renewal matches the 216 // previous contract and makes all of the appropriate payments. 217 func (h *Host) managedVerifyRenewedContract(so storageObligation, txnSet []types.Transaction, renterPK crypto.PublicKey) error { 218 // Check that the transaction set is not empty. 219 if len(txnSet) < 1 { 220 return extendErr("zero-length transaction set: ", errEmptyObject) 221 } 222 // Check that the transaction set has a file contract. 223 if len(txnSet[len(txnSet)-1].FileContracts) < 1 { 224 return extendErr("transaction without file contract: ", errEmptyObject) 225 } 226 227 h.mu.Lock() 228 blockHeight := h.blockHeight 229 externalSettings := h.externalSettings() 230 internalSettings := h.settings 231 lockedStorageCollateral := h.financialMetrics.LockedStorageCollateral 232 publicKey := h.publicKey 233 unlockHash := h.unlockHash 234 h.mu.Unlock() 235 fc := txnSet[len(txnSet)-1].FileContracts[0] 236 237 // The file size and merkle root must match the file size and merkle root 238 // from the previous file contract. 239 if fc.FileSize != so.fileSize() { 240 return errBadFileSize 241 } 242 if fc.FileMerkleRoot != so.merkleRoot() { 243 return errBadFileMerkleRoot 244 } 245 // The WindowStart must be at least revisionSubmissionBuffer blocks into 246 // the future. 247 if fc.WindowStart <= blockHeight+revisionSubmissionBuffer { 248 return errEarlyWindow 249 } 250 // WindowEnd must be at least settings.WindowSize blocks after WindowStart. 251 if fc.WindowEnd < fc.WindowStart+externalSettings.WindowSize { 252 return errSmallWindow 253 } 254 // WindowStart must not be more than settings.MaxDuration blocks into the 255 // future. 256 if fc.WindowStart > blockHeight+externalSettings.MaxDuration { 257 return errLongDuration 258 } 259 260 // ValidProofOutputs shoud have 2 outputs (renter + host) and missed 261 // outputs should have 3 (renter + host + void) 262 if len(fc.ValidProofOutputs) != 2 || len(fc.MissedProofOutputs) != 3 { 263 return errBadContractOutputCounts 264 } 265 // The unlock hashes of the valid and missed proof outputs for the host 266 // must match the host's unlock hash. The third missed output should point 267 // to the void. 268 if fc.ValidProofOutputs[1].UnlockHash != unlockHash || fc.MissedProofOutputs[1].UnlockHash != unlockHash || fc.MissedProofOutputs[2].UnlockHash != (types.UnlockHash{}) { 269 return errBadPayoutUnlockHashes 270 } 271 272 // Check that the collateral does not exceed the maximum amount of 273 // collateral allowed. 274 expectedCollateral := renewContractCollateral(so, externalSettings, fc) 275 if expectedCollateral.Cmp(externalSettings.MaxCollateral) > 0 { 276 return errMaxCollateralReached 277 } 278 // Check that the host has enough room in the collateral budget to add this 279 // collateral. 280 if lockedStorageCollateral.Add(expectedCollateral).Cmp(internalSettings.CollateralBudget) > 0 { 281 return errCollateralBudgetExceeded 282 } 283 // Check that the missed proof outputs contain enough money, and that the 284 // void output contains enough money. 285 basePrice := renewBasePrice(so, externalSettings, fc) 286 baseCollateral := renewBaseCollateral(so, externalSettings, fc) 287 if fc.ValidProofOutputs[1].Value.Cmp(basePrice.Add(baseCollateral)) < 0 { 288 return errLowHostValidOutput 289 } 290 expectedHostMissedOutput := fc.ValidProofOutputs[1].Value.Sub(basePrice).Sub(baseCollateral) 291 if fc.MissedProofOutputs[1].Value.Cmp(expectedHostMissedOutput) < 0 { 292 return errLowHostMissedOutput 293 } 294 // Check that the void output has the correct value. 295 expectedVoidOutput := basePrice.Add(baseCollateral) 296 if fc.MissedProofOutputs[2].Value.Cmp(expectedVoidOutput) > 0 { 297 return errLowVoidOutput 298 } 299 300 // The unlock hash for the file contract must match the unlock hash that 301 // the host knows how to spend. 302 expectedUH := types.UnlockConditions{ 303 PublicKeys: []types.SiaPublicKey{ 304 types.Ed25519PublicKey(renterPK), 305 publicKey, 306 }, 307 SignaturesRequired: 2, 308 }.UnlockHash() 309 if fc.UnlockHash != expectedUH { 310 return errBadUnlockHash 311 } 312 313 // Check that the transaction set has enough fees on it to get into the 314 // blockchain. 315 setFee := modules.CalculateFee(txnSet) 316 minFee, _ := h.tpool.FeeEstimation() 317 if setFee.Cmp(minFee) < 0 { 318 return errLowTransactionFees 319 } 320 return nil 321 }