github.com/NebulousLabs/Sia@v1.3.7/modules/host/negotiateformcontract.go (about)

     1  package host
     2  
     3  import (
     4  	"net"
     5  	"time"
     6  
     7  	"github.com/NebulousLabs/Sia/crypto"
     8  	"github.com/NebulousLabs/Sia/encoding"
     9  	"github.com/NebulousLabs/Sia/modules"
    10  	"github.com/NebulousLabs/Sia/types"
    11  )
    12  
    13  var (
    14  	// errCollateralBudgetExceeded is returned if the host does not have enough
    15  	// room in the collateral budget to accept a particular file contract.
    16  	errCollateralBudgetExceeded = ErrorInternal("host has reached its collateral budget and cannot accept the file contract")
    17  
    18  	// errMaxCollateralReached is returned if a file contract is provided which
    19  	// would require the host to supply more collateral than the host allows
    20  	// per file contract.
    21  	errMaxCollateralReached = ErrorInternal("file contract proposal expects the host to pay more than the maximum allowed collateral")
    22  )
    23  
    24  // contractCollateral returns the amount of collateral that the host is
    25  // expected to add to the file contract based on the payout of the file
    26  // contract and based on the host settings.
    27  func contractCollateral(settings modules.HostExternalSettings, fc types.FileContract) types.Currency {
    28  	return fc.ValidProofOutputs[1].Value.Sub(settings.ContractPrice)
    29  }
    30  
    31  // managedAddCollateral adds the host's collateral to the file contract
    32  // transaction set, returning the new inputs and outputs that get added to the
    33  // transaction, as well as any new parents that get added to the transaction
    34  // set. The builder that is used to add the collateral is also returned,
    35  // because the new transaction has not yet been signed.
    36  func (h *Host) managedAddCollateral(settings modules.HostExternalSettings, txnSet []types.Transaction) (builder modules.TransactionBuilder, newParents []types.Transaction, newInputs []types.SiacoinInput, newOutputs []types.SiacoinOutput, err error) {
    37  	txn := txnSet[len(txnSet)-1]
    38  	parents := txnSet[:len(txnSet)-1]
    39  	fc := txn.FileContracts[0]
    40  	hostPortion := contractCollateral(settings, fc)
    41  	builder, err = h.wallet.RegisterTransaction(txn, parents)
    42  	if err != nil {
    43  		return
    44  	}
    45  	err = builder.FundSiacoins(hostPortion)
    46  	if err != nil {
    47  		builder.Drop()
    48  		return nil, nil, nil, nil, extendErr("could not add collateral: ", ErrorInternal(err.Error()))
    49  	}
    50  
    51  	// Return which inputs and outputs have been added by the collateral call.
    52  	newParentIndices, newInputIndices, newOutputIndices, _ := builder.ViewAdded()
    53  	updatedTxn, updatedParents := builder.View()
    54  	for _, parentIndex := range newParentIndices {
    55  		newParents = append(newParents, updatedParents[parentIndex])
    56  	}
    57  	for _, inputIndex := range newInputIndices {
    58  		newInputs = append(newInputs, updatedTxn.SiacoinInputs[inputIndex])
    59  	}
    60  	for _, outputIndex := range newOutputIndices {
    61  		newOutputs = append(newOutputs, updatedTxn.SiacoinOutputs[outputIndex])
    62  	}
    63  	return builder, newParents, newInputs, newOutputs, nil
    64  }
    65  
    66  // managedRPCFormContract accepts a file contract from a renter, checks the
    67  // file contract for compliance with the host settings, and then commits to the
    68  // file contract, creating a storage obligation and submitting the contract to
    69  // the blockchain.
    70  func (h *Host) managedRPCFormContract(conn net.Conn) error {
    71  	// Send the host settings to the renter.
    72  	err := h.managedRPCSettings(conn)
    73  	if err != nil {
    74  		return extendErr("failed RPCSettings: ", err)
    75  	}
    76  	// If the host is not accepting contracts, the connection can be closed.
    77  	// The renter has been given enough information in the host settings to
    78  	// understand that the connection is going to be closed.
    79  	h.mu.Lock()
    80  	settings := h.externalSettings()
    81  	h.mu.Unlock()
    82  	if !settings.AcceptingContracts {
    83  		h.log.Debugln("Turning down contract because the host is not accepting contracts.")
    84  		return nil
    85  	}
    86  
    87  	// Extend the deadline to meet the rest of file contract negotiation.
    88  	conn.SetDeadline(time.Now().Add(modules.NegotiateFileContractTime))
    89  
    90  	// The renter will either accept or reject the host's settings.
    91  	err = modules.ReadNegotiationAcceptance(conn)
    92  	if err != nil {
    93  		return extendErr("renter did not accept settings: ", ErrorCommunication(err.Error()))
    94  	}
    95  	// If the renter sends an acceptance of the settings, it will be followed
    96  	// by an unsigned transaction containing funding from the renter and a file
    97  	// contract which matches what the final file contract should look like.
    98  	// After the file contract, the renter will send a public key which is the
    99  	// renter's public key in the unlock conditions that protect the file
   100  	// contract from revision.
   101  	var txnSet []types.Transaction
   102  	var renterPK crypto.PublicKey
   103  	err = encoding.ReadObject(conn, &txnSet, modules.NegotiateMaxFileContractSetLen)
   104  	if err != nil {
   105  		return extendErr("could not read renter transaction set: ", ErrorConnection(err.Error()))
   106  	}
   107  	err = encoding.ReadObject(conn, &renterPK, modules.NegotiateMaxSiaPubkeySize)
   108  	if err != nil {
   109  		return extendErr("could not read renter public key: ", ErrorConnection(err.Error()))
   110  	}
   111  
   112  	// The host verifies that the file contract coming over the wire is
   113  	// acceptable.
   114  	err = h.managedVerifyNewContract(txnSet, renterPK, settings)
   115  	if err != nil {
   116  		// The incoming file contract is not acceptable to the host, indicate
   117  		// why to the renter.
   118  		modules.WriteNegotiationRejection(conn, err) // Error ignored to preserve type in extendErr
   119  		return extendErr("contract verification failed: ", err)
   120  	}
   121  	// The host adds collateral to the transaction.
   122  	txnBuilder, newParents, newInputs, newOutputs, err := h.managedAddCollateral(settings, txnSet)
   123  	if err != nil {
   124  		modules.WriteNegotiationRejection(conn, err) // Error ignored to preserve type in extendErr
   125  		return extendErr("failed to add collateral: ", err)
   126  	}
   127  	// The host indicates acceptance, and then sends any new parent
   128  	// transactions, inputs and outputs that were added to the transaction.
   129  	err = modules.WriteNegotiationAcceptance(conn)
   130  	if err != nil {
   131  		return extendErr("accepting verified contract failed: ", ErrorConnection(err.Error()))
   132  	}
   133  	err = encoding.WriteObject(conn, newParents)
   134  	if err != nil {
   135  		return extendErr("failed to write new parents: ", ErrorConnection(err.Error()))
   136  	}
   137  	err = encoding.WriteObject(conn, newInputs)
   138  	if err != nil {
   139  		return extendErr("failed to write new inputs: ", ErrorConnection(err.Error()))
   140  	}
   141  	err = encoding.WriteObject(conn, newOutputs)
   142  	if err != nil {
   143  		return extendErr("failed to write new outputs: ", ErrorConnection(err.Error()))
   144  	}
   145  
   146  	// The renter will now send a negotiation response, followed by transaction
   147  	// signatures for the file contract transaction in the case of acceptance.
   148  	// The transaction signatures will be followed by another transaction
   149  	// signature, to sign a no-op file contract revision.
   150  	err = modules.ReadNegotiationAcceptance(conn)
   151  	if err != nil {
   152  		return extendErr("renter did not accept updated transactions: ", ErrorCommunication(err.Error()))
   153  	}
   154  	var renterTxnSignatures []types.TransactionSignature
   155  	var renterRevisionSignature types.TransactionSignature
   156  	err = encoding.ReadObject(conn, &renterTxnSignatures, modules.NegotiateMaxTransactionSignaturesSize)
   157  	if err != nil {
   158  		return extendErr("could not read renter transaction signatures: ", ErrorConnection(err.Error()))
   159  	}
   160  	err = encoding.ReadObject(conn, &renterRevisionSignature, modules.NegotiateMaxTransactionSignatureSize)
   161  	if err != nil {
   162  		return extendErr("could not read renter revision signatures: ", ErrorConnection(err.Error()))
   163  	}
   164  
   165  	// The host adds the renter transaction signatures, then signs the
   166  	// transaction and submits it to the blockchain, creating a storage
   167  	// obligation in the process. The host's part is done before anything is
   168  	// written to the renter, but to give the renter confidence, the host will
   169  	// send the signatures so that the renter can immediately have the
   170  	// completed file contract.
   171  	//
   172  	// During finalization, the signature for the revision is also checked, and
   173  	// signatures for the revision transaction are created.
   174  	h.mu.RLock()
   175  	hostCollateral := contractCollateral(settings, txnSet[len(txnSet)-1].FileContracts[0])
   176  	h.mu.RUnlock()
   177  	hostTxnSignatures, hostRevisionSignature, newSOID, err := h.managedFinalizeContract(txnBuilder, renterPK, renterTxnSignatures, renterRevisionSignature, nil, hostCollateral, types.ZeroCurrency, types.ZeroCurrency, settings)
   178  	if err != nil {
   179  		// The incoming file contract is not acceptable to the host, indicate
   180  		// why to the renter.
   181  		modules.WriteNegotiationRejection(conn, err) // Error ignored to preserve type in extendErr
   182  		return extendErr("contract finalization failed: ", err)
   183  	}
   184  	defer h.managedUnlockStorageObligation(newSOID)
   185  	err = modules.WriteNegotiationAcceptance(conn)
   186  	if err != nil {
   187  		return extendErr("failed to write acceptance after contract finalization: ", ErrorConnection(err.Error()))
   188  	}
   189  	// The host sends the transaction signatures to the renter, followed by the
   190  	// revision signature. Negotiation is complete.
   191  	err = encoding.WriteObject(conn, hostTxnSignatures)
   192  	if err != nil {
   193  		return extendErr("failed to write host transaction signatures: ", ErrorConnection(err.Error()))
   194  	}
   195  	err = encoding.WriteObject(conn, hostRevisionSignature)
   196  	if err != nil {
   197  		return extendErr("failed to write host revision signatures: ", ErrorConnection(err.Error()))
   198  	}
   199  	return nil
   200  }
   201  
   202  // managedVerifyNewContract checks that an incoming file contract matches the host's
   203  // expectations for a valid contract.
   204  func (h *Host) managedVerifyNewContract(txnSet []types.Transaction, renterPK crypto.PublicKey, eSettings modules.HostExternalSettings) error {
   205  	// Check that the transaction set is not empty.
   206  	if len(txnSet) < 1 {
   207  		return extendErr("zero-length transaction set: ", errEmptyObject)
   208  	}
   209  	// Check that there is a file contract in the txnSet.
   210  	if len(txnSet[len(txnSet)-1].FileContracts) < 1 {
   211  		return extendErr("transaction without file contract: ", errEmptyObject)
   212  	}
   213  
   214  	h.mu.RLock()
   215  	blockHeight := h.blockHeight
   216  	lockedStorageCollateral := h.financialMetrics.LockedStorageCollateral
   217  	publicKey := h.publicKey
   218  	iSettings := h.settings
   219  	unlockHash := h.unlockHash
   220  	h.mu.RUnlock()
   221  	fc := txnSet[len(txnSet)-1].FileContracts[0]
   222  
   223  	// A new file contract should have a file size of zero.
   224  	if fc.FileSize != 0 {
   225  		return errBadFileSize
   226  	}
   227  	if fc.FileMerkleRoot != (crypto.Hash{}) {
   228  		return errBadFileMerkleRoot
   229  	}
   230  	// WindowStart must be at least revisionSubmissionBuffer blocks into the
   231  	// future.
   232  	if fc.WindowStart <= blockHeight+revisionSubmissionBuffer {
   233  		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)
   234  		return errEarlyWindow
   235  	}
   236  	// WindowEnd must be at least settings.WindowSize blocks after
   237  	// WindowStart.
   238  	if fc.WindowEnd < fc.WindowStart+eSettings.WindowSize {
   239  		return errSmallWindow
   240  	}
   241  	// WindowStart must not be more than settings.MaxDuration blocks into the
   242  	// future.
   243  	if fc.WindowStart > blockHeight+eSettings.MaxDuration {
   244  		return errLongDuration
   245  	}
   246  
   247  	// ValidProofOutputs shoud have 2 outputs (renter + host) and missed
   248  	// outputs should have 3 (renter + host + void)
   249  	if len(fc.ValidProofOutputs) != 2 || len(fc.MissedProofOutputs) != 3 {
   250  		return errBadContractOutputCounts
   251  	}
   252  	// The unlock hashes of the valid and missed proof outputs for the host
   253  	// must match the host's unlock hash. The third missed output should point
   254  	// to the void.
   255  	if fc.ValidProofOutputs[1].UnlockHash != unlockHash || fc.MissedProofOutputs[1].UnlockHash != unlockHash || fc.MissedProofOutputs[2].UnlockHash != (types.UnlockHash{}) {
   256  		return errBadPayoutUnlockHashes
   257  	}
   258  	// Check that the payouts for the valid proof outputs and the missed proof
   259  	// outputs are the same - this is important because no data has been added
   260  	// to the file contract yet.
   261  	if !fc.ValidProofOutputs[1].Value.Equals(fc.MissedProofOutputs[1].Value) {
   262  		return errMismatchedHostPayouts
   263  	}
   264  	// Check that there's enough payout for the host to cover at least the
   265  	// contract price. This will prevent negative currency panics when working
   266  	// with the collateral.
   267  	if fc.ValidProofOutputs[1].Value.Cmp(eSettings.ContractPrice) < 0 {
   268  		return errLowHostValidOutput
   269  	}
   270  	// Check that the collateral does not exceed the maximum amount of
   271  	// collateral allowed.
   272  	expectedCollateral := contractCollateral(eSettings, fc)
   273  	if expectedCollateral.Cmp(eSettings.MaxCollateral) > 0 {
   274  		return errMaxCollateralReached
   275  	}
   276  	// Check that the host has enough room in the collateral budget to add this
   277  	// collateral.
   278  	if lockedStorageCollateral.Add(expectedCollateral).Cmp(iSettings.CollateralBudget) > 0 {
   279  		return errCollateralBudgetExceeded
   280  	}
   281  
   282  	// The unlock hash for the file contract must match the unlock hash that
   283  	// the host knows how to spend.
   284  	expectedUH := types.UnlockConditions{
   285  		PublicKeys: []types.SiaPublicKey{
   286  			types.Ed25519PublicKey(renterPK),
   287  			publicKey,
   288  		},
   289  		SignaturesRequired: 2,
   290  	}.UnlockHash()
   291  	if fc.UnlockHash != expectedUH {
   292  		return errBadUnlockHash
   293  	}
   294  
   295  	// Check that the transaction set has enough fees on it to get into the
   296  	// blockchain.
   297  	setFee := modules.CalculateFee(txnSet)
   298  	minFee, _ := h.tpool.FeeEstimation()
   299  	if setFee.Cmp(minFee) < 0 {
   300  		return errLowTransactionFees
   301  	}
   302  	return nil
   303  }