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