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

     1  package host
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"net"
     7  	"time"
     8  
     9  	"gitlab.com/SiaPrime/SiaPrime/crypto"
    10  	"gitlab.com/SiaPrime/SiaPrime/encoding"
    11  	"gitlab.com/SiaPrime/SiaPrime/modules"
    12  	"gitlab.com/SiaPrime/SiaPrime/types"
    13  )
    14  
    15  // cachedMerkleRoot calculates the root of a set of sector roots.
    16  func cachedMerkleRoot(roots []crypto.Hash) crypto.Hash {
    17  	log2SectorSize := uint64(0)
    18  	for 1<<log2SectorSize < (modules.SectorSize / crypto.SegmentSize) {
    19  		log2SectorSize++
    20  	}
    21  	ct := crypto.NewCachedTree(log2SectorSize)
    22  	for _, root := range roots {
    23  		ct.Push(root)
    24  	}
    25  	return ct.Root()
    26  }
    27  
    28  // managedRevisionIteration handles one iteration of the revision loop. As a
    29  // performance optimization, multiple iterations of revisions are allowed to be
    30  // made over the same connection.
    31  func (h *Host) managedRevisionIteration(conn net.Conn, so *storageObligation, finalIter bool) error {
    32  	// Send the settings to the renter. The host will keep going even if it is
    33  	// not accepting contracts, because in this case the contract already
    34  	// exists.
    35  	err := h.managedRPCSettings(conn)
    36  	if err != nil {
    37  		return extendErr("RPCSettings failed: ", err)
    38  	}
    39  
    40  	// Set the negotiation deadline.
    41  	conn.SetDeadline(time.Now().Add(modules.NegotiateFileContractRevisionTime))
    42  
    43  	// The renter will either accept or reject the settings + revision
    44  	// transaction. It may also return a stop response to indicate that it
    45  	// wishes to terminate the revision loop.
    46  	err = modules.ReadNegotiationAcceptance(conn)
    47  	if err == modules.ErrStopResponse {
    48  		return err // managedRPCReviseContract will catch this and exit gracefully
    49  	} else if err != nil {
    50  		return extendErr("renter rejected host settings: ", ErrorCommunication(err.Error()))
    51  	}
    52  
    53  	// Read some variables from the host for use later in the function.
    54  	h.mu.Lock()
    55  	settings := h.externalSettings()
    56  	secretKey := h.secretKey
    57  	blockHeight := h.blockHeight
    58  	h.mu.Unlock()
    59  
    60  	// The renter is going to send its intended modifications, followed by the
    61  	// file contract revision that pays for them.
    62  	var modifications []modules.RevisionAction
    63  	var revision types.FileContractRevision
    64  	err = encoding.ReadObject(conn, &modifications, settings.MaxReviseBatchSize)
    65  	if err != nil {
    66  		return extendErr("unable to read revision modifications: ", ErrorConnection(err.Error()))
    67  	}
    68  	err = encoding.ReadObject(conn, &revision, modules.NegotiateMaxFileContractRevisionSize)
    69  	if err != nil {
    70  		return extendErr("unable to read proposed revision: ", ErrorConnection(err.Error()))
    71  	}
    72  
    73  	// First read all of the modifications. Then make the modifications, but
    74  	// with the ability to reverse them. Then verify the file contract revision
    75  	// correctly accounts for the changes.
    76  	var bandwidthRevenue types.Currency // Upload bandwidth.
    77  	var storageRevenue types.Currency
    78  	var newCollateral types.Currency
    79  	var sectorsRemoved []crypto.Hash
    80  	var sectorsGained []crypto.Hash
    81  	var gainedSectorData [][]byte
    82  	err = func() error {
    83  		for _, modification := range modifications {
    84  			// Check that the index points to an existing sector root. If the type
    85  			// is ActionInsert, we permit inserting at the end.
    86  			if modification.Type == modules.ActionInsert {
    87  				if modification.SectorIndex > uint64(len(so.SectorRoots)) {
    88  					return errBadModificationIndex
    89  				}
    90  			} else if modification.SectorIndex >= uint64(len(so.SectorRoots)) {
    91  				return errBadModificationIndex
    92  			}
    93  			// Check that the data sent for the sector is not too large.
    94  			if uint64(len(modification.Data)) > modules.SectorSize {
    95  				return errLargeSector
    96  			}
    97  
    98  			switch modification.Type {
    99  			case modules.ActionDelete:
   100  				// There is no financial information to change, it is enough to
   101  				// remove the sector.
   102  				sectorsRemoved = append(sectorsRemoved, so.SectorRoots[modification.SectorIndex])
   103  				so.SectorRoots = append(so.SectorRoots[0:modification.SectorIndex], so.SectorRoots[modification.SectorIndex+1:]...)
   104  			case modules.ActionInsert:
   105  				// Check that the sector size is correct.
   106  				if uint64(len(modification.Data)) != modules.SectorSize {
   107  					return errBadSectorSize
   108  				}
   109  
   110  				// Update finances.
   111  				blocksRemaining := so.proofDeadline() - blockHeight
   112  				blockBytesCurrency := types.NewCurrency64(uint64(blocksRemaining)).Mul64(modules.SectorSize)
   113  				bandwidthRevenue = bandwidthRevenue.Add(settings.UploadBandwidthPrice.Mul64(modules.SectorSize))
   114  				storageRevenue = storageRevenue.Add(settings.StoragePrice.Mul(blockBytesCurrency))
   115  				newCollateral = newCollateral.Add(settings.Collateral.Mul(blockBytesCurrency))
   116  
   117  				// Insert the sector into the root list.
   118  				newRoot := crypto.MerkleRoot(modification.Data)
   119  				sectorsGained = append(sectorsGained, newRoot)
   120  				gainedSectorData = append(gainedSectorData, modification.Data)
   121  				so.SectorRoots = append(so.SectorRoots[:modification.SectorIndex], append([]crypto.Hash{newRoot}, so.SectorRoots[modification.SectorIndex:]...)...)
   122  			case modules.ActionModify:
   123  				// Check that the offset and length are okay. Length is already
   124  				// known to be appropriately small, but the offset needs to be
   125  				// checked for being appropriately small as well otherwise there is
   126  				// a risk of overflow.
   127  				if modification.Offset > modules.SectorSize || modification.Offset+uint64(len(modification.Data)) > modules.SectorSize {
   128  					return errIllegalOffsetAndLength
   129  				}
   130  
   131  				// Get the data for the new sector.
   132  				sector, err := h.ReadSector(so.SectorRoots[modification.SectorIndex])
   133  				if err != nil {
   134  					return extendErr("could not read sector: ", ErrorInternal(err.Error()))
   135  				}
   136  				copy(sector[modification.Offset:], modification.Data)
   137  
   138  				// Update finances.
   139  				bandwidthRevenue = bandwidthRevenue.Add(settings.UploadBandwidthPrice.Mul64(uint64(len(modification.Data))))
   140  
   141  				// Update the sectors removed and gained to indicate that the old
   142  				// sector has been replaced with a new sector.
   143  				newRoot := crypto.MerkleRoot(sector)
   144  				sectorsRemoved = append(sectorsRemoved, so.SectorRoots[modification.SectorIndex])
   145  				sectorsGained = append(sectorsGained, newRoot)
   146  				gainedSectorData = append(gainedSectorData, sector)
   147  				so.SectorRoots[modification.SectorIndex] = newRoot
   148  			default:
   149  				return errUnknownModification
   150  			}
   151  		}
   152  		newRevenue := storageRevenue.Add(bandwidthRevenue)
   153  		return extendErr("unable to verify updated contract: ", verifyRevision(*so, revision, blockHeight, newRevenue, newCollateral))
   154  	}()
   155  	if err != nil {
   156  		modules.WriteNegotiationRejection(conn, err) // Error is ignored so that the error type can be preserved in extendErr.
   157  		return extendErr("rejected proposed modifications: ", err)
   158  	}
   159  	// Revision is acceptable, write an acceptance string.
   160  	err = modules.WriteNegotiationAcceptance(conn)
   161  	if err != nil {
   162  		return extendErr("could not accept revision modifications: ", ErrorConnection(err.Error()))
   163  	}
   164  
   165  	// Renter will send a transaction signature for the file contract revision.
   166  	var renterSig types.TransactionSignature
   167  	err = encoding.ReadObject(conn, &renterSig, modules.NegotiateMaxTransactionSignatureSize)
   168  	if err != nil {
   169  		return extendErr("could not read renter transaction signature: ", ErrorConnection(err.Error()))
   170  	}
   171  	// Verify that the signature is valid and get the host's signature.
   172  	txn, err := createRevisionSignature(revision, renterSig, secretKey, blockHeight)
   173  	if err != nil {
   174  		modules.WriteNegotiationRejection(conn, err) // Error is ignored so that the error type can be preserved in extendErr.
   175  		return extendErr("could not create revision signature: ", err)
   176  	}
   177  
   178  	so.PotentialStorageRevenue = so.PotentialStorageRevenue.Add(storageRevenue)
   179  	so.RiskedCollateral = so.RiskedCollateral.Add(newCollateral)
   180  	so.PotentialUploadRevenue = so.PotentialUploadRevenue.Add(bandwidthRevenue)
   181  	so.RevisionTransactionSet = []types.Transaction{txn}
   182  	h.mu.Lock()
   183  	err = h.modifyStorageObligation(*so, sectorsRemoved, sectorsGained, gainedSectorData)
   184  	h.mu.Unlock()
   185  	if err != nil {
   186  		modules.WriteNegotiationRejection(conn, err) // Error is ignored so that the error type can be preserved in extendErr.
   187  		return extendErr("could not modify storage obligation: ", ErrorInternal(err.Error()))
   188  	}
   189  
   190  	// Host will now send acceptance and its signature to the renter. This
   191  	// iteration is complete. If the finalIter flag is set, StopResponse will
   192  	// be sent instead. This indicates to the renter that the host wishes to
   193  	// terminate the revision loop.
   194  	if finalIter {
   195  		err = modules.WriteNegotiationStop(conn)
   196  	} else {
   197  		err = modules.WriteNegotiationAcceptance(conn)
   198  	}
   199  	if err != nil {
   200  		return extendErr("iteration signal failed to send: ", ErrorConnection(err.Error()))
   201  	}
   202  	err = encoding.WriteObject(conn, txn.TransactionSignatures[1])
   203  	if err != nil {
   204  		return extendErr("failed to write revision signatures: ", ErrorConnection(err.Error()))
   205  	}
   206  	return nil
   207  }
   208  
   209  // managedRPCReviseContract accepts a request to revise an existing contract.
   210  // Revisions can add sectors, delete sectors, and modify existing sectors.
   211  func (h *Host) managedRPCReviseContract(conn net.Conn) error {
   212  	// Set a preliminary deadline for receiving the storage obligation.
   213  	startTime := time.Now()
   214  	// Perform the file contract revision exchange, giving the renter the most
   215  	// recent file contract revision and getting the storage obligation that
   216  	// will be used to pay for the data.
   217  	_, so, err := h.managedRPCRecentRevision(conn)
   218  	if err != nil {
   219  		return extendErr("failed RPCRecentRevision during RPCReviseContract: ", err)
   220  	}
   221  	// The storage obligation is received with a lock on it. Defer a call to
   222  	// unlock the storage obligation.
   223  	defer func() {
   224  		h.managedUnlockStorageObligation(so.id())
   225  	}()
   226  
   227  	// Begin the revision loop. The host will process revisions until a
   228  	// timeout is reached, or until the renter sends a StopResponse.
   229  	for timeoutReached := false; !timeoutReached; {
   230  		timeoutReached = time.Since(startTime) > iteratedConnectionTime
   231  		err := h.managedRevisionIteration(conn, &so, timeoutReached)
   232  		if err == modules.ErrStopResponse {
   233  			return nil
   234  		} else if err != nil {
   235  			return extendErr("revision iteration failed: ", err)
   236  		}
   237  	}
   238  	return nil
   239  }
   240  
   241  // verifyRevision checks that the revision pays the host correctly, and that
   242  // the revision does not attempt any malicious or unexpected changes.
   243  func verifyRevision(so storageObligation, revision types.FileContractRevision, blockHeight types.BlockHeight, expectedExchange, expectedCollateral types.Currency) error {
   244  	// Check that the revision is well-formed.
   245  	if len(revision.NewValidProofOutputs) != 2 || len(revision.NewMissedProofOutputs) != 3 {
   246  		return errBadContractOutputCounts
   247  	}
   248  
   249  	// Check that the time to finalize and submit the file contract revision
   250  	// has not already passed.
   251  	if so.expiration()-revisionSubmissionBuffer <= blockHeight {
   252  		return errLateRevision
   253  	}
   254  
   255  	oldFCR := so.RevisionTransactionSet[len(so.RevisionTransactionSet)-1].FileContractRevisions[0]
   256  
   257  	// Host payout addresses shouldn't change
   258  	if revision.NewValidProofOutputs[1].UnlockHash != oldFCR.NewValidProofOutputs[1].UnlockHash {
   259  		return errors.New("host payout address changed")
   260  	}
   261  	if revision.NewMissedProofOutputs[1].UnlockHash != oldFCR.NewMissedProofOutputs[1].UnlockHash {
   262  		return errors.New("host payout address changed")
   263  	}
   264  	// Make sure the lost collateral still goes to the void
   265  	if revision.NewMissedProofOutputs[2].UnlockHash != oldFCR.NewMissedProofOutputs[2].UnlockHash {
   266  		return errors.New("lost collateral address was changed")
   267  	}
   268  
   269  	// Check that all non-volatile fields are the same.
   270  	if oldFCR.ParentID != revision.ParentID {
   271  		return errBadContractParent
   272  	}
   273  	if oldFCR.UnlockConditions.UnlockHash() != revision.UnlockConditions.UnlockHash() {
   274  		return errBadUnlockConditions
   275  	}
   276  	if oldFCR.NewRevisionNumber >= revision.NewRevisionNumber {
   277  		return errBadRevisionNumber
   278  	}
   279  	if revision.NewFileSize != uint64(len(so.SectorRoots))*modules.SectorSize {
   280  		return errBadFileSize
   281  	}
   282  	if oldFCR.NewWindowStart != revision.NewWindowStart {
   283  		return errBadWindowStart
   284  	}
   285  	if oldFCR.NewWindowEnd != revision.NewWindowEnd {
   286  		return errBadWindowEnd
   287  	}
   288  	if oldFCR.NewUnlockHash != revision.NewUnlockHash {
   289  		return errBadUnlockHash
   290  	}
   291  
   292  	// Determine the amount that was transferred from the renter.
   293  	if revision.NewValidProofOutputs[0].Value.Cmp(oldFCR.NewValidProofOutputs[0].Value) > 0 {
   294  		return extendErr("renter increased its valid proof output: ", errHighRenterValidOutput)
   295  	}
   296  	fromRenter := oldFCR.NewValidProofOutputs[0].Value.Sub(revision.NewValidProofOutputs[0].Value)
   297  	// Verify that enough money was transferred.
   298  	if fromRenter.Cmp(expectedExchange) < 0 {
   299  		s := fmt.Sprintf("expected at least %v to be exchanged, but %v was exchanged: ", expectedExchange, fromRenter)
   300  		return extendErr(s, errHighRenterValidOutput)
   301  	}
   302  
   303  	// Determine the amount of money that was transferred to the host.
   304  	if oldFCR.NewValidProofOutputs[1].Value.Cmp(revision.NewValidProofOutputs[1].Value) > 0 {
   305  		return extendErr("host valid proof output was decreased: ", errLowHostValidOutput)
   306  	}
   307  	toHost := revision.NewValidProofOutputs[1].Value.Sub(oldFCR.NewValidProofOutputs[1].Value)
   308  	// Verify that enough money was transferred.
   309  	if !toHost.Equals(fromRenter) {
   310  		s := fmt.Sprintf("expected exactly %v to be transferred to the host, but %v was transferred: ", fromRenter, toHost)
   311  		return extendErr(s, errLowHostValidOutput)
   312  	}
   313  
   314  	// If the renter's valid proof output is larger than the renter's missed
   315  	// proof output, the renter has incentive to see the host fail. Make sure
   316  	// that this incentive is not present.
   317  	if revision.NewValidProofOutputs[0].Value.Cmp(revision.NewMissedProofOutputs[0].Value) > 0 {
   318  		return extendErr("renter has incentive to see host fail: ", errHighRenterMissedOutput)
   319  	}
   320  
   321  	// Check that the host is not going to be posting more collateral than is
   322  	// expected. If the new misesd output is greater than the old one, the host
   323  	// is actually posting negative collateral, which is fine.
   324  	if revision.NewMissedProofOutputs[1].Value.Cmp(oldFCR.NewMissedProofOutputs[1].Value) <= 0 {
   325  		collateral := oldFCR.NewMissedProofOutputs[1].Value.Sub(revision.NewMissedProofOutputs[1].Value)
   326  		if collateral.Cmp(expectedCollateral) > 0 {
   327  			s := fmt.Sprintf("host expected to post at most %v collateral, but contract has host posting %v: ", expectedCollateral, collateral)
   328  			return extendErr(s, errLowHostMissedOutput)
   329  		}
   330  	}
   331  
   332  	// Check that the revision count has increased.
   333  	if revision.NewRevisionNumber <= oldFCR.NewRevisionNumber {
   334  		return errBadRevisionNumber
   335  	}
   336  
   337  	// The Merkle root is checked last because it is the most expensive check.
   338  	if revision.NewFileMerkleRoot != cachedMerkleRoot(so.SectorRoots) {
   339  		return errBadFileMerkleRoot
   340  	}
   341  
   342  	return nil
   343  }