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