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  }