gitlab.com/SiaPrime/SiaPrime@v1.4.1/modules/renter/proto/formcontract.go (about) 1 package proto 2 3 import ( 4 "net" 5 6 "gitlab.com/SiaPrime/SiaPrime/build" 7 "gitlab.com/SiaPrime/SiaPrime/crypto" 8 "gitlab.com/SiaPrime/SiaPrime/encoding" 9 "gitlab.com/SiaPrime/SiaPrime/modules" 10 "gitlab.com/SiaPrime/SiaPrime/types" 11 12 "gitlab.com/NebulousLabs/errors" 13 ) 14 15 // FormContract forms a contract with a host and submits the contract 16 // transaction to tpool. The contract is added to the ContractSet and its 17 // metadata is returned. 18 func (cs *ContractSet) FormContract(params ContractParams, txnBuilder transactionBuilder, tpool transactionPool, hdb hostDB, cancel <-chan struct{}) (rc modules.RenterContract, err error) { 19 // use the new renter-host protocol for hosts that support it. 20 // 21 // NOTE: due to a bug, we use the old protocol even for v1.4.0 hosts. 22 if build.VersionCmp(params.Host.Version, "1.4.1") >= 0 { 23 return cs.newFormContract(params, txnBuilder, tpool, hdb, cancel) 24 } 25 return cs.oldFormContract(params, txnBuilder, tpool, hdb, cancel) 26 } 27 28 func (cs *ContractSet) oldFormContract(params ContractParams, txnBuilder transactionBuilder, tpool transactionPool, hdb hostDB, cancel <-chan struct{}) (rc modules.RenterContract, err error) { 29 // Extract vars from params, for convenience. 30 allowance, host, funding, startHeight, endHeight, refundAddress := params.Allowance, params.Host, params.Funding, params.StartHeight, params.EndHeight, params.RefundAddress 31 32 // Calculate the anticipated transaction fee. 33 _, maxFee := tpool.FeeEstimation() 34 txnFee := maxFee.Mul64(modules.EstimatedFileContractTransactionSetSize) 35 36 // Calculate the payouts for the renter, host, and whole contract. 37 period := endHeight - startHeight 38 expectedStorage := allowance.ExpectedStorage / allowance.Hosts 39 renterPayout, hostPayout, _, err := modules.RenterPayoutsPreTax(host, funding, txnFee, types.ZeroCurrency, types.ZeroCurrency, period, expectedStorage) 40 if err != nil { 41 return modules.RenterContract{}, err 42 } 43 totalPayout := renterPayout.Add(hostPayout) 44 45 // Check for negative currency. 46 if types.PostTax(startHeight, totalPayout).Cmp(hostPayout) < 0 { 47 return modules.RenterContract{}, errors.New("not enough money to pay both siafund fee and also host payout") 48 } 49 // Fund the transaction. 50 err = txnBuilder.FundSiacoins(funding) 51 if err != nil { 52 return modules.RenterContract{}, err 53 } 54 // Add FileContract identifier. 55 fcTxn, _ := txnBuilder.View() 56 si, hk := PrefixedSignedIdentifier(params.RenterSeed, fcTxn, host.PublicKey) 57 _ = txnBuilder.AddArbitraryData(append(si[:], hk[:]...)) 58 // Create our key. 59 ourSK, ourPK := GenerateKeyPair(params.RenterSeed, fcTxn) 60 // Create unlock conditions. 61 uc := types.UnlockConditions{ 62 PublicKeys: []types.SiaPublicKey{ 63 types.Ed25519PublicKey(ourPK), 64 host.PublicKey, 65 }, 66 SignaturesRequired: 2, 67 } 68 69 // Create file contract. 70 fc := types.FileContract{ 71 FileSize: 0, 72 FileMerkleRoot: crypto.Hash{}, // no proof possible without data 73 WindowStart: endHeight, 74 WindowEnd: endHeight + host.WindowSize, 75 Payout: totalPayout, 76 UnlockHash: uc.UnlockHash(), 77 RevisionNumber: 0, 78 ValidProofOutputs: []types.SiacoinOutput{ 79 // Outputs need to account for tax. 80 {Value: types.PostTax(startHeight, totalPayout).Sub(hostPayout), UnlockHash: refundAddress}, // This is the renter payout, but with tax applied. 81 // Collateral is returned to host. 82 {Value: hostPayout, UnlockHash: host.UnlockHash}, 83 }, 84 MissedProofOutputs: []types.SiacoinOutput{ 85 // Same as above. 86 {Value: types.PostTax(startHeight, totalPayout).Sub(hostPayout), UnlockHash: refundAddress}, 87 // Same as above. 88 {Value: hostPayout, UnlockHash: host.UnlockHash}, 89 // Once we start doing revisions, we'll move some coins to the host and some to the void. 90 {Value: types.ZeroCurrency, UnlockHash: types.UnlockHash{}}, 91 }, 92 } 93 94 // Add file contract. 95 txnBuilder.AddFileContract(fc) 96 // Add miner fee. 97 txnBuilder.AddMinerFee(txnFee) 98 99 // Create initial transaction set. 100 txn, parentTxns := txnBuilder.View() 101 unconfirmedParents, err := txnBuilder.UnconfirmedParents() 102 if err != nil { 103 return modules.RenterContract{}, err 104 } 105 txnSet := append(unconfirmedParents, append(parentTxns, txn)...) 106 107 // Increase Successful/Failed interactions accordingly 108 defer func() { 109 if err != nil { 110 hdb.IncrementFailedInteractions(host.PublicKey) 111 err = errors.Extend(err, modules.ErrHostFault) 112 } else { 113 hdb.IncrementSuccessfulInteractions(host.PublicKey) 114 } 115 }() 116 117 // Initiate connection. 118 dialer := &net.Dialer{ 119 Cancel: cancel, 120 Timeout: connTimeout, 121 } 122 conn, err := dialer.Dial("tcp", string(host.NetAddress)) 123 if err != nil { 124 return modules.RenterContract{}, err 125 } 126 defer func() { _ = conn.Close() }() 127 128 // Allot time for sending RPC ID + verifySettings. 129 extendDeadline(conn, modules.NegotiateSettingsTime) 130 if err = encoding.WriteObject(conn, modules.RPCFormContract); err != nil { 131 return modules.RenterContract{}, err 132 } 133 134 // Verify the host's settings and confirm its identity. 135 host, err = verifySettings(conn, host) 136 if err != nil { 137 return modules.RenterContract{}, err 138 } 139 if !host.AcceptingContracts { 140 return modules.RenterContract{}, errors.New("host is not accepting contracts") 141 } 142 143 // Allot time for negotiation. 144 extendDeadline(conn, modules.NegotiateFileContractTime) 145 146 // Send acceptance, txn signed by us, and pubkey. 147 if err = modules.WriteNegotiationAcceptance(conn); err != nil { 148 return modules.RenterContract{}, errors.New("couldn't send initial acceptance: " + err.Error()) 149 } 150 if err = encoding.WriteObject(conn, txnSet); err != nil { 151 return modules.RenterContract{}, errors.New("couldn't send the contract signed by us: " + err.Error()) 152 } 153 if err = encoding.WriteObject(conn, ourSK.PublicKey()); err != nil { 154 return modules.RenterContract{}, errors.New("couldn't send our public key: " + err.Error()) 155 } 156 157 // Read acceptance and txn signed by host. 158 if err = modules.ReadNegotiationAcceptance(conn); err != nil { 159 return modules.RenterContract{}, errors.New("host did not accept our proposed contract: " + err.Error()) 160 } 161 // Host now sends any new parent transactions, inputs and outputs that 162 // were added to the transaction. 163 var newParents []types.Transaction 164 var newInputs []types.SiacoinInput 165 var newOutputs []types.SiacoinOutput 166 if err = encoding.ReadObject(conn, &newParents, types.BlockSizeLimit); err != nil { 167 return modules.RenterContract{}, errors.New("couldn't read the host's added parents: " + err.Error()) 168 } 169 if err = encoding.ReadObject(conn, &newInputs, types.BlockSizeLimit); err != nil { 170 return modules.RenterContract{}, errors.New("couldn't read the host's added inputs: " + err.Error()) 171 } 172 if err = encoding.ReadObject(conn, &newOutputs, types.BlockSizeLimit); err != nil { 173 return modules.RenterContract{}, errors.New("couldn't read the host's added outputs: " + err.Error()) 174 } 175 176 // Merge txnAdditions with txnSet. 177 txnBuilder.AddParents(newParents) 178 for _, input := range newInputs { 179 txnBuilder.AddSiacoinInput(input) 180 } 181 for _, output := range newOutputs { 182 txnBuilder.AddSiacoinOutput(output) 183 } 184 185 // Sign the txn. 186 signedTxnSet, err := txnBuilder.Sign(true) 187 if err != nil { 188 return modules.RenterContract{}, modules.WriteNegotiationRejection(conn, errors.New("failed to sign transaction: "+err.Error())) 189 } 190 191 // Calculate signatures added by the transaction builder. 192 var addedSignatures []types.TransactionSignature 193 _, _, _, addedSignatureIndices := txnBuilder.ViewAdded() 194 for _, i := range addedSignatureIndices { 195 addedSignatures = append(addedSignatures, signedTxnSet[len(signedTxnSet)-1].TransactionSignatures[i]) 196 } 197 198 // create initial (no-op) revision, transaction, and signature 199 initRevision := types.FileContractRevision{ 200 ParentID: signedTxnSet[len(signedTxnSet)-1].FileContractID(0), 201 UnlockConditions: uc, 202 NewRevisionNumber: 1, 203 204 NewFileSize: fc.FileSize, 205 NewFileMerkleRoot: fc.FileMerkleRoot, 206 NewWindowStart: fc.WindowStart, 207 NewWindowEnd: fc.WindowEnd, 208 NewValidProofOutputs: fc.ValidProofOutputs, 209 NewMissedProofOutputs: fc.MissedProofOutputs, 210 NewUnlockHash: fc.UnlockHash, 211 } 212 renterRevisionSig := types.TransactionSignature{ 213 ParentID: crypto.Hash(initRevision.ParentID), 214 PublicKeyIndex: 0, 215 CoveredFields: types.CoveredFields{ 216 FileContractRevisions: []uint64{0}, 217 }, 218 } 219 revisionTxn := types.Transaction{ 220 FileContractRevisions: []types.FileContractRevision{initRevision}, 221 TransactionSignatures: []types.TransactionSignature{renterRevisionSig}, 222 } 223 encodedSig := crypto.SignHash(revisionTxn.SigHash(0, startHeight), ourSK) 224 revisionTxn.TransactionSignatures[0].Signature = encodedSig[:] 225 226 // Send acceptance and signatures. 227 if err = modules.WriteNegotiationAcceptance(conn); err != nil { 228 return modules.RenterContract{}, errors.New("couldn't send transaction acceptance: " + err.Error()) 229 } 230 if err = encoding.WriteObject(conn, addedSignatures); err != nil { 231 return modules.RenterContract{}, errors.New("couldn't send added signatures: " + err.Error()) 232 } 233 if err = encoding.WriteObject(conn, revisionTxn.TransactionSignatures[0]); err != nil { 234 return modules.RenterContract{}, errors.New("couldn't send revision signature: " + err.Error()) 235 } 236 237 // Read the host acceptance and signatures. 238 err = modules.ReadNegotiationAcceptance(conn) 239 if err != nil { 240 return modules.RenterContract{}, errors.New("host did not accept our signatures: " + err.Error()) 241 } 242 var hostSigs []types.TransactionSignature 243 if err = encoding.ReadObject(conn, &hostSigs, 2e3); err != nil { 244 return modules.RenterContract{}, errors.New("couldn't read the host's signatures: " + err.Error()) 245 } 246 for _, sig := range hostSigs { 247 txnBuilder.AddTransactionSignature(sig) 248 } 249 var hostRevisionSig types.TransactionSignature 250 if err = encoding.ReadObject(conn, &hostRevisionSig, 2e3); err != nil { 251 return modules.RenterContract{}, errors.New("couldn't read the host's revision signature: " + err.Error()) 252 } 253 revisionTxn.TransactionSignatures = append(revisionTxn.TransactionSignatures, hostRevisionSig) 254 255 // Construct the final transaction. 256 txn, parentTxns = txnBuilder.View() 257 txnSet = append(parentTxns, txn) 258 259 // Submit to blockchain. 260 err = tpool.AcceptTransactionSet(txnSet) 261 if err == modules.ErrDuplicateTransactionSet { 262 // As long as it made it into the transaction pool, we're good. 263 err = nil 264 } 265 if err != nil { 266 return modules.RenterContract{}, err 267 } 268 269 // Construct contract header. 270 header := contractHeader{ 271 Transaction: revisionTxn, 272 SecretKey: ourSK, 273 StartHeight: startHeight, 274 TotalCost: funding, 275 ContractFee: host.ContractPrice, 276 TxnFee: txnFee, 277 SiafundFee: types.Tax(startHeight, fc.Payout), 278 Utility: modules.ContractUtility{ 279 GoodForUpload: true, 280 GoodForRenew: true, 281 }, 282 } 283 284 // Add contract to set. 285 meta, err := cs.managedInsertContract(header, nil) // no Merkle roots yet 286 if err != nil { 287 return modules.RenterContract{}, err 288 } 289 return meta, nil 290 } 291 292 // newFormContract forms a contract with a host using the new renter-host 293 // protocol. 294 func (cs *ContractSet) newFormContract(params ContractParams, txnBuilder transactionBuilder, tpool transactionPool, hdb hostDB, cancel <-chan struct{}) (rc modules.RenterContract, err error) { 295 // Extract vars from params, for convenience. 296 allowance, host, funding, startHeight, endHeight, refundAddress := params.Allowance, params.Host, params.Funding, params.StartHeight, params.EndHeight, params.RefundAddress 297 298 // Calculate the anticipated transaction fee. 299 _, maxFee := tpool.FeeEstimation() 300 txnFee := maxFee.Mul64(modules.EstimatedFileContractTransactionSetSize) 301 302 // Calculate the payouts for the renter, host, and whole contract. 303 period := endHeight - startHeight 304 expectedStorage := allowance.ExpectedStorage / allowance.Hosts 305 renterPayout, hostPayout, _, err := modules.RenterPayoutsPreTax(host, funding, txnFee, types.ZeroCurrency, types.ZeroCurrency, period, expectedStorage) 306 if err != nil { 307 return modules.RenterContract{}, err 308 } 309 totalPayout := renterPayout.Add(hostPayout) 310 311 // Check for negative currency. 312 if types.PostTax(startHeight, totalPayout).Cmp(hostPayout) < 0 { 313 return modules.RenterContract{}, errors.New("not enough money to pay both siafund fee and also host payout") 314 } 315 // Fund the transaction. 316 err = txnBuilder.FundSiacoins(funding) 317 if err != nil { 318 return modules.RenterContract{}, err 319 } 320 // Add FileContract identifier. 321 fcTxn, _ := txnBuilder.View() 322 si, hk := PrefixedSignedIdentifier(params.RenterSeed, fcTxn, host.PublicKey) 323 _ = txnBuilder.AddArbitraryData(append(si[:], hk[:]...)) 324 // Create our key. 325 ourSK, ourPK := GenerateKeyPair(params.RenterSeed, fcTxn) 326 // Create unlock conditions. 327 uc := types.UnlockConditions{ 328 PublicKeys: []types.SiaPublicKey{ 329 types.Ed25519PublicKey(ourPK), 330 host.PublicKey, 331 }, 332 SignaturesRequired: 2, 333 } 334 335 // Create file contract. 336 fc := types.FileContract{ 337 FileSize: 0, 338 FileMerkleRoot: crypto.Hash{}, // no proof possible without data 339 WindowStart: endHeight, 340 WindowEnd: endHeight + host.WindowSize, 341 Payout: totalPayout, 342 UnlockHash: uc.UnlockHash(), 343 RevisionNumber: 0, 344 ValidProofOutputs: []types.SiacoinOutput{ 345 // Outputs need to account for tax. 346 {Value: types.PostTax(startHeight, totalPayout).Sub(hostPayout), UnlockHash: refundAddress}, // This is the renter payout, but with tax applied. 347 // Collateral is returned to host. 348 {Value: hostPayout, UnlockHash: host.UnlockHash}, 349 }, 350 MissedProofOutputs: []types.SiacoinOutput{ 351 // Same as above. 352 {Value: types.PostTax(startHeight, totalPayout).Sub(hostPayout), UnlockHash: refundAddress}, 353 // Same as above. 354 {Value: hostPayout, UnlockHash: host.UnlockHash}, 355 // Once we start doing revisions, we'll move some coins to the host and some to the void. 356 {Value: types.ZeroCurrency, UnlockHash: types.UnlockHash{}}, 357 }, 358 } 359 360 // Add file contract. 361 txnBuilder.AddFileContract(fc) 362 // Add miner fee. 363 txnBuilder.AddMinerFee(txnFee) 364 365 // Create initial transaction set. 366 txn, parentTxns := txnBuilder.View() 367 unconfirmedParents, err := txnBuilder.UnconfirmedParents() 368 if err != nil { 369 return modules.RenterContract{}, err 370 } 371 txnSet := append(unconfirmedParents, append(parentTxns, txn)...) 372 373 // Increase Successful/Failed interactions accordingly 374 defer func() { 375 if err != nil { 376 hdb.IncrementFailedInteractions(host.PublicKey) 377 err = errors.Extend(err, modules.ErrHostFault) 378 } else { 379 hdb.IncrementSuccessfulInteractions(host.PublicKey) 380 } 381 }() 382 383 // Initiate protocol. 384 s, err := cs.NewRawSession(host, startHeight, hdb, cancel) 385 if err != nil { 386 return modules.RenterContract{}, err 387 } 388 defer s.Close() 389 390 // Send the FormContract request. 391 req := modules.LoopFormContractRequest{ 392 Transactions: txnSet, 393 RenterKey: uc.PublicKeys[0], 394 } 395 if err := s.writeRequest(modules.RPCLoopFormContract, req); err != nil { 396 return modules.RenterContract{}, err 397 } 398 399 // Read the host's response. 400 var resp modules.LoopContractAdditions 401 if err := s.readResponse(&resp, modules.RPCMinLen); err != nil { 402 return modules.RenterContract{}, err 403 } 404 405 // Incorporate host's modifications. 406 txnBuilder.AddParents(resp.Parents) 407 for _, input := range resp.Inputs { 408 txnBuilder.AddSiacoinInput(input) 409 } 410 for _, output := range resp.Outputs { 411 txnBuilder.AddSiacoinOutput(output) 412 } 413 414 // Sign the txn. 415 signedTxnSet, err := txnBuilder.Sign(true) 416 if err != nil { 417 err = errors.New("failed to sign transaction: " + err.Error()) 418 modules.WriteRPCResponse(s.conn, s.aead, nil, err) 419 return modules.RenterContract{}, err 420 } 421 422 // Calculate signatures added by the transaction builder. 423 var addedSignatures []types.TransactionSignature 424 _, _, _, addedSignatureIndices := txnBuilder.ViewAdded() 425 for _, i := range addedSignatureIndices { 426 addedSignatures = append(addedSignatures, signedTxnSet[len(signedTxnSet)-1].TransactionSignatures[i]) 427 } 428 429 // create initial (no-op) revision, transaction, and signature 430 initRevision := types.FileContractRevision{ 431 ParentID: signedTxnSet[len(signedTxnSet)-1].FileContractID(0), 432 UnlockConditions: uc, 433 NewRevisionNumber: 1, 434 435 NewFileSize: fc.FileSize, 436 NewFileMerkleRoot: fc.FileMerkleRoot, 437 NewWindowStart: fc.WindowStart, 438 NewWindowEnd: fc.WindowEnd, 439 NewValidProofOutputs: fc.ValidProofOutputs, 440 NewMissedProofOutputs: fc.MissedProofOutputs, 441 NewUnlockHash: fc.UnlockHash, 442 } 443 renterRevisionSig := types.TransactionSignature{ 444 ParentID: crypto.Hash(initRevision.ParentID), 445 PublicKeyIndex: 0, 446 CoveredFields: types.CoveredFields{ 447 FileContractRevisions: []uint64{0}, 448 }, 449 } 450 revisionTxn := types.Transaction{ 451 FileContractRevisions: []types.FileContractRevision{initRevision}, 452 TransactionSignatures: []types.TransactionSignature{renterRevisionSig}, 453 } 454 encodedSig := crypto.SignHash(revisionTxn.SigHash(0, startHeight), ourSK) 455 revisionTxn.TransactionSignatures[0].Signature = encodedSig[:] 456 457 // Send acceptance and signatures. 458 renterSigs := modules.LoopContractSignatures{ 459 ContractSignatures: addedSignatures, 460 RevisionSignature: revisionTxn.TransactionSignatures[0], 461 } 462 if err := modules.WriteRPCResponse(s.conn, s.aead, renterSigs, nil); err != nil { 463 return modules.RenterContract{}, err 464 } 465 466 // Read the host acceptance and signatures. 467 var hostSigs modules.LoopContractSignatures 468 if err := s.readResponse(&hostSigs, modules.RPCMinLen); err != nil { 469 return modules.RenterContract{}, err 470 } 471 for _, sig := range hostSigs.ContractSignatures { 472 txnBuilder.AddTransactionSignature(sig) 473 } 474 revisionTxn.TransactionSignatures = append(revisionTxn.TransactionSignatures, hostSigs.RevisionSignature) 475 476 // Construct the final transaction. 477 txn, parentTxns = txnBuilder.View() 478 txnSet = append(parentTxns, txn) 479 480 // Submit to blockchain. 481 err = tpool.AcceptTransactionSet(txnSet) 482 if err == modules.ErrDuplicateTransactionSet { 483 // As long as it made it into the transaction pool, we're good. 484 err = nil 485 } 486 if err != nil { 487 return modules.RenterContract{}, err 488 } 489 490 // Construct contract header. 491 header := contractHeader{ 492 Transaction: revisionTxn, 493 SecretKey: ourSK, 494 StartHeight: startHeight, 495 TotalCost: funding, 496 ContractFee: host.ContractPrice, 497 TxnFee: txnFee, 498 SiafundFee: types.Tax(startHeight, fc.Payout), 499 Utility: modules.ContractUtility{ 500 GoodForUpload: true, 501 GoodForRenew: true, 502 }, 503 } 504 505 // Add contract to set. 506 meta, err := cs.managedInsertContract(header, nil) // no Merkle roots yet 507 if err != nil { 508 return modules.RenterContract{}, err 509 } 510 return meta, nil 511 }