github.com/johnathanhowell/sia@v0.5.1-beta.0.20160524050156-83dcc3d37c94/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 timeExtension := fc.WindowEnd - so.proofDeadline() 24 return settings.Collateral.Mul64(fc.FileSize).Mul64(uint64(timeExtension)) 25 } 26 27 // renewBasePrice returns the base cost of the storage in the file contract, 28 // using the host external settings and the starting file contract. 29 func renewBasePrice(so *storageObligation, settings modules.HostExternalSettings, fc types.FileContract) types.Currency { 30 timeExtension := fc.WindowEnd - so.proofDeadline() 31 return settings.StoragePrice.Mul64(fc.FileSize).Mul64(uint64(timeExtension)) 32 } 33 34 // renewContractCollateral returns the amount of collateral that the host is 35 // expected to add to the file contract based on the file contract and host 36 // settings. 37 func renewContractCollateral(so *storageObligation, settings modules.HostExternalSettings, fc types.FileContract) types.Currency { 38 return fc.ValidProofOutputs[1].Value.Sub(settings.ContractPrice).Sub(renewBasePrice(so, settings, fc)) 39 } 40 41 // managedAddRenewCollateral adds the host's collateral to the renewed file 42 // contract. 43 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) { 44 txn := txnSet[len(txnSet)-1] 45 parents := txnSet[:len(txnSet)-1] 46 fc := txn.FileContracts[0] 47 hostPortion := renewContractCollateral(so, settings, fc) 48 builder = h.wallet.RegisterTransaction(txn, parents) 49 err = builder.FundSiacoins(hostPortion) 50 if err != nil { 51 builder.Drop() 52 return nil, nil, nil, nil, err 53 } 54 55 // Return which inputs and outputs have been added by the collateral call. 56 newParentIndices, newInputIndices, newOutputIndices, _ := builder.ViewAdded() 57 updatedTxn, updatedParents := builder.View() 58 for _, parentIndex := range newParentIndices { 59 newParents = append(newParents, updatedParents[parentIndex]) 60 } 61 for _, inputIndex := range newInputIndices { 62 newInputs = append(newInputs, updatedTxn.SiacoinInputs[inputIndex]) 63 } 64 for _, outputIndex := range newOutputIndices { 65 newOutputs = append(newOutputs, updatedTxn.SiacoinOutputs[outputIndex]) 66 } 67 return builder, newParents, newInputs, newOutputs, nil 68 } 69 70 // managedRenewContract accepts a request to renew a file contract. 71 func (h *Host) managedRPCRenewContract(conn net.Conn) error { 72 // Perform the recent revision protocol to get the file contract being 73 // revised. 74 _, so, err := h.managedRPCRecentRevision(conn) 75 if err != nil { 76 return err 77 } 78 79 // Lock the storage obligation for the remainder of the connection. 80 h.mu.Lock() 81 err = h.lockStorageObligation(so) 82 h.mu.Unlock() 83 if err != nil { 84 return err 85 } 86 defer func() { 87 h.mu.Lock() 88 err = h.unlockStorageObligation(so) 89 h.mu.Unlock() 90 if err != nil { 91 h.log.Critical(err) 92 } 93 }() 94 95 // Perform the host settings exchange with the renter. 96 err = h.managedRPCSettings(conn) 97 if err != nil { 98 return err 99 } 100 101 // Set the renewal deadline. 102 conn.SetDeadline(time.Now().Add(modules.NegotiateRenewContractTime)) 103 104 // The renter will either accept or reject the host's settings. 105 err = modules.ReadNegotiationAcceptance(conn) 106 if err != nil { 107 return err 108 } 109 // If the renter sends an acceptance of the settings, it will be followed 110 // by an unsigned transaction containing funding from the renter and a file 111 // contract which matches what the final file contract should look like. 112 // After the file contract, the renter will send a public key which is the 113 // renter's public key in the unlock conditions that protect the file 114 // contract from revision. 115 var txnSet []types.Transaction 116 var renterPK crypto.PublicKey 117 err = encoding.ReadObject(conn, &txnSet, modules.NegotiateMaxFileContractSetLen) 118 if err != nil { 119 return err 120 } 121 err = encoding.ReadObject(conn, &renterPK, modules.NegotiateMaxSiaPubkeySize) 122 if err != nil { 123 return err 124 } 125 126 h.mu.RLock() 127 settings := h.externalSettings() 128 h.mu.RUnlock() 129 130 // Verify that the transaction coming over the wire is a proper renewal. 131 err = h.managedVerifyRenewedContract(so, txnSet, renterPK) 132 if err != nil { 133 return modules.WriteNegotiationRejection(conn, err) 134 } 135 txnBuilder, newParents, newInputs, newOutputs, err := h.managedAddRenewCollateral(so, settings, txnSet) 136 if err != nil { 137 return modules.WriteNegotiationRejection(conn, 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 err 144 } 145 err = encoding.WriteObject(conn, newParents) 146 if err != nil { 147 return err 148 } 149 err = encoding.WriteObject(conn, newInputs) 150 if err != nil { 151 return err 152 } 153 err = encoding.WriteObject(conn, newOutputs) 154 if err != nil { 155 return err 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 err 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 err 172 } 173 err = encoding.ReadObject(conn, &renterRevisionSignature, modules.NegotiateMaxTransactionSignatureSize) 174 if err != nil { 175 return err 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 sigantures 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 hostTxnSignatures, hostRevisionSignature, err := h.managedFinalizeContract(txnBuilder, renterPK, renterTxnSignatures, renterRevisionSignature) 187 if err != nil { 188 return modules.WriteNegotiationRejection(conn, err) 189 } 190 err = modules.WriteNegotiationAcceptance(conn) 191 if err != nil { 192 return err 193 } 194 // The host sends the transaction signatures to the renter, followed by the 195 // revision signature. Negotiation is complete. 196 err = encoding.WriteObject(conn, hostTxnSignatures) 197 if err != nil { 198 return err 199 } 200 return encoding.WriteObject(conn, hostRevisionSignature) 201 } 202 203 // managedVerifyRenewedContract checks that the contract renewal matches the 204 // previous contract and makes all of the appropriate payments. 205 func (h *Host) managedVerifyRenewedContract(so *storageObligation, txnSet []types.Transaction, renterPK crypto.PublicKey) error { 206 // Check that the transaction set is not empty. 207 if len(txnSet) < 1 { 208 return errEmptyFileContractTransactionSet 209 } 210 // Check that the transaction set has a file contract. 211 if len(txnSet[len(txnSet)-1].FileContracts) < 1 { 212 return errNoFileContract 213 } 214 215 h.mu.RLock() 216 blockHeight := h.blockHeight 217 externalSettings := h.externalSettings() 218 internalSettings := h.settings 219 lockedStorageCollateral := h.financialMetrics.LockedStorageCollateral 220 publicKey := h.publicKey 221 unlockHash := h.unlockHash 222 h.mu.RUnlock() 223 fc := txnSet[len(txnSet)-1].FileContracts[0] 224 225 // The file size and merkle root must match the file size and merkle root 226 // from the previous file contract. 227 if fc.FileSize != so.fileSize() { 228 return errBadFileSize 229 } 230 if fc.FileMerkleRoot != so.merkleRoot() { 231 return errBadFileMerkleRoot 232 } 233 // The WindowStart must be at least revisionSubmissionBuffer blocks into 234 // the future. 235 if fc.WindowStart <= blockHeight+revisionSubmissionBuffer { 236 return errWindowStartTooSoon 237 } 238 // WindowEnd must be at least settings.WindowSize blocks after WindowStart. 239 if fc.WindowEnd < fc.WindowStart+externalSettings.WindowSize { 240 return errWindowSizeTooSmall 241 } 242 // The WindowEnd for the new file contract must be further in the future 243 // than the WindowEnd for the existing file contract. 244 if fc.WindowEnd <= so.proofDeadline() { 245 return errRenewDoesNotExtend 246 } 247 248 // ValidProofOutputs shoud have 2 outputs (renter + host) and missed 249 // outputs should have 3 (renter + host + void) 250 if len(fc.ValidProofOutputs) != 2 || len(fc.MissedProofOutputs) != 3 { 251 return errBadPayoutsLen 252 } 253 // The unlock hashes of the valid and missed proof outputs for the host 254 // must match the host's unlock hash. The third missed output should point 255 // to the void. 256 if fc.ValidProofOutputs[1].UnlockHash != unlockHash || fc.MissedProofOutputs[1].UnlockHash != unlockHash || fc.MissedProofOutputs[2].UnlockHash != (types.UnlockHash{}) { 257 return errBadPayoutsUnlockHashes 258 } 259 260 // Check that the collateral does not exceed the maximum amount of 261 // collateral allowed. 262 expectedCollateral := renewContractCollateral(so, externalSettings, fc) 263 if expectedCollateral.Cmp(externalSettings.MaxCollateral) > 0 { 264 return errMaxCollateralReached 265 } 266 // Check that the host has enough room in the collateral budget to add this 267 // collateral. 268 if lockedStorageCollateral.Add(expectedCollateral).Cmp(internalSettings.CollateralBudget) > 0 { 269 return errCollateralBudgetExceeded 270 } 271 // Check that the missed proof outputs contains enough money, and that the 272 // void contains enough money. Before calculating the expected value, check 273 // that the subtraction won't cause a negative currency. 274 basePrice := renewBasePrice(so, externalSettings, fc) 275 baseCollateral := renewBaseCollateral(so, externalSettings, fc) 276 if fc.ValidProofOutputs[1].Value.Cmp(basePrice.Add(baseCollateral)) < 0 { 277 return errBadPayoutsAmounts 278 } 279 expectedHostMissedOutput := fc.ValidProofOutputs[1].Value.Sub(basePrice).Sub(baseCollateral) 280 if fc.MissedProofOutputs[1].Value.Cmp(expectedHostMissedOutput) != 0 { 281 return errBadPayoutsAmounts 282 } 283 // Check that the void output has the correct value. 284 expectedVoidOutput := basePrice.Add(baseCollateral) 285 if fc.MissedProofOutputs[2].Value.Cmp(expectedVoidOutput) != 0 { 286 return errBadPayoutsAmounts 287 } 288 289 // The unlock hash for the file contract must match the unlock hash that 290 // the host knows how to spend. 291 expectedUH := types.UnlockConditions{ 292 PublicKeys: []types.SiaPublicKey{ 293 { 294 Algorithm: types.SignatureEd25519, 295 Key: renterPK[:], 296 }, 297 publicKey, 298 }, 299 SignaturesRequired: 2, 300 }.UnlockHash() 301 if fc.UnlockHash != expectedUH { 302 return errBadContractUnlockHash 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 errLowFees 311 } 312 return nil 313 }