github.com/johnathanhowell/sia@v0.5.1-beta.0.20160524050156-83dcc3d37c94/modules/host/negotiateformcontract.go (about)

     1  package host
     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  var (
    15  	// errBadContractUnlockHash is returned when the host receives a file
    16  	// contract where it does not understand the unlock hash driving the
    17  	// contract.
    18  	errBadContractUnlockHash = errors.New("file contract has an unexpected unlock hash")
    19  
    20  	// errBadFileSize is returned if a file contract is provided by the renter
    21  	// which does not have the right file size.
    22  	errBadFileSize = errors.New("new file contract does not have the right file size")
    23  
    24  	// errBadFileMerkleRoot is returned if a file contract is provided by the
    25  	// renter which does not have the right file size.
    26  	errBadFileMerkleRoot = errors.New("new file contract does not have the right file Merkle root")
    27  
    28  	// errBadPayoutsLen is returned if a new file contract is presented that
    29  	// has the wrong number of valid or missed proof payouts.
    30  	errBadPayoutsLen = errors.New("file contract has the wrong number of payouts - there should be two valid and three missed payouts")
    31  
    32  	// errBadPayoutsAmounts is returned if a new file contract is presented that
    33  	// does not pay the correct amount to the host - by default, the payouts
    34  	// should be paying the contract price.
    35  	errBadPayoutsAmounts = errors.New("file contract has payouts that do not correctly cover the contract price")
    36  
    37  	// errBadPayoutsUnlockHashes is returned if a new file contract is
    38  	// presented that does not make payments to the correct addresses.
    39  	errBadPayoutsUnlockHashes = errors.New("file contract has payouts which pay to the wrong unlock hashes for the host")
    40  
    41  	// errCollateralBudgetExceeded is returned if the host does not have enough
    42  	// room in the collateral budget to accept a particular file contract.
    43  	errCollateralBudgetExceeded = errors.New("host has reached its collateral budget and cannot accept the file contract")
    44  
    45  	// errDurationTooLong is returned if the renter proposes a file contract
    46  	// which is longer than the host's maximum duration.
    47  	errDurationTooLong = errors.New("file contract has a duration which exceeds the duration permitted by the host")
    48  
    49  	// errEmptyFileContractTransactionSet is returned if the renter provides a
    50  	// nil file contract transaction set during file contract negotiation.
    51  	errEmptyFileContractTransactionSet = errors.New("file contract transaction set is empty")
    52  
    53  	// errLowHostPayout is returned if the host is not getting paid enough in
    54  	// the file contract to cover the contract price.
    55  	errLowHostPayout = errors.New("file contract payout does not cover the contract cost")
    56  
    57  	// errLowFees is returned if a transaction set provided by the renter does
    58  	// not have large enough transaction fees to have a reasonalbe chance at
    59  	// making it onto the blockchain.
    60  	errLowFees = errors.New("file contract proposal does not have enough transaction fees to be acceptable")
    61  
    62  	// errMaxCollateralReached is returned if a file contract is provided which
    63  	// would require the host to supply more collateral than the host allows
    64  	// per file contract.
    65  	errMaxCollateralReached = errors.New("file contract proposal expects the host to pay more than the maximum allowed collateral")
    66  
    67  	// errNoFileContract is returned if a transaction set is sent that does not
    68  	// have a file contract.
    69  	errNoFileContract = errors.New("transaction set does not have a file contract")
    70  
    71  	// errWindowSizeTooSmall is returned if a file contract has a window size
    72  	// (defined by fc.WindowEnd - fc.WindowStart) which is too small to be
    73  	// acceptable to the host - the host needs to submit its storage proof to
    74  	// the blockchain inside of that window.
    75  	errWindowSizeTooSmall = errors.New("file contract has a storage proof window which is not wide enough to match the host's requirements")
    76  
    77  	// errWindowStartTooSoon is returned if the storage proof window for the
    78  	// file contract opens too soon into the future - the host needs time to
    79  	// submit the file contract and all revisions to the blockchain before the
    80  	// storage proof window opens.
    81  	errWindowStartTooSoon = errors.New("the storage proof window is opening too soon")
    82  )
    83  
    84  // contractCollateral returns the amount of collateral that the host is
    85  // expected to add to the file contract based on the payout of the file
    86  // contract and based on the host settings.
    87  func contractCollateral(settings modules.HostInternalSettings, fc types.FileContract) types.Currency {
    88  	return fc.ValidProofOutputs[1].Value.Sub(settings.MinimumContractPrice)
    89  }
    90  
    91  // managedAddCollateral adds the host's collateral to the file contract
    92  // transaction set, returning the new inputs and outputs that get added to the
    93  // transaction, as well as any new parents that get added to the transaction
    94  // set. The builder that is used to add the collateral is also returned,
    95  // because the new transaction has not yet been signed.
    96  func (h *Host) managedAddCollateral(settings modules.HostInternalSettings, txnSet []types.Transaction) (builder modules.TransactionBuilder, newParents []types.Transaction, newInputs []types.SiacoinInput, newOutputs []types.SiacoinOutput, err error) {
    97  	txn := txnSet[len(txnSet)-1]
    98  	parents := txnSet[:len(txnSet)-1]
    99  	fc := txn.FileContracts[0]
   100  	hostPortion := contractCollateral(settings, fc)
   101  	builder = h.wallet.RegisterTransaction(txn, parents)
   102  	err = builder.FundSiacoins(hostPortion)
   103  	if err != nil {
   104  		builder.Drop()
   105  		return nil, nil, nil, nil, err
   106  	}
   107  
   108  	// Return which inputs and outputs have been added by the collateral call.
   109  	newParentIndices, newInputIndices, newOutputIndices, _ := builder.ViewAdded()
   110  	updatedTxn, updatedParents := builder.View()
   111  	for _, parentIndex := range newParentIndices {
   112  		newParents = append(newParents, updatedParents[parentIndex])
   113  	}
   114  	for _, inputIndex := range newInputIndices {
   115  		newInputs = append(newInputs, updatedTxn.SiacoinInputs[inputIndex])
   116  	}
   117  	for _, outputIndex := range newOutputIndices {
   118  		newOutputs = append(newOutputs, updatedTxn.SiacoinOutputs[outputIndex])
   119  	}
   120  	return builder, newParents, newInputs, newOutputs, nil
   121  }
   122  
   123  // managedFinalizeContract will take a file contract, add the host's
   124  // collateral, and then try submitting the file contract to the transaction
   125  // pool. If there is no error, the completed transaction set will be returned
   126  // to the caller.
   127  func (h *Host) managedFinalizeContract(builder modules.TransactionBuilder, renterPK crypto.PublicKey, renterSignatures []types.TransactionSignature, renterRevisionSignature types.TransactionSignature) ([]types.TransactionSignature, types.TransactionSignature, error) {
   128  	for _, sig := range renterSignatures {
   129  		builder.AddTransactionSignature(sig)
   130  	}
   131  	fullTxnSet, err := builder.Sign(true)
   132  	if err != nil {
   133  		builder.Drop()
   134  		return nil, types.TransactionSignature{}, err
   135  	}
   136  
   137  	// Verify that the signature for the revision from the renter is correct.
   138  	h.mu.RLock()
   139  	blockHeight := h.blockHeight
   140  	hostSPK := h.publicKey
   141  	hostSK := h.secretKey
   142  	h.mu.RUnlock()
   143  	contractTxn := fullTxnSet[len(fullTxnSet)-1]
   144  	fc := contractTxn.FileContracts[0]
   145  	noOpRevision := types.FileContractRevision{
   146  		ParentID: contractTxn.FileContractID(0),
   147  		UnlockConditions: types.UnlockConditions{
   148  			PublicKeys: []types.SiaPublicKey{
   149  				{
   150  					Algorithm: types.SignatureEd25519,
   151  					Key:       renterPK[:],
   152  				},
   153  				hostSPK,
   154  			},
   155  			SignaturesRequired: 2,
   156  		},
   157  		NewRevisionNumber: fc.RevisionNumber + 1,
   158  
   159  		NewFileSize:           fc.FileSize,
   160  		NewFileMerkleRoot:     fc.FileMerkleRoot,
   161  		NewWindowStart:        fc.WindowStart,
   162  		NewWindowEnd:          fc.WindowEnd,
   163  		NewValidProofOutputs:  fc.ValidProofOutputs,
   164  		NewMissedProofOutputs: fc.MissedProofOutputs,
   165  		NewUnlockHash:         fc.UnlockHash,
   166  	}
   167  	// createRevisionSignature will also perform validation on the result,
   168  	// returning an error if the renter.
   169  	revisionTransaction, err := createRevisionSignature(noOpRevision, renterRevisionSignature, hostSK, blockHeight)
   170  	if err != nil {
   171  		return nil, types.TransactionSignature{}, err
   172  	}
   173  
   174  	// Create and add the storage obligation for this file contract.
   175  	h.mu.Lock()
   176  	defer h.mu.Unlock()
   177  	fullTxn, _ := builder.View()
   178  	hostPortion := contractCollateral(h.settings, fc)
   179  	so := &storageObligation{
   180  		ContractCost:     h.settings.MinimumContractPrice,
   181  		LockedCollateral: hostPortion,
   182  
   183  		OriginTransactionSet:   fullTxnSet,
   184  		RevisionTransactionSet: []types.Transaction{revisionTransaction},
   185  	}
   186  	lockErr := h.lockStorageObligation(so)
   187  	if lockErr != nil {
   188  		return nil, types.TransactionSignature{}, lockErr
   189  	}
   190  	// addStorageObligation will submit the transaction to the transaction
   191  	// pool, and will only do so if there was not some error in creating the
   192  	// storage obligation.
   193  	err = h.addStorageObligation(so)
   194  	lockErr = h.unlockStorageObligation(so)
   195  	if lockErr != nil {
   196  		return nil, types.TransactionSignature{}, lockErr
   197  	}
   198  	if err != nil {
   199  		// AcceptingContracts is set to false in the event of an error, because
   200  		// it means that the host is having some type of disk error. Under
   201  		// normal circumstances, adding a storage obligation should not cause
   202  		// problems unexpectedly.
   203  		h.log.Println(err)
   204  		h.settings.AcceptingContracts = false
   205  		builder.Drop()
   206  		return nil, types.TransactionSignature{}, err
   207  	}
   208  
   209  	// Get the host's transaction signatures from the builder.
   210  	var hostTxnSignatures []types.TransactionSignature
   211  	_, _, _, txnSigIndices := builder.ViewAdded()
   212  	for _, sigIndex := range txnSigIndices {
   213  		hostTxnSignatures = append(hostTxnSignatures, fullTxn.TransactionSignatures[sigIndex])
   214  	}
   215  	return hostTxnSignatures, revisionTransaction.TransactionSignatures[1], nil
   216  }
   217  
   218  // managedRPCFormContract accepts a file contract from a renter, checks the
   219  // file contract for compliance with the host settings, and then commits to the
   220  // file contract, creating a storage obligation and submitting the contract to
   221  // the blockchain.
   222  func (h *Host) managedRPCFormContract(conn net.Conn) error {
   223  	// Send the host settings to the renter.
   224  	err := h.managedRPCSettings(conn)
   225  	if err != nil {
   226  		return err
   227  	}
   228  	// If the host is not accepting contracts, the connection can be closed.
   229  	// The renter has been given enough information in the host settings to
   230  	// understand that the connection is going to be closed.
   231  	h.mu.RLock()
   232  	settings := h.settings
   233  	h.mu.RUnlock()
   234  	if !settings.AcceptingContracts {
   235  		return nil
   236  	}
   237  
   238  	// Extend the deadline to meet the rest of file contract negotiation.
   239  	conn.SetDeadline(time.Now().Add(modules.NegotiateFileContractTime))
   240  
   241  	// The renter will either accept or reject the host's settings.
   242  	err = modules.ReadNegotiationAcceptance(conn)
   243  	if err != nil {
   244  		return err
   245  	}
   246  	// If the renter sends an acceptance of the settings, it will be followed
   247  	// by an unsigned transaction containing funding from the renter and a file
   248  	// contract which matches what the final file contract should look like.
   249  	// After the file contract, the renter will send a public key which is the
   250  	// renter's public key in the unlock conditions that protect the file
   251  	// contract from revision.
   252  	var txnSet []types.Transaction
   253  	var renterPK crypto.PublicKey
   254  	err = encoding.ReadObject(conn, &txnSet, modules.NegotiateMaxFileContractSetLen)
   255  	if err != nil {
   256  		return err
   257  	}
   258  	err = encoding.ReadObject(conn, &renterPK, modules.NegotiateMaxSiaPubkeySize)
   259  	if err != nil {
   260  		return err
   261  	}
   262  
   263  	// The host verifies that the file contract coming over the wire is
   264  	// acceptable.
   265  	err = h.managedVerifyNewContract(txnSet, renterPK)
   266  	if err != nil {
   267  		// The incoming file contract is not acceptable to the host, indicate
   268  		// why to the renter.
   269  		return modules.WriteNegotiationRejection(conn, err)
   270  	}
   271  	// The host adds collateral to the transaction.
   272  	txnBuilder, newParents, newInputs, newOutputs, err := h.managedAddCollateral(settings, txnSet)
   273  	if err != nil {
   274  		return modules.WriteNegotiationRejection(conn, err)
   275  	}
   276  	// The host indicates acceptance, and then sends any new parent
   277  	// transactions, inputs and outputs that were added to the transaction.
   278  	err = modules.WriteNegotiationAcceptance(conn)
   279  	if err != nil {
   280  		return err
   281  	}
   282  	err = encoding.WriteObject(conn, newParents)
   283  	if err != nil {
   284  		return err
   285  	}
   286  	err = encoding.WriteObject(conn, newInputs)
   287  	if err != nil {
   288  		return err
   289  	}
   290  	err = encoding.WriteObject(conn, newOutputs)
   291  	if err != nil {
   292  		return err
   293  	}
   294  
   295  	// The renter will now send a negotiation response, followed by transaction
   296  	// signatures for the file contract transaction in the case of acceptance.
   297  	// The transaction signatures will be followed by another transaction
   298  	// siganture, to sign a no-op file contract revision.
   299  	err = modules.ReadNegotiationAcceptance(conn)
   300  	if err != nil {
   301  		return err
   302  	}
   303  	var renterTxnSignatures []types.TransactionSignature
   304  	var renterRevisionSignature types.TransactionSignature
   305  	err = encoding.ReadObject(conn, &renterTxnSignatures, modules.NegotiateMaxTransactionSignaturesSize)
   306  	if err != nil {
   307  		return err
   308  	}
   309  	err = encoding.ReadObject(conn, &renterRevisionSignature, modules.NegotiateMaxTransactionSignatureSize)
   310  	if err != nil {
   311  		return err
   312  	}
   313  
   314  	// The host adds the renter transaction signatures, then signs the
   315  	// transaction and submits it to the blockchain, creating a storage
   316  	// obligation in the process. The host's part is done before anything is
   317  	// written to the renter, but to give the renter confidence, the host will
   318  	// send the signatures so that the renter can immediately have the
   319  	// completed file contract.
   320  	//
   321  	// During finalization, the siganture for the revision is also checked, and
   322  	// signatures for the revision transaction are created.
   323  	hostTxnSignatures, hostRevisionSignature, err := h.managedFinalizeContract(txnBuilder, renterPK, renterTxnSignatures, renterRevisionSignature)
   324  	if err != nil {
   325  		// The incoming file contract is not acceptable to the host, indicate
   326  		// why to the renter.
   327  		return modules.WriteNegotiationRejection(conn, err)
   328  	}
   329  	err = modules.WriteNegotiationAcceptance(conn)
   330  	if err != nil {
   331  		return err
   332  	}
   333  	// The host sends the transaction signatures to the renter, followed by the
   334  	// revision signature. Negotiation is complete.
   335  	err = encoding.WriteObject(conn, hostTxnSignatures)
   336  	if err != nil {
   337  		return err
   338  	}
   339  	return encoding.WriteObject(conn, hostRevisionSignature)
   340  }
   341  
   342  // managedVerifyNewContract checks that an incoming file contract matches the host's
   343  // expectations for a valid contract.
   344  func (h *Host) managedVerifyNewContract(txnSet []types.Transaction, renterPK crypto.PublicKey) error {
   345  	// Check that the transaction set is not empty.
   346  	if len(txnSet) < 1 {
   347  		return errEmptyFileContractTransactionSet
   348  	}
   349  	// Check that there is a file contract in the txnSet.
   350  	if len(txnSet[len(txnSet)-1].FileContracts) < 1 {
   351  		return errNoFileContract
   352  	}
   353  
   354  	h.mu.RLock()
   355  	blockHeight := h.blockHeight
   356  	lockedStorageCollateral := h.financialMetrics.LockedStorageCollateral
   357  	publicKey := h.publicKey
   358  	settings := h.settings
   359  	unlockHash := h.unlockHash
   360  	h.mu.RUnlock()
   361  	fc := txnSet[len(txnSet)-1].FileContracts[0]
   362  
   363  	// A new file contract should have a file size of zero.
   364  	if fc.FileSize != 0 {
   365  		return errBadFileSize
   366  	}
   367  	if fc.FileMerkleRoot != (crypto.Hash{}) {
   368  		return errBadFileMerkleRoot
   369  	}
   370  	// WindowStart must be at least revisionSubmissionBuffer blocks into the
   371  	// future.
   372  	if fc.WindowStart <= blockHeight+revisionSubmissionBuffer {
   373  		h.log.Debugf("A renter tried to form a contract that had a window start which was too soon. The contract started at %v, the current height is %v, the revisionSubmissionBuffer is %v, and the comparison was %v <= %v\n", fc.WindowStart, blockHeight, revisionSubmissionBuffer, fc.WindowStart, blockHeight+revisionSubmissionBuffer)
   374  		return errWindowStartTooSoon
   375  	}
   376  	// WindowEnd must be at least settings.WindowSize blocks after
   377  	// WindowStart.
   378  	if fc.WindowEnd < fc.WindowStart+settings.WindowSize {
   379  		return errWindowSizeTooSmall
   380  	}
   381  	// WindowEnd must not be more than settings.MaxDuration blocks into the
   382  	// future.
   383  	if fc.WindowStart > blockHeight+settings.MaxDuration {
   384  		return errDurationTooLong
   385  	}
   386  
   387  	// ValidProofOutputs shoud have 2 outputs (renter + host) and missed
   388  	// outputs should have 3 (renter + host + void)
   389  	if len(fc.ValidProofOutputs) != 2 || len(fc.MissedProofOutputs) != 3 {
   390  		return errBadPayoutsLen
   391  	}
   392  	// The unlock hashes of the valid and missed proof outputs for the host
   393  	// must match the host's unlock hash. The third missed output should point
   394  	// to the void.
   395  	if fc.ValidProofOutputs[1].UnlockHash != unlockHash || fc.MissedProofOutputs[1].UnlockHash != unlockHash || fc.MissedProofOutputs[2].UnlockHash != (types.UnlockHash{}) {
   396  		return errBadPayoutsUnlockHashes
   397  	}
   398  	// Check that the payouts for the valid proof outputs and the missed proof
   399  	// outputs are the same - this is important because no data has been added
   400  	// to the file contract yet.
   401  	if fc.ValidProofOutputs[1].Value.Cmp(fc.MissedProofOutputs[1].Value) != 0 {
   402  		return errBadPayoutsAmounts
   403  	}
   404  	// Check that there's enough payout for the host to cover at least the
   405  	// contract price. This will prevent negative currency panics when working
   406  	// with the collateral.
   407  	if fc.ValidProofOutputs[1].Value.Cmp(settings.MinimumContractPrice) < 0 {
   408  		return errLowHostPayout
   409  	}
   410  	// Check that the collateral does not exceed the maximum amount of
   411  	// collateral allowed.
   412  	expectedCollateral := contractCollateral(settings, fc)
   413  	if expectedCollateral.Cmp(settings.MaxCollateral) > 0 {
   414  		return errMaxCollateralReached
   415  	}
   416  	// Check that the host has enough room in the collateral budget to add this
   417  	// collateral.
   418  	if lockedStorageCollateral.Add(expectedCollateral).Cmp(settings.CollateralBudget) > 0 {
   419  		return errCollateralBudgetExceeded
   420  	}
   421  
   422  	// The unlock hash for the file contract must match the unlock hash that
   423  	// the host knows how to spend.
   424  	expectedUH := types.UnlockConditions{
   425  		PublicKeys: []types.SiaPublicKey{
   426  			{
   427  				Algorithm: types.SignatureEd25519,
   428  				Key:       renterPK[:],
   429  			},
   430  			publicKey,
   431  		},
   432  		SignaturesRequired: 2,
   433  	}.UnlockHash()
   434  	if fc.UnlockHash != expectedUH {
   435  		return errBadContractUnlockHash
   436  	}
   437  
   438  	// Check that the transaction set has enough fees on it to get into the
   439  	// blockchain.
   440  	setFee := modules.CalculateFee(txnSet)
   441  	minFee, _ := h.tpool.FeeEstimation()
   442  	if setFee.Cmp(minFee) < 0 {
   443  		return errLowFees
   444  	}
   445  	return nil
   446  }