gitlab.com/SiaPrime/SiaPrime@v1.4.1/modules/host/negotiatedownload.go (about)

     1  package host
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"net"
     7  	"time"
     8  
     9  	"gitlab.com/SiaPrime/SiaPrime/encoding"
    10  	"gitlab.com/SiaPrime/SiaPrime/modules"
    11  	"gitlab.com/SiaPrime/SiaPrime/types"
    12  )
    13  
    14  var (
    15  	// errLargeDownloadBatch is returned if the renter requests a download
    16  	// batch that exceeds the maximum batch size that the host will
    17  	// accommodate.
    18  	errLargeDownloadBatch = ErrorCommunication("download request exceeded maximum batch size")
    19  
    20  	// errRequestOutOfBounds is returned when a download request is made which
    21  	// asks for elements of a sector which do not exist.
    22  	errRequestOutOfBounds = ErrorCommunication("download request has invalid sector bounds")
    23  )
    24  
    25  // managedDownloadIteration is responsible for managing a single iteration of
    26  // the download loop for RPCDownload.
    27  func (h *Host) managedDownloadIteration(conn net.Conn, so *storageObligation) error {
    28  	// Exchange settings with the renter.
    29  	err := h.managedRPCSettings(conn)
    30  	if err != nil {
    31  		return extendErr("RPCSettings failed: ", err)
    32  	}
    33  
    34  	// Extend the deadline for the download.
    35  	conn.SetDeadline(time.Now().Add(modules.NegotiateDownloadTime))
    36  
    37  	// The renter will either accept or reject the host's settings.
    38  	err = modules.ReadNegotiationAcceptance(conn)
    39  	if err == modules.ErrStopResponse {
    40  		return err // managedRPCDownload will catch this and exit gracefully
    41  	} else if err != nil {
    42  		return extendErr("renter rejected host settings: ", ErrorCommunication(err.Error()))
    43  	}
    44  
    45  	// Grab a set of variables that will be useful later in the function.
    46  	h.mu.Lock()
    47  	blockHeight := h.blockHeight
    48  	secretKey := h.secretKey
    49  	settings := h.externalSettings()
    50  	h.mu.Unlock()
    51  
    52  	// Read the download requests, followed by the file contract revision that
    53  	// pays for them.
    54  	var requests []modules.DownloadAction
    55  	var paymentRevision types.FileContractRevision
    56  	err = encoding.ReadObject(conn, &requests, modules.NegotiateMaxDownloadActionRequestSize)
    57  	if err != nil {
    58  		return extendErr("failed to read download requests:", ErrorConnection(err.Error()))
    59  	}
    60  	err = encoding.ReadObject(conn, &paymentRevision, modules.NegotiateMaxFileContractRevisionSize)
    61  	if err != nil {
    62  		return extendErr("failed to read payment revision:", ErrorConnection(err.Error()))
    63  	}
    64  
    65  	// Verify that the request is acceptable, and then fetch all of the data
    66  	// for the renter.
    67  	existingRevision := so.RevisionTransactionSet[len(so.RevisionTransactionSet)-1].FileContractRevisions[0]
    68  	var payload [][]byte
    69  	err = func() error {
    70  		// Check that the length of each file is in-bounds, and that the total
    71  		// size being requested is acceptable.
    72  		var totalSize uint64
    73  		for _, request := range requests {
    74  			if request.Length > modules.SectorSize || request.Offset+request.Length > modules.SectorSize {
    75  				return extendErr("download iteration request failed: ", errRequestOutOfBounds)
    76  			}
    77  			totalSize += request.Length
    78  		}
    79  		if totalSize > settings.MaxDownloadBatchSize {
    80  			return extendErr("download iteration batch failed: ", errLargeDownloadBatch)
    81  		}
    82  
    83  		// Verify that the correct amount of money has been moved from the
    84  		// renter's contract funds to the host's contract funds.
    85  		expectedTransfer := settings.DownloadBandwidthPrice.Mul64(totalSize)
    86  		err = verifyPaymentRevision(existingRevision, paymentRevision, blockHeight, expectedTransfer)
    87  		if err != nil {
    88  			return extendErr("payment verification failed: ", err)
    89  		}
    90  
    91  		// Load the sectors and build the data payload.
    92  		for _, request := range requests {
    93  			sectorData, err := h.ReadSector(request.MerkleRoot)
    94  			if err != nil {
    95  				return extendErr("failed to load sector: ", ErrorInternal(err.Error()))
    96  			}
    97  			payload = append(payload, sectorData[request.Offset:request.Offset+request.Length])
    98  		}
    99  		return nil
   100  	}()
   101  	if err != nil {
   102  		modules.WriteNegotiationRejection(conn, err) // Error not reported to preserve type in extendErr
   103  		return extendErr("download request rejected: ", err)
   104  	}
   105  	// Revision is acceptable, write acceptance.
   106  	err = modules.WriteNegotiationAcceptance(conn)
   107  	if err != nil {
   108  		return extendErr("failed to write acceptance for renter revision: ", ErrorConnection(err.Error()))
   109  	}
   110  
   111  	// Renter will send a transaction signature for the file contract revision.
   112  	var renterSignature types.TransactionSignature
   113  	err = encoding.ReadObject(conn, &renterSignature, modules.NegotiateMaxTransactionSignatureSize)
   114  	if err != nil {
   115  		return extendErr("failed to read renter signature: ", ErrorConnection(err.Error()))
   116  	}
   117  	txn, err := createRevisionSignature(paymentRevision, renterSignature, secretKey, blockHeight)
   118  
   119  	// Update the storage obligation.
   120  	paymentTransfer := existingRevision.NewValidProofOutputs[0].Value.Sub(paymentRevision.NewValidProofOutputs[0].Value)
   121  	so.PotentialDownloadRevenue = so.PotentialDownloadRevenue.Add(paymentTransfer)
   122  	so.RevisionTransactionSet = []types.Transaction{{
   123  		FileContractRevisions: []types.FileContractRevision{paymentRevision},
   124  		TransactionSignatures: []types.TransactionSignature{renterSignature, txn.TransactionSignatures[1]},
   125  	}}
   126  	h.mu.Lock()
   127  	err = h.modifyStorageObligation(*so, nil, nil, nil)
   128  	h.mu.Unlock()
   129  	if err != nil {
   130  		return extendErr("failed to modify storage obligation: ", ErrorInternal(modules.WriteNegotiationRejection(conn, err).Error()))
   131  	}
   132  
   133  	// Write acceptance to the renter - the data request can be fulfilled by
   134  	// the host, the payment is satisfactory, signature is correct. Then send
   135  	// the host signature and all of the data.
   136  	err = modules.WriteNegotiationAcceptance(conn)
   137  	if err != nil {
   138  		return extendErr("failed to write acceptance following obligation modification: ", ErrorConnection(err.Error()))
   139  	}
   140  	err = encoding.WriteObject(conn, txn.TransactionSignatures[1])
   141  	if err != nil {
   142  		return extendErr("failed to write signature: ", ErrorConnection(err.Error()))
   143  	}
   144  	err = encoding.WriteObject(conn, payload)
   145  	if err != nil {
   146  		return extendErr("failed to write payload: ", ErrorConnection(err.Error()))
   147  	}
   148  	return nil
   149  }
   150  
   151  // verifyPaymentRevision verifies that the revision being provided to pay for
   152  // the data has transferred the expected amount of money from the renter to the
   153  // host.
   154  func verifyPaymentRevision(existingRevision, paymentRevision types.FileContractRevision, blockHeight types.BlockHeight, expectedTransfer types.Currency) error {
   155  	// Check that the revision is well-formed.
   156  	if len(paymentRevision.NewValidProofOutputs) != 2 || len(paymentRevision.NewMissedProofOutputs) != 3 {
   157  		return errBadContractOutputCounts
   158  	}
   159  
   160  	// Check that the time to finalize and submit the file contract revision
   161  	// has not already passed.
   162  	if existingRevision.NewWindowStart-revisionSubmissionBuffer <= blockHeight {
   163  		return errLateRevision
   164  	}
   165  
   166  	// Host payout addresses shouldn't change
   167  	if paymentRevision.NewValidProofOutputs[1].UnlockHash != existingRevision.NewValidProofOutputs[1].UnlockHash {
   168  		return errors.New("host payout address changed")
   169  	}
   170  	if paymentRevision.NewMissedProofOutputs[1].UnlockHash != existingRevision.NewMissedProofOutputs[1].UnlockHash {
   171  		return errors.New("host payout address changed")
   172  	}
   173  	// Make sure the lost collateral still goes to the void
   174  	if paymentRevision.NewMissedProofOutputs[2].UnlockHash != existingRevision.NewMissedProofOutputs[2].UnlockHash {
   175  		return errors.New("lost collateral address was changed")
   176  	}
   177  
   178  	// Determine the amount that was transferred from the renter.
   179  	if paymentRevision.NewValidProofOutputs[0].Value.Cmp(existingRevision.NewValidProofOutputs[0].Value) > 0 {
   180  		return extendErr("renter increased its valid proof output: ", errHighRenterValidOutput)
   181  	}
   182  	fromRenter := existingRevision.NewValidProofOutputs[0].Value.Sub(paymentRevision.NewValidProofOutputs[0].Value)
   183  	// Verify that enough money was transferred.
   184  	if fromRenter.Cmp(expectedTransfer) < 0 {
   185  		s := fmt.Sprintf("expected at least %v to be exchanged, but %v was exchanged: ", expectedTransfer, fromRenter)
   186  		return extendErr(s, errHighRenterValidOutput)
   187  	}
   188  
   189  	// Determine the amount of money that was transferred to the host.
   190  	if existingRevision.NewValidProofOutputs[1].Value.Cmp(paymentRevision.NewValidProofOutputs[1].Value) > 0 {
   191  		return extendErr("host valid proof output was decreased: ", errLowHostValidOutput)
   192  	}
   193  	toHost := paymentRevision.NewValidProofOutputs[1].Value.Sub(existingRevision.NewValidProofOutputs[1].Value)
   194  	// Verify that enough money was transferred.
   195  	if !toHost.Equals(fromRenter) {
   196  		s := fmt.Sprintf("expected exactly %v to be transferred to the host, but %v was transferred: ", fromRenter, toHost)
   197  		return extendErr(s, errLowHostValidOutput)
   198  	}
   199  
   200  	// If the renter's valid proof output is larger than the renter's missed
   201  	// proof output, the renter has incentive to see the host fail. Make sure
   202  	// that this incentive is not present.
   203  	if paymentRevision.NewValidProofOutputs[0].Value.Cmp(paymentRevision.NewMissedProofOutputs[0].Value) > 0 {
   204  		return extendErr("renter has incentive to see host fail: ", errHighRenterMissedOutput)
   205  	}
   206  
   207  	// Check that the host is not going to be posting collateral.
   208  	if paymentRevision.NewMissedProofOutputs[1].Value.Cmp(existingRevision.NewMissedProofOutputs[1].Value) < 0 {
   209  		collateral := existingRevision.NewMissedProofOutputs[1].Value.Sub(paymentRevision.NewMissedProofOutputs[1].Value)
   210  		s := fmt.Sprintf("host not expecting to post any collateral, but contract has host posting %v collateral: ", collateral)
   211  		return extendErr(s, errLowHostMissedOutput)
   212  	}
   213  
   214  	// Check that the revision count has increased.
   215  	if paymentRevision.NewRevisionNumber <= existingRevision.NewRevisionNumber {
   216  		return errBadRevisionNumber
   217  	}
   218  
   219  	// Check that all of the non-volatile fields are the same.
   220  	if paymentRevision.ParentID != existingRevision.ParentID {
   221  		return errBadParentID
   222  	}
   223  	if paymentRevision.UnlockConditions.UnlockHash() != existingRevision.UnlockConditions.UnlockHash() {
   224  		return errBadUnlockConditions
   225  	}
   226  	if paymentRevision.NewFileSize != existingRevision.NewFileSize {
   227  		return errBadFileSize
   228  	}
   229  	if paymentRevision.NewFileMerkleRoot != existingRevision.NewFileMerkleRoot {
   230  		return errBadFileMerkleRoot
   231  	}
   232  	if paymentRevision.NewWindowStart != existingRevision.NewWindowStart {
   233  		return errBadWindowStart
   234  	}
   235  	if paymentRevision.NewWindowEnd != existingRevision.NewWindowEnd {
   236  		return errBadWindowEnd
   237  	}
   238  	if paymentRevision.NewUnlockHash != existingRevision.NewUnlockHash {
   239  		return errBadUnlockHash
   240  	}
   241  	if !paymentRevision.NewMissedProofOutputs[1].Value.Equals(existingRevision.NewMissedProofOutputs[1].Value) {
   242  		return errLowHostMissedOutput
   243  	}
   244  	return nil
   245  }
   246  
   247  // managedRPCDownload is responsible for handling an RPC request from the
   248  // renter to download data.
   249  func (h *Host) managedRPCDownload(conn net.Conn) error {
   250  	// Get the start time to limit the length of the whole connection.
   251  	startTime := time.Now()
   252  	// Perform the file contract revision exchange, giving the renter the most
   253  	// recent file contract revision and getting the storage obligation that
   254  	// will be used to pay for the data.
   255  	_, so, err := h.managedRPCRecentRevision(conn)
   256  	if err != nil {
   257  		return extendErr("failed RPCRecentRevision during RPCDownload: ", err)
   258  	}
   259  	// The storage obligation is returned with a lock on it. Defer a call to
   260  	// unlock the storage obligation.
   261  	defer func() {
   262  		h.managedUnlockStorageObligation(so.id())
   263  	}()
   264  
   265  	// Perform a loop that will allow downloads to happen until the maximum
   266  	// time for a single connection has been reached.
   267  	for time.Now().Before(startTime.Add(iteratedConnectionTime)) {
   268  		err := h.managedDownloadIteration(conn, &so)
   269  		if err == modules.ErrStopResponse {
   270  			// The renter has indicated that it has finished downloading the
   271  			// data, therefore there is no error. Return nil.
   272  			return nil
   273  		} else if err != nil {
   274  			return extendErr("download iteration failed: ", err)
   275  		}
   276  	}
   277  	return nil
   278  }