github.com/nebulouslabs/sia@v1.3.7/modules/host/negotiaterevisecontract.go (about)

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