github.com/nebulouslabs/sia@v1.3.7/modules/renter/proto/formcontract.go (about)

     1  package proto
     2  
     3  import (
     4  	"net"
     5  
     6  	"github.com/NebulousLabs/Sia/crypto"
     7  	"github.com/NebulousLabs/Sia/encoding"
     8  	"github.com/NebulousLabs/Sia/modules"
     9  	"github.com/NebulousLabs/Sia/types"
    10  
    11  	"github.com/NebulousLabs/errors"
    12  )
    13  
    14  // FormContract forms a contract with a host and submits the contract
    15  // transaction to tpool. The contract is added to the ContractSet and its
    16  // metadata is returned.
    17  func (cs *ContractSet) FormContract(params ContractParams, txnBuilder transactionBuilder, tpool transactionPool, hdb hostDB, cancel <-chan struct{}) (rc modules.RenterContract, err error) {
    18  	// Extract vars from params, for convenience.
    19  	host, funding, startHeight, endHeight, refundAddress := params.Host, params.Funding, params.StartHeight, params.EndHeight, params.RefundAddress
    20  
    21  	// Create our key.
    22  	ourSK, ourPK := crypto.GenerateKeyPair()
    23  	// Create unlock conditions.
    24  	uc := types.UnlockConditions{
    25  		PublicKeys: []types.SiaPublicKey{
    26  			types.Ed25519PublicKey(ourPK),
    27  			host.PublicKey,
    28  		},
    29  		SignaturesRequired: 2,
    30  	}
    31  
    32  	// Calculate the anticipated transaction fee.
    33  	_, maxFee := tpool.FeeEstimation()
    34  	txnFee := maxFee.Mul64(modules.EstimatedFileContractTransactionSetSize)
    35  
    36  	// Underflow check.
    37  	if funding.Cmp(host.ContractPrice.Add(txnFee)) <= 0 {
    38  		return modules.RenterContract{}, errors.New("insufficient funds to cover contract fee and transaction fee during contract formation")
    39  	}
    40  	// Divide by zero check.
    41  	if host.StoragePrice.IsZero() {
    42  		host.StoragePrice = types.NewCurrency64(1)
    43  	}
    44  
    45  	// Calculate the payouts for the renter, host, and whole contract.
    46  	renterPayout := funding.Sub(host.ContractPrice).Sub(txnFee) // renter payout is pre-tax
    47  	maxStorageSize := renterPayout.Div(host.StoragePrice)
    48  	hostCollateral := maxStorageSize.Mul(host.Collateral)
    49  	if hostCollateral.Cmp(host.MaxCollateral) > 0 {
    50  		hostCollateral = host.MaxCollateral
    51  	}
    52  	// Calculate the initial host payout.
    53  	hostPayout := hostCollateral.Add(host.ContractPrice)
    54  	totalPayout := renterPayout.Add(hostPayout)
    55  
    56  	// Check for negative currency.
    57  	if types.PostTax(startHeight, totalPayout).Cmp(hostPayout) < 0 {
    58  		return modules.RenterContract{}, errors.New("not enough money to pay both siafund fee and also host payout")
    59  	}
    60  	// Create file contract.
    61  	fc := types.FileContract{
    62  		FileSize:       0,
    63  		FileMerkleRoot: crypto.Hash{}, // no proof possible without data
    64  		WindowStart:    endHeight,
    65  		WindowEnd:      endHeight + host.WindowSize,
    66  		Payout:         totalPayout,
    67  		UnlockHash:     uc.UnlockHash(),
    68  		RevisionNumber: 0,
    69  		ValidProofOutputs: []types.SiacoinOutput{
    70  			// Outputs need to account for tax.
    71  			{Value: types.PostTax(startHeight, totalPayout).Sub(hostPayout), UnlockHash: refundAddress}, // This is the renter payout, but with tax applied.
    72  			// Collateral is returned to host.
    73  			{Value: hostPayout, UnlockHash: host.UnlockHash},
    74  		},
    75  		MissedProofOutputs: []types.SiacoinOutput{
    76  			// Same as above.
    77  			{Value: types.PostTax(startHeight, totalPayout).Sub(hostPayout), UnlockHash: refundAddress},
    78  			// Same as above.
    79  			{Value: hostPayout, UnlockHash: host.UnlockHash},
    80  			// Once we start doing revisions, we'll move some coins to the host and some to the void.
    81  			{Value: types.ZeroCurrency, UnlockHash: types.UnlockHash{}},
    82  		},
    83  	}
    84  
    85  	// Build transaction containing fc, e.g. the File Contract.
    86  	err = txnBuilder.FundSiacoins(funding)
    87  	if err != nil {
    88  		return modules.RenterContract{}, err
    89  	}
    90  	txnBuilder.AddFileContract(fc)
    91  	// Add miner fee.
    92  	txnBuilder.AddMinerFee(txnFee)
    93  
    94  	// Create initial transaction set.
    95  	txn, parentTxns := txnBuilder.View()
    96  	unconfirmedParents, err := txnBuilder.UnconfirmedParents()
    97  	if err != nil {
    98  		return modules.RenterContract{}, err
    99  	}
   100  	txnSet := append(unconfirmedParents, append(parentTxns, txn)...)
   101  
   102  	// Increase Successful/Failed interactions accordingly
   103  	defer func() {
   104  		if err != nil {
   105  			hdb.IncrementFailedInteractions(host.PublicKey)
   106  			err = errors.Extend(err, modules.ErrHostFault)
   107  		} else {
   108  			hdb.IncrementSuccessfulInteractions(host.PublicKey)
   109  		}
   110  	}()
   111  
   112  	// Initiate connection.
   113  	dialer := &net.Dialer{
   114  		Cancel:  cancel,
   115  		Timeout: connTimeout,
   116  	}
   117  	conn, err := dialer.Dial("tcp", string(host.NetAddress))
   118  	if err != nil {
   119  		return modules.RenterContract{}, err
   120  	}
   121  	defer func() { _ = conn.Close() }()
   122  
   123  	// Allot time for sending RPC ID + verifySettings.
   124  	extendDeadline(conn, modules.NegotiateSettingsTime)
   125  	if err = encoding.WriteObject(conn, modules.RPCFormContract); err != nil {
   126  		return modules.RenterContract{}, err
   127  	}
   128  
   129  	// Verify the host's settings and confirm its identity.
   130  	host, err = verifySettings(conn, host)
   131  	if err != nil {
   132  		return modules.RenterContract{}, err
   133  	}
   134  	if !host.AcceptingContracts {
   135  		return modules.RenterContract{}, errors.New("host is not accepting contracts")
   136  	}
   137  
   138  	// Allot time for negotiation.
   139  	extendDeadline(conn, modules.NegotiateFileContractTime)
   140  
   141  	// Send acceptance, txn signed by us, and pubkey.
   142  	if err = modules.WriteNegotiationAcceptance(conn); err != nil {
   143  		return modules.RenterContract{}, errors.New("couldn't send initial acceptance: " + err.Error())
   144  	}
   145  	if err = encoding.WriteObject(conn, txnSet); err != nil {
   146  		return modules.RenterContract{}, errors.New("couldn't send the contract signed by us: " + err.Error())
   147  	}
   148  	if err = encoding.WriteObject(conn, ourSK.PublicKey()); err != nil {
   149  		return modules.RenterContract{}, errors.New("couldn't send our public key: " + err.Error())
   150  	}
   151  
   152  	// Read acceptance and txn signed by host.
   153  	if err = modules.ReadNegotiationAcceptance(conn); err != nil {
   154  		return modules.RenterContract{}, errors.New("host did not accept our proposed contract: " + err.Error())
   155  	}
   156  	// Host now sends any new parent transactions, inputs and outputs that
   157  	// were added to the transaction.
   158  	var newParents []types.Transaction
   159  	var newInputs []types.SiacoinInput
   160  	var newOutputs []types.SiacoinOutput
   161  	if err = encoding.ReadObject(conn, &newParents, types.BlockSizeLimit); err != nil {
   162  		return modules.RenterContract{}, errors.New("couldn't read the host's added parents: " + err.Error())
   163  	}
   164  	if err = encoding.ReadObject(conn, &newInputs, types.BlockSizeLimit); err != nil {
   165  		return modules.RenterContract{}, errors.New("couldn't read the host's added inputs: " + err.Error())
   166  	}
   167  	if err = encoding.ReadObject(conn, &newOutputs, types.BlockSizeLimit); err != nil {
   168  		return modules.RenterContract{}, errors.New("couldn't read the host's added outputs: " + err.Error())
   169  	}
   170  
   171  	// Merge txnAdditions with txnSet.
   172  	txnBuilder.AddParents(newParents)
   173  	for _, input := range newInputs {
   174  		txnBuilder.AddSiacoinInput(input)
   175  	}
   176  	for _, output := range newOutputs {
   177  		txnBuilder.AddSiacoinOutput(output)
   178  	}
   179  
   180  	// Sign the txn.
   181  	signedTxnSet, err := txnBuilder.Sign(true)
   182  	if err != nil {
   183  		return modules.RenterContract{}, modules.WriteNegotiationRejection(conn, errors.New("failed to sign transaction: "+err.Error()))
   184  	}
   185  
   186  	// Calculate signatures added by the transaction builder.
   187  	var addedSignatures []types.TransactionSignature
   188  	_, _, _, addedSignatureIndices := txnBuilder.ViewAdded()
   189  	for _, i := range addedSignatureIndices {
   190  		addedSignatures = append(addedSignatures, signedTxnSet[len(signedTxnSet)-1].TransactionSignatures[i])
   191  	}
   192  
   193  	// create initial (no-op) revision, transaction, and signature
   194  	initRevision := types.FileContractRevision{
   195  		ParentID:          signedTxnSet[len(signedTxnSet)-1].FileContractID(0),
   196  		UnlockConditions:  uc,
   197  		NewRevisionNumber: 1,
   198  
   199  		NewFileSize:           fc.FileSize,
   200  		NewFileMerkleRoot:     fc.FileMerkleRoot,
   201  		NewWindowStart:        fc.WindowStart,
   202  		NewWindowEnd:          fc.WindowEnd,
   203  		NewValidProofOutputs:  fc.ValidProofOutputs,
   204  		NewMissedProofOutputs: fc.MissedProofOutputs,
   205  		NewUnlockHash:         fc.UnlockHash,
   206  	}
   207  	renterRevisionSig := types.TransactionSignature{
   208  		ParentID:       crypto.Hash(initRevision.ParentID),
   209  		PublicKeyIndex: 0,
   210  		CoveredFields: types.CoveredFields{
   211  			FileContractRevisions: []uint64{0},
   212  		},
   213  	}
   214  	revisionTxn := types.Transaction{
   215  		FileContractRevisions: []types.FileContractRevision{initRevision},
   216  		TransactionSignatures: []types.TransactionSignature{renterRevisionSig},
   217  	}
   218  	encodedSig := crypto.SignHash(revisionTxn.SigHash(0), ourSK)
   219  	revisionTxn.TransactionSignatures[0].Signature = encodedSig[:]
   220  
   221  	// Send acceptance and signatures.
   222  	if err = modules.WriteNegotiationAcceptance(conn); err != nil {
   223  		return modules.RenterContract{}, errors.New("couldn't send transaction acceptance: " + err.Error())
   224  	}
   225  	if err = encoding.WriteObject(conn, addedSignatures); err != nil {
   226  		return modules.RenterContract{}, errors.New("couldn't send added signatures: " + err.Error())
   227  	}
   228  	if err = encoding.WriteObject(conn, revisionTxn.TransactionSignatures[0]); err != nil {
   229  		return modules.RenterContract{}, errors.New("couldn't send revision signature: " + err.Error())
   230  	}
   231  
   232  	// Read the host acceptance and signatures.
   233  	err = modules.ReadNegotiationAcceptance(conn)
   234  	if err != nil {
   235  		return modules.RenterContract{}, errors.New("host did not accept our signatures: " + err.Error())
   236  	}
   237  	var hostSigs []types.TransactionSignature
   238  	if err = encoding.ReadObject(conn, &hostSigs, 2e3); err != nil {
   239  		return modules.RenterContract{}, errors.New("couldn't read the host's signatures: " + err.Error())
   240  	}
   241  	for _, sig := range hostSigs {
   242  		txnBuilder.AddTransactionSignature(sig)
   243  	}
   244  	var hostRevisionSig types.TransactionSignature
   245  	if err = encoding.ReadObject(conn, &hostRevisionSig, 2e3); err != nil {
   246  		return modules.RenterContract{}, errors.New("couldn't read the host's revision signature: " + err.Error())
   247  	}
   248  	revisionTxn.TransactionSignatures = append(revisionTxn.TransactionSignatures, hostRevisionSig)
   249  
   250  	// Construct the final transaction.
   251  	txn, parentTxns = txnBuilder.View()
   252  	txnSet = append(parentTxns, txn)
   253  
   254  	// Submit to blockchain.
   255  	err = tpool.AcceptTransactionSet(txnSet)
   256  	if err == modules.ErrDuplicateTransactionSet {
   257  		// As long as it made it into the transaction pool, we're good.
   258  		err = nil
   259  	}
   260  	if err != nil {
   261  		return modules.RenterContract{}, err
   262  	}
   263  
   264  	// Construct contract header.
   265  	header := contractHeader{
   266  		Transaction: revisionTxn,
   267  		SecretKey:   ourSK,
   268  		StartHeight: startHeight,
   269  		TotalCost:   funding,
   270  		ContractFee: host.ContractPrice,
   271  		TxnFee:      txnFee,
   272  		SiafundFee:  types.Tax(startHeight, fc.Payout),
   273  		Utility: modules.ContractUtility{
   274  			GoodForUpload: true,
   275  			GoodForRenew:  true,
   276  		},
   277  	}
   278  
   279  	// Add contract to set.
   280  	meta, err := cs.managedInsertContract(header, nil) // no Merkle roots yet
   281  	if err != nil {
   282  		return modules.RenterContract{}, err
   283  	}
   284  	return meta, nil
   285  }