github.com/avahowell/sia@v0.5.1-beta.0.20160524050156-83dcc3d37c94/modules/renter/proto/formcontract.go (about)

     1  package proto
     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  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.
    22  func FormContract(params ContractParams, txnBuilder transactionBuilder, tpool transactionPool) (modules.RenterContract, error) {
    23  	// extract vars from params, for convenience
    24  	host, filesize, startHeight, endHeight, refundAddress := params.Host, params.Filesize, params.StartHeight, params.EndHeight, params.RefundAddress
    25  
    26  	// create our key
    27  	ourSK, ourPK, err := crypto.GenerateKeyPair()
    28  	if err != nil {
    29  		return modules.RenterContract{}, err
    30  	}
    31  	ourPublicKey := types.SiaPublicKey{
    32  		Algorithm: types.SignatureEd25519,
    33  		Key:       ourPK[:],
    34  	}
    35  	// create unlock conditions
    36  	uc := types.UnlockConditions{
    37  		PublicKeys:         []types.SiaPublicKey{ourPublicKey, host.PublicKey},
    38  		SignaturesRequired: 2,
    39  	}
    40  
    41  	// calculate cost to renter and cost to host
    42  	// TODO: clarify/abstract this math
    43  	storageAllocation := host.StoragePrice.Mul64(filesize).Mul64(uint64(endHeight - startHeight))
    44  	hostCollateral := host.Collateral.Mul64(filesize).Mul64(uint64(endHeight - startHeight))
    45  	if hostCollateral.Cmp(host.MaxCollateral) > 0 {
    46  		// TODO: if we have to cap the collateral, it probably means we shouldn't be using this host
    47  		// (ok within a factor of 2)
    48  		hostCollateral = host.MaxCollateral
    49  	}
    50  	hostPayout := hostCollateral.Add(host.ContractPrice)
    51  	payout := storageAllocation.Add(hostPayout).Mul64(10406).Div64(10000) // renter pays for siafund fee
    52  	renterCost := payout.Sub(hostCollateral)
    53  
    54  	// create file contract
    55  	fc := types.FileContract{
    56  		FileSize:       0,
    57  		FileMerkleRoot: crypto.Hash{}, // no proof possible without data
    58  		WindowStart:    endHeight,
    59  		WindowEnd:      endHeight + host.WindowSize,
    60  		Payout:         payout,
    61  		UnlockHash:     uc.UnlockHash(),
    62  		RevisionNumber: 0,
    63  		ValidProofOutputs: []types.SiacoinOutput{
    64  			// outputs need to account for tax
    65  			{Value: types.PostTax(startHeight, payout).Sub(hostPayout), UnlockHash: refundAddress},
    66  			// collateral is returned to host
    67  			{Value: hostPayout, UnlockHash: host.UnlockHash},
    68  		},
    69  		MissedProofOutputs: []types.SiacoinOutput{
    70  			// same as above
    71  			{Value: types.PostTax(startHeight, payout).Sub(hostPayout), UnlockHash: refundAddress},
    72  			// same as above
    73  			{Value: hostPayout, UnlockHash: host.UnlockHash},
    74  			// once we start doing revisions, we'll move some coins to the host and some to the void
    75  			{Value: types.ZeroCurrency, UnlockHash: types.UnlockHash{}},
    76  		},
    77  	}
    78  
    79  	// calculate transaction fee
    80  	_, maxFee := tpool.FeeEstimation()
    81  	fee := maxFee.Mul64(estTxnSize)
    82  
    83  	// build transaction containing fc
    84  	err = txnBuilder.FundSiacoins(renterCost.Add(fee))
    85  	if err != nil {
    86  		return modules.RenterContract{}, err
    87  	}
    88  	txnBuilder.AddFileContract(fc)
    89  
    90  	// add miner fee
    91  	txnBuilder.AddMinerFee(fee)
    92  
    93  	// create initial transaction set
    94  	txn, parentTxns := txnBuilder.View()
    95  	txnSet := append(parentTxns, txn)
    96  
    97  	// initiate connection
    98  	conn, err := net.DialTimeout("tcp", string(host.NetAddress), 15*time.Second)
    99  	if err != nil {
   100  		return modules.RenterContract{}, err
   101  	}
   102  	defer func() { _ = conn.Close() }()
   103  
   104  	// allot time for sending RPC ID + verifySettings
   105  	extendDeadline(conn, modules.NegotiateSettingsTime)
   106  	if err = encoding.WriteObject(conn, modules.RPCFormContract); err != nil {
   107  		return modules.RenterContract{}, err
   108  	}
   109  
   110  	// verify the host's settings and confirm its identity
   111  	host, err = verifySettings(conn, host)
   112  	if err != nil {
   113  		return modules.RenterContract{}, err
   114  	}
   115  	if !host.AcceptingContracts {
   116  		return modules.RenterContract{}, errors.New("host is not accepting contracts")
   117  	}
   118  
   119  	// allot time for negotiation
   120  	extendDeadline(conn, modules.NegotiateFileContractTime)
   121  
   122  	// send acceptance, txn signed by us, and pubkey
   123  	if err = modules.WriteNegotiationAcceptance(conn); err != nil {
   124  		return modules.RenterContract{}, errors.New("couldn't send initial acceptance: " + err.Error())
   125  	}
   126  	if err = encoding.WriteObject(conn, txnSet); err != nil {
   127  		return modules.RenterContract{}, errors.New("couldn't send the contract signed by us: " + err.Error())
   128  	}
   129  	if err = encoding.WriteObject(conn, ourSK.PublicKey()); err != nil {
   130  		return modules.RenterContract{}, errors.New("couldn't send our public key: " + err.Error())
   131  	}
   132  
   133  	// read acceptance and txn signed by host
   134  	if err = modules.ReadNegotiationAcceptance(conn); err != nil {
   135  		return modules.RenterContract{}, errors.New("host did not accept our proposed contract: " + err.Error())
   136  	}
   137  	// host now sends any new parent transactions, inputs and outputs that
   138  	// were added to the transaction
   139  	var newParents []types.Transaction
   140  	var newInputs []types.SiacoinInput
   141  	var newOutputs []types.SiacoinOutput
   142  	if err = encoding.ReadObject(conn, &newParents, types.BlockSizeLimit); err != nil {
   143  		return modules.RenterContract{}, errors.New("couldn't read the host's added parents: " + err.Error())
   144  	}
   145  	if err = encoding.ReadObject(conn, &newInputs, types.BlockSizeLimit); err != nil {
   146  		return modules.RenterContract{}, errors.New("couldn't read the host's added inputs: " + err.Error())
   147  	}
   148  	if err = encoding.ReadObject(conn, &newOutputs, types.BlockSizeLimit); err != nil {
   149  		return modules.RenterContract{}, errors.New("couldn't read the host's added outputs: " + err.Error())
   150  	}
   151  
   152  	// merge txnAdditions with txnSet
   153  	txnBuilder.AddParents(newParents)
   154  	for _, input := range newInputs {
   155  		txnBuilder.AddSiacoinInput(input)
   156  	}
   157  	for _, output := range newOutputs {
   158  		txnBuilder.AddSiacoinOutput(output)
   159  	}
   160  
   161  	// sign the txn
   162  	signedTxnSet, err := txnBuilder.Sign(true)
   163  	if err != nil {
   164  		return modules.RenterContract{}, modules.WriteNegotiationRejection(conn, errors.New("failed to sign transaction: "+err.Error()))
   165  	}
   166  
   167  	// calculate signatures added by the transaction builder
   168  	var addedSignatures []types.TransactionSignature
   169  	_, _, _, addedSignatureIndices := txnBuilder.ViewAdded()
   170  	for _, i := range addedSignatureIndices {
   171  		addedSignatures = append(addedSignatures, signedTxnSet[len(signedTxnSet)-1].TransactionSignatures[i])
   172  	}
   173  
   174  	// create initial (no-op) revision, transaction, and signature
   175  	initRevision := types.FileContractRevision{
   176  		ParentID:          signedTxnSet[len(signedTxnSet)-1].FileContractID(0),
   177  		UnlockConditions:  uc,
   178  		NewRevisionNumber: 1,
   179  
   180  		NewFileSize:           fc.FileSize,
   181  		NewFileMerkleRoot:     fc.FileMerkleRoot,
   182  		NewWindowStart:        fc.WindowStart,
   183  		NewWindowEnd:          fc.WindowEnd,
   184  		NewValidProofOutputs:  fc.ValidProofOutputs,
   185  		NewMissedProofOutputs: fc.MissedProofOutputs,
   186  		NewUnlockHash:         fc.UnlockHash,
   187  	}
   188  	renterRevisionSig := types.TransactionSignature{
   189  		ParentID:       crypto.Hash(initRevision.ParentID),
   190  		PublicKeyIndex: 0,
   191  		CoveredFields: types.CoveredFields{
   192  			FileContractRevisions: []uint64{0},
   193  		},
   194  	}
   195  	revisionTxn := types.Transaction{
   196  		FileContractRevisions: []types.FileContractRevision{initRevision},
   197  		TransactionSignatures: []types.TransactionSignature{renterRevisionSig},
   198  	}
   199  	encodedSig, err := crypto.SignHash(revisionTxn.SigHash(0), ourSK)
   200  	if err != nil {
   201  		return modules.RenterContract{}, modules.WriteNegotiationRejection(conn, errors.New("failed to sign revision transaction: "+err.Error()))
   202  	}
   203  	revisionTxn.TransactionSignatures[0].Signature = encodedSig[:]
   204  
   205  	// Send acceptance and signatures
   206  	if err = modules.WriteNegotiationAcceptance(conn); err != nil {
   207  		return modules.RenterContract{}, errors.New("couldn't send transaction acceptance: " + err.Error())
   208  	}
   209  	if err = encoding.WriteObject(conn, addedSignatures); err != nil {
   210  		return modules.RenterContract{}, errors.New("couldn't send added signatures: " + err.Error())
   211  	}
   212  	if err = encoding.WriteObject(conn, revisionTxn.TransactionSignatures[0]); err != nil {
   213  		return modules.RenterContract{}, errors.New("couldn't send revision signature: " + err.Error())
   214  	}
   215  
   216  	// Read the host acceptance and signatures.
   217  	err = modules.ReadNegotiationAcceptance(conn)
   218  	if err != nil {
   219  		return modules.RenterContract{}, errors.New("host did not accept our signatures: " + err.Error())
   220  	}
   221  	var hostSigs []types.TransactionSignature
   222  	if err = encoding.ReadObject(conn, &hostSigs, 2e3); err != nil {
   223  		return modules.RenterContract{}, errors.New("couldn't read the host's signatures: " + err.Error())
   224  	}
   225  	for _, sig := range hostSigs {
   226  		txnBuilder.AddTransactionSignature(sig)
   227  	}
   228  	var hostRevisionSig types.TransactionSignature
   229  	if err = encoding.ReadObject(conn, &hostRevisionSig, 2e3); err != nil {
   230  		return modules.RenterContract{}, errors.New("couldn't read the host's revision signature: " + err.Error())
   231  	}
   232  	revisionTxn.TransactionSignatures = append(revisionTxn.TransactionSignatures, hostRevisionSig)
   233  
   234  	// Construct the final transaction.
   235  	txn, parentTxns = txnBuilder.View()
   236  	txnSet = append(parentTxns, txn)
   237  
   238  	// Submit to blockchain.
   239  	err = tpool.AcceptTransactionSet(txnSet)
   240  	if err == modules.ErrDuplicateTransactionSet {
   241  		// as long as it made it into the transaction pool, we're good
   242  		err = nil
   243  	}
   244  	if err != nil {
   245  		return modules.RenterContract{}, err
   246  	}
   247  
   248  	// calculate contract ID
   249  	fcid := txn.FileContractID(0)
   250  
   251  	return modules.RenterContract{
   252  		FileContract:    fc,
   253  		ID:              fcid,
   254  		LastRevision:    initRevision,
   255  		LastRevisionTxn: revisionTxn,
   256  		NetAddress:      host.NetAddress,
   257  		SecretKey:       ourSK,
   258  	}, nil
   259  }