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