github.com/johnathanhowell/sia@v0.5.1-beta.0.20160524050156-83dcc3d37c94/modules/host/negotiaterevisecontract.go (about)

     1  package host
     2  
     3  import (
     4  	"errors"
     5  	"net"
     6  	"time"
     7  
     8  	"github.com/NebulousLabs/Sia/crypto"
     9  	"github.com/NebulousLabs/Sia/encoding"
    10  	"github.com/NebulousLabs/Sia/modules"
    11  	"github.com/NebulousLabs/Sia/types"
    12  )
    13  
    14  var (
    15  	// errBadModificationIndex is returned if the renter requests a change on a
    16  	// sector root that is not in the file contract.
    17  	errBadModificationIndex = errors.New("renter has made a modification that points to a nonexistent sector")
    18  
    19  	// badSectorSize is returned if the renter provides a sector to be inserted
    20  	// that is the wrong size.
    21  	errBadSectorSize = errors.New("renter has provided an incorrectly sized sector")
    22  
    23  	// errIllegalOffsetAndLength is returned if the renter tries perform a
    24  	// modify operation that uses a troublesome combination of offset and
    25  	// length.
    26  	errIllegalOffsetAndLength = errors.New("renter is trying to do a modify with an illegal offset and length")
    27  
    28  	// errLargeSector is returned if the renter sends a RevisionAction that has
    29  	// data which creates a sector that is larger than what the host uses.
    30  	errLargeSector = errors.New("renter has sent a sector that exceeds the host's sector size")
    31  
    32  	// errLateRevision is returned if the renter is attempting to revise a
    33  	// revision after the revision deadline. The host needs time to submit the
    34  	// final revision to the blockchain to guarantee payment, and therefore
    35  	// will not accept revisions once the window start is too close.
    36  	errLateRevision = errors.New("renter is attempting to revise a revision which the host has closed")
    37  
    38  	// errReviseBadCollateralDeduction is returned if a proposed file contrct
    39  	// revision does not correctly deduct value from the host's missed proof
    40  	// output - which is the host's collateral pool.
    41  	errReviseBadCollateralDeduction = errors.New("proposed file contract revision does not correctly deduct from the host's collateral pool")
    42  
    43  	// errReviseBadFileMerkleRoot is returned if the renter sends a file
    44  	// contract revision with a Merkle root that does not match the changes
    45  	// presented by the revision request.
    46  	errReviseBadFileMerkleRoot = errors.New("proposed file contract revision has an incorrect Merkle merkle root")
    47  
    48  	// errReviseBadHostValidOutput is returned if a proposed file contract
    49  	// revision does not correctly add value to the host's valid proof outputs.
    50  	errReviseBadHostValidOutput = errors.New("proposed file contract revision does not correctly add to the host's valid proof output")
    51  
    52  	// errReviseBadNewFileSize is returned if a proposed file contract revision
    53  	// does not have a file size which matches the revisions that have been
    54  	// made.
    55  	errReviseBadNewFileSize = errors.New("propsed file contract revision has a bad filesize")
    56  
    57  	// errReviseBadNewWindowEnd is returned if a proposed file contract
    58  	// revision does not have a window end which matches the original file
    59  	// contract revision.
    60  	errReviseBadNewWindowEnd = errors.New("proposed file contract revision has a bad window end")
    61  
    62  	// errReviseBadNewWindowStart is returned if a proposed file contract
    63  	// revision does not have a window start which matches the window start of
    64  	// the original file contract.
    65  	errReviseBadNewWindowStart = errors.New("propsed file contract revision has a bad window start")
    66  
    67  	// errReviseBadParent is returned when a file contract revision is
    68  	// presented which has a parent id that doesn't match the file contract
    69  	// which is supposed to be getting revised.
    70  	errReviseBadParent = errors.New("proposed file contract revision has the wrong parent id")
    71  
    72  	// errReviseBadRenterValidOutput is returned if a propsed file contract
    73  	// revision does not corectly deduct value from the renter's valid proof
    74  	// output.
    75  	errReviseBadRenterValidOutput = errors.New("proposed file contract revision does not correctly deduct from the renter's valid proof output")
    76  
    77  	// errReviseBadRenterMissedOutput is returned if a proposed file contract
    78  	// revision does not correctly deduct value from the renter's missed proof
    79  	// output.
    80  	errReviseBadRenterMissedOutput = errors.New("proposed file contract revision does not correctly deduct from the renter's missed proof output")
    81  
    82  	// errReviseBadRevisionNumber is returned if a proposed file contract
    83  	// revision does not have a revision number which is strictly greater than
    84  	// the most recent revision number for the file contract being modified.
    85  	errReviseBadRevisionNumber = errors.New("proposed file contract revision did not correctly increase the revision number")
    86  
    87  	// errReviseBadUnlockConditions is returned when a file contract revision
    88  	// has unlock conditions that do not match the file contract being revised.
    89  	errReviseBadUnlockConditions = errors.New("propsed file contract revision appears to have the wrong unlock conditions")
    90  
    91  	// errRevisionBadUnlockHash is returned if a proposed file contract
    92  	// revision does not have an unlock hash which matches the unlock hash of
    93  	// the previous file contract revision.
    94  	errReviseBadUnlockHash = errors.New("proposed file contract revision has a bad new unlock hash")
    95  
    96  	// errReviseBadVoidOutput is returned if a proposed file contract revision
    97  	// does not correct add value to the void output to compensate for revenue
    98  	// from the renter.
    99  	errReviseBadVoidOutput = errors.New("proposed file contract revision does not correctly add to the host's void outputs")
   100  
   101  	// errUnknownModification is returned if the host receives a modification
   102  	// action from the renter that it does not understand.
   103  	errUnknownModification = errors.New("renter is attempting an action that the host is not aware of")
   104  )
   105  
   106  // managedRevisionIteration handles one iteration of the revision loop. As a
   107  // performance optimization, multiple iterations of revisions are allowed to be
   108  // made over the same connection.
   109  func (h *Host) managedRevisionIteration(conn net.Conn, so *storageObligation, finalIter bool) error {
   110  	// Send the settings to the renter. The host will keep going even if it is
   111  	// not accepting contracts, because in this case the contract already
   112  	// exists.
   113  	err := h.managedRPCSettings(conn)
   114  	if err != nil {
   115  		return err
   116  	}
   117  
   118  	// Set the negotiation deadline.
   119  	conn.SetDeadline(time.Now().Add(modules.NegotiateFileContractRevisionTime))
   120  
   121  	// The renter will either accept or reject the settings + revision
   122  	// transaction. It may also return a stop response to indicate that it
   123  	// wishes to terminate the revision loop.
   124  	err = modules.ReadNegotiationAcceptance(conn)
   125  	if err != nil {
   126  		return err
   127  	}
   128  
   129  	// Read some variables from the host for use later in the function.
   130  	h.mu.RLock()
   131  	settings := h.settings
   132  	secretKey := h.secretKey
   133  	blockHeight := h.blockHeight
   134  	h.mu.RUnlock()
   135  
   136  	// The renter is going to send its intended modifications, followed by the
   137  	// file contract revision that pays for them.
   138  	var modifications []modules.RevisionAction
   139  	var revision types.FileContractRevision
   140  	err = encoding.ReadObject(conn, &modifications, settings.MaxReviseBatchSize)
   141  	if err != nil {
   142  		return err
   143  	}
   144  	err = encoding.ReadObject(conn, &revision, modules.NegotiateMaxFileContractRevisionSize)
   145  	if err != nil {
   146  		return err
   147  	}
   148  
   149  	// First read all of the modifications. Then make the modifications, but
   150  	// with the ability to reverse them. Then verify the file contract revision
   151  	// correctly accounts for the changes.
   152  	var bandwidthRevenue types.Currency // Upload bandwidth.
   153  	var storageRevenue types.Currency
   154  	var newCollateral types.Currency
   155  	var sectorsRemoved []crypto.Hash
   156  	var sectorsGained []crypto.Hash
   157  	var gainedSectorData [][]byte
   158  	err = func() error {
   159  		for _, modification := range modifications {
   160  			// Check that the index points to an existing sector root. If the type
   161  			// is ActionInsert, we permit inserting at the end.
   162  			if modification.Type == modules.ActionInsert {
   163  				if modification.SectorIndex > uint64(len(so.SectorRoots)) {
   164  					return errBadModificationIndex
   165  				}
   166  			} else if modification.SectorIndex >= uint64(len(so.SectorRoots)) {
   167  				return errBadModificationIndex
   168  			}
   169  			// Check that the data sent for the sector is not too large.
   170  			if uint64(len(modification.Data)) > modules.SectorSize {
   171  				return errLargeSector
   172  			}
   173  
   174  			switch modification.Type {
   175  			case modules.ActionDelete:
   176  				// There is no financial information to change, it is enough to
   177  				// remove the sector.
   178  				sectorsRemoved = append(sectorsRemoved, so.SectorRoots[modification.SectorIndex])
   179  				so.SectorRoots = append(so.SectorRoots[0:modification.SectorIndex], so.SectorRoots[modification.SectorIndex+1:]...)
   180  			case modules.ActionInsert:
   181  				// Check that the sector size is correct.
   182  				if uint64(len(modification.Data)) != modules.SectorSize {
   183  					return errBadSectorSize
   184  				}
   185  
   186  				// Update finances.
   187  				blocksRemaining := so.proofDeadline() - blockHeight
   188  				blockBytesCurrency := types.NewCurrency64(uint64(blocksRemaining)).Mul64(modules.SectorSize)
   189  				bandwidthRevenue = bandwidthRevenue.Add(settings.MinimumUploadBandwidthPrice.Mul64(modules.SectorSize))
   190  				storageRevenue = storageRevenue.Add(settings.MinimumStoragePrice.Mul(blockBytesCurrency))
   191  				newCollateral = newCollateral.Add(settings.Collateral.Mul(blockBytesCurrency))
   192  
   193  				// Insert the sector into the root list.
   194  				newRoot := crypto.MerkleRoot(modification.Data)
   195  				sectorsGained = append(sectorsGained, newRoot)
   196  				gainedSectorData = append(gainedSectorData, modification.Data)
   197  				so.SectorRoots = append(so.SectorRoots[:modification.SectorIndex], append([]crypto.Hash{newRoot}, so.SectorRoots[modification.SectorIndex:]...)...)
   198  			case modules.ActionModify:
   199  				// Check that the offset and length are okay. Length is already
   200  				// known to be appropriately small, but the offset needs to be
   201  				// checked for being appropriately small as well otherwise there is
   202  				// a risk of overflow.
   203  				if modification.Offset > modules.SectorSize || modification.Offset+uint64(len(modification.Data)) > modules.SectorSize {
   204  					return errIllegalOffsetAndLength
   205  				}
   206  
   207  				// Get the data for the new sector.
   208  				sector, err := h.ReadSector(so.SectorRoots[modification.SectorIndex])
   209  				if err != nil {
   210  					return err
   211  				}
   212  				copy(sector[modification.Offset:], modification.Data)
   213  
   214  				// Update finances.
   215  				bandwidthRevenue = bandwidthRevenue.Add(settings.MinimumUploadBandwidthPrice.Mul64(uint64(len(modification.Data))))
   216  
   217  				// Update the sectors removed and gained to indicate that the old
   218  				// sector has been replaced with a new sector.
   219  				newRoot := crypto.MerkleRoot(sector)
   220  				sectorsRemoved = append(sectorsRemoved, so.SectorRoots[modification.SectorIndex])
   221  				sectorsGained = append(sectorsGained, newRoot)
   222  				gainedSectorData = append(gainedSectorData, sector)
   223  				so.SectorRoots[modification.SectorIndex] = newRoot
   224  			default:
   225  				return errUnknownModification
   226  			}
   227  		}
   228  		newRevenue := storageRevenue.Add(bandwidthRevenue)
   229  		return verifyRevision(so, revision, blockHeight, newRevenue, newCollateral)
   230  	}()
   231  	if err != nil {
   232  		return modules.WriteNegotiationRejection(conn, err)
   233  	}
   234  	// Revision is acceptable, write an acceptance string.
   235  	err = modules.WriteNegotiationAcceptance(conn)
   236  	if err != nil {
   237  		return err
   238  	}
   239  
   240  	// Renter will send a transaction signature for the file contract revision.
   241  	var renterSig types.TransactionSignature
   242  	err = encoding.ReadObject(conn, &renterSig, modules.NegotiateMaxTransactionSignatureSize)
   243  	if err != nil {
   244  		return err
   245  	}
   246  	// Verify that the signature is valid and get the host's signature.
   247  	txn, err := createRevisionSignature(revision, renterSig, secretKey, blockHeight)
   248  	if err != nil {
   249  		return modules.WriteNegotiationRejection(conn, err)
   250  	}
   251  
   252  	so.PotentialStorageRevenue = so.PotentialStorageRevenue.Add(storageRevenue)
   253  	so.RiskedCollateral = so.RiskedCollateral.Add(newCollateral)
   254  	so.PotentialUploadRevenue = so.PotentialUploadRevenue.Add(bandwidthRevenue)
   255  	so.RevisionTransactionSet = []types.Transaction{txn}
   256  	err = h.modifyStorageObligation(so, sectorsRemoved, sectorsGained, gainedSectorData)
   257  	if err != nil {
   258  		return modules.WriteNegotiationRejection(conn, err)
   259  	}
   260  
   261  	// Host will now send acceptance and its signature to the renter. This
   262  	// iteration is complete. If the finalIter flag is set, StopResponse will
   263  	// be sent instead. This indicates to the renter that the host wishes to
   264  	// terminate the revision loop.
   265  	if finalIter {
   266  		err = modules.WriteNegotiationStop(conn)
   267  	} else {
   268  		err = modules.WriteNegotiationAcceptance(conn)
   269  	}
   270  	if err != nil {
   271  		return err
   272  	}
   273  	return encoding.WriteObject(conn, txn.TransactionSignatures[1])
   274  }
   275  
   276  // managedRPCReviseContract accepts a request to revise an existing contract.
   277  // Revisions can add sectors, delete sectors, and modify existing sectors.
   278  func (h *Host) managedRPCReviseContract(conn net.Conn) error {
   279  	// Set a preliminary deadline for receiving the storage obligation.
   280  	startTime := time.Now()
   281  	// Perform the file contract revision exchange, giving the renter the most
   282  	// recent file contract revision and getting the storage obligation that
   283  	// will be used to pay for the data.
   284  	_, so, err := h.managedRPCRecentRevision(conn)
   285  	if err != nil {
   286  		return err
   287  	}
   288  
   289  	// Lock the storage obligation during the revision.
   290  	h.mu.Lock()
   291  	err = h.lockStorageObligation(so)
   292  	h.mu.Unlock()
   293  	if err != nil {
   294  		return err
   295  	}
   296  	defer func() {
   297  		h.mu.Lock()
   298  		err = h.unlockStorageObligation(so)
   299  		h.mu.Unlock()
   300  		if err != nil {
   301  			h.log.Critical(err)
   302  		}
   303  	}()
   304  
   305  	// Begin the revision loop. The host will process revisions until a
   306  	// timeout is reached, or until the renter sends a StopResponse.
   307  	for timeoutReached := false; !timeoutReached; {
   308  		timeoutReached = time.Since(startTime) > iteratedConnectionTime
   309  		err := h.managedRevisionIteration(conn, so, timeoutReached)
   310  		if err == modules.ErrStopResponse {
   311  			return nil
   312  		} else if err != nil {
   313  			return err
   314  		}
   315  	}
   316  	return nil
   317  }
   318  
   319  // verifyRevision checks that the revision pays the host correctly, and that
   320  // the revision does not attempt any malicious or unexpected changes.
   321  func verifyRevision(so *storageObligation, revision types.FileContractRevision, blockHeight types.BlockHeight, newRevenue, newCollateral types.Currency) error {
   322  	// Check that the revision is well-formed.
   323  	if len(revision.NewValidProofOutputs) != 2 || len(revision.NewMissedProofOutputs) != 3 {
   324  		return errInsaneFileContractRevisionOutputCounts
   325  	}
   326  
   327  	// Check that the time to finalize and submit the file contract revision
   328  	// has not already passed.
   329  	if so.expiration()-revisionSubmissionBuffer <= blockHeight {
   330  		return errLateRevision
   331  	}
   332  
   333  	oldFCR := so.RevisionTransactionSet[len(so.RevisionTransactionSet)-1].FileContractRevisions[0]
   334  
   335  	// Check that all non-volatile fields are the same.
   336  	if oldFCR.ParentID != revision.ParentID {
   337  		return errReviseBadParent
   338  	}
   339  	if oldFCR.UnlockConditions.UnlockHash() != revision.UnlockConditions.UnlockHash() {
   340  		return errReviseBadUnlockConditions
   341  	}
   342  	if oldFCR.NewRevisionNumber >= revision.NewRevisionNumber {
   343  		return errReviseBadRevisionNumber
   344  	}
   345  	if revision.NewFileSize != uint64(len(so.SectorRoots))*modules.SectorSize {
   346  		return errReviseBadNewFileSize
   347  	}
   348  	if oldFCR.NewWindowStart != revision.NewWindowStart {
   349  		return errReviseBadNewWindowStart
   350  	}
   351  	if oldFCR.NewWindowEnd != revision.NewWindowEnd {
   352  		return errReviseBadNewWindowEnd
   353  	}
   354  	if oldFCR.NewUnlockHash != revision.NewUnlockHash {
   355  		return errReviseBadUnlockHash
   356  	}
   357  
   358  	// The new revenue comes out of the renter's valid outputs.
   359  	if revision.NewValidProofOutputs[0].Value.Add(newRevenue).Cmp(oldFCR.NewValidProofOutputs[0].Value) > 0 {
   360  		return errReviseBadRenterValidOutput
   361  	}
   362  	// The new revenue goes into the host's valid outputs.
   363  	if oldFCR.NewValidProofOutputs[1].Value.Add(newRevenue).Cmp(revision.NewValidProofOutputs[1].Value) < 0 {
   364  		return errReviseBadHostValidOutput
   365  	}
   366  	// The new revenue comes out of the renter's missed outputs.
   367  	if revision.NewMissedProofOutputs[0].Value.Add(newRevenue).Cmp(oldFCR.NewMissedProofOutputs[0].Value) > 0 {
   368  		return errReviseBadRenterMissedOutput
   369  	}
   370  	// The new collateral comes out of the host's missed outputs.
   371  	if revision.NewMissedProofOutputs[1].Value.Add(newCollateral).Cmp(oldFCR.NewMissedProofOutputs[1].Value) > 0 {
   372  		return errReviseBadCollateralDeduction
   373  	}
   374  	// The new collateral and new revenue goes into the host's void outputs.
   375  	if oldFCR.NewMissedProofOutputs[2].Value.Add(newRevenue).Add(newCollateral).Cmp(revision.NewMissedProofOutputs[2].Value) < 0 {
   376  		return errReviseBadVoidOutput
   377  	}
   378  
   379  	// The Merkle root is checked last because it is the most expensive check.
   380  	log2SectorSize := uint64(0)
   381  	for 1<<log2SectorSize < (modules.SectorSize / crypto.SegmentSize) {
   382  		log2SectorSize++
   383  	}
   384  	ct := crypto.NewCachedTree(log2SectorSize)
   385  	for _, root := range so.SectorRoots {
   386  		ct.Push(root)
   387  	}
   388  	expectedMerkleRoot := ct.Root()
   389  	if revision.NewFileMerkleRoot != expectedMerkleRoot {
   390  		return errReviseBadFileMerkleRoot
   391  	}
   392  
   393  	return nil
   394  }