github.com/avahowell/sia@v0.5.1-beta.0.20160524050156-83dcc3d37c94/modules/host/negotiaterenewcontract.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  	// errRenewDoesNotExtend is returned if a file contract renewal is
    16  	// presented which does not extend the existing file contract.
    17  	errRenewDoesNotExtend = errors.New("file contract renewal does not extend the existing file contract")
    18  )
    19  
    20  // renewBaseCollateral returns the base collateral on the storage in the file
    21  // contract, using the host's external settings and the starting file contract.
    22  func renewBaseCollateral(so *storageObligation, settings modules.HostExternalSettings, fc types.FileContract) types.Currency {
    23  	timeExtension := fc.WindowEnd - so.proofDeadline()
    24  	return settings.Collateral.Mul64(fc.FileSize).Mul64(uint64(timeExtension))
    25  }
    26  
    27  // renewBasePrice returns the base cost of the storage in the file contract,
    28  // using the host external settings and the starting file contract.
    29  func renewBasePrice(so *storageObligation, settings modules.HostExternalSettings, fc types.FileContract) types.Currency {
    30  	timeExtension := fc.WindowEnd - so.proofDeadline()
    31  	return settings.StoragePrice.Mul64(fc.FileSize).Mul64(uint64(timeExtension))
    32  }
    33  
    34  // renewContractCollateral returns the amount of collateral that the host is
    35  // expected to add to the file contract based on the file contract and host
    36  // settings.
    37  func renewContractCollateral(so *storageObligation, settings modules.HostExternalSettings, fc types.FileContract) types.Currency {
    38  	return fc.ValidProofOutputs[1].Value.Sub(settings.ContractPrice).Sub(renewBasePrice(so, settings, fc))
    39  }
    40  
    41  // managedAddRenewCollateral adds the host's collateral to the renewed file
    42  // contract.
    43  func (h *Host) managedAddRenewCollateral(so *storageObligation, settings modules.HostExternalSettings, txnSet []types.Transaction) (builder modules.TransactionBuilder, newParents []types.Transaction, newInputs []types.SiacoinInput, newOutputs []types.SiacoinOutput, err error) {
    44  	txn := txnSet[len(txnSet)-1]
    45  	parents := txnSet[:len(txnSet)-1]
    46  	fc := txn.FileContracts[0]
    47  	hostPortion := renewContractCollateral(so, settings, fc)
    48  	builder = h.wallet.RegisterTransaction(txn, parents)
    49  	err = builder.FundSiacoins(hostPortion)
    50  	if err != nil {
    51  		builder.Drop()
    52  		return nil, nil, nil, nil, err
    53  	}
    54  
    55  	// Return which inputs and outputs have been added by the collateral call.
    56  	newParentIndices, newInputIndices, newOutputIndices, _ := builder.ViewAdded()
    57  	updatedTxn, updatedParents := builder.View()
    58  	for _, parentIndex := range newParentIndices {
    59  		newParents = append(newParents, updatedParents[parentIndex])
    60  	}
    61  	for _, inputIndex := range newInputIndices {
    62  		newInputs = append(newInputs, updatedTxn.SiacoinInputs[inputIndex])
    63  	}
    64  	for _, outputIndex := range newOutputIndices {
    65  		newOutputs = append(newOutputs, updatedTxn.SiacoinOutputs[outputIndex])
    66  	}
    67  	return builder, newParents, newInputs, newOutputs, nil
    68  }
    69  
    70  // managedRenewContract accepts a request to renew a file contract.
    71  func (h *Host) managedRPCRenewContract(conn net.Conn) error {
    72  	// Perform the recent revision protocol to get the file contract being
    73  	// revised.
    74  	_, so, err := h.managedRPCRecentRevision(conn)
    75  	if err != nil {
    76  		return err
    77  	}
    78  
    79  	// Lock the storage obligation for the remainder of the connection.
    80  	h.mu.Lock()
    81  	err = h.lockStorageObligation(so)
    82  	h.mu.Unlock()
    83  	if err != nil {
    84  		return err
    85  	}
    86  	defer func() {
    87  		h.mu.Lock()
    88  		err = h.unlockStorageObligation(so)
    89  		h.mu.Unlock()
    90  		if err != nil {
    91  			h.log.Critical(err)
    92  		}
    93  	}()
    94  
    95  	// Perform the host settings exchange with the renter.
    96  	err = h.managedRPCSettings(conn)
    97  	if err != nil {
    98  		return err
    99  	}
   100  
   101  	// Set the renewal deadline.
   102  	conn.SetDeadline(time.Now().Add(modules.NegotiateRenewContractTime))
   103  
   104  	// The renter will either accept or reject the host's settings.
   105  	err = modules.ReadNegotiationAcceptance(conn)
   106  	if err != nil {
   107  		return err
   108  	}
   109  	// If the renter sends an acceptance of the settings, it will be followed
   110  	// by an unsigned transaction containing funding from the renter and a file
   111  	// contract which matches what the final file contract should look like.
   112  	// After the file contract, the renter will send a public key which is the
   113  	// renter's public key in the unlock conditions that protect the file
   114  	// contract from revision.
   115  	var txnSet []types.Transaction
   116  	var renterPK crypto.PublicKey
   117  	err = encoding.ReadObject(conn, &txnSet, modules.NegotiateMaxFileContractSetLen)
   118  	if err != nil {
   119  		return err
   120  	}
   121  	err = encoding.ReadObject(conn, &renterPK, modules.NegotiateMaxSiaPubkeySize)
   122  	if err != nil {
   123  		return err
   124  	}
   125  
   126  	h.mu.RLock()
   127  	settings := h.externalSettings()
   128  	h.mu.RUnlock()
   129  
   130  	// Verify that the transaction coming over the wire is a proper renewal.
   131  	err = h.managedVerifyRenewedContract(so, txnSet, renterPK)
   132  	if err != nil {
   133  		return modules.WriteNegotiationRejection(conn, err)
   134  	}
   135  	txnBuilder, newParents, newInputs, newOutputs, err := h.managedAddRenewCollateral(so, settings, txnSet)
   136  	if err != nil {
   137  		return modules.WriteNegotiationRejection(conn, err)
   138  	}
   139  	// The host indicates acceptance, then sends the new parents, inputs, and
   140  	// outputs to the transaction.
   141  	err = modules.WriteNegotiationAcceptance(conn)
   142  	if err != nil {
   143  		return err
   144  	}
   145  	err = encoding.WriteObject(conn, newParents)
   146  	if err != nil {
   147  		return err
   148  	}
   149  	err = encoding.WriteObject(conn, newInputs)
   150  	if err != nil {
   151  		return err
   152  	}
   153  	err = encoding.WriteObject(conn, newOutputs)
   154  	if err != nil {
   155  		return err
   156  	}
   157  
   158  	// The renter will send a negotiation response, followed by transaction
   159  	// signatures for the file contract transaction in the case of acceptance.
   160  	// The transaction signatures will be followed by another transaction
   161  	// signature to sign the no-op file contract revision associated with the
   162  	// new file contract.
   163  	err = modules.ReadNegotiationAcceptance(conn)
   164  	if err != nil {
   165  		return err
   166  	}
   167  	var renterTxnSignatures []types.TransactionSignature
   168  	var renterRevisionSignature types.TransactionSignature
   169  	err = encoding.ReadObject(conn, &renterTxnSignatures, modules.NegotiateMaxTransactionSignatureSize)
   170  	if err != nil {
   171  		return err
   172  	}
   173  	err = encoding.ReadObject(conn, &renterRevisionSignature, modules.NegotiateMaxTransactionSignatureSize)
   174  	if err != nil {
   175  		return err
   176  	}
   177  
   178  	// The host adds the renter transaction signatures, then signs the
   179  	// transaction and submits it to the blockchain, creating a storage
   180  	// obligation in the process. The host's part is now complete and the
   181  	// contract is finalized, but to give confidence to the renter the host
   182  	// will send the sigantures so that the renter can immediately have the
   183  	// completed file contract.
   184  	//
   185  	// During finalization the signatures sent by the renter are all checked.
   186  	hostTxnSignatures, hostRevisionSignature, err := h.managedFinalizeContract(txnBuilder, renterPK, renterTxnSignatures, renterRevisionSignature)
   187  	if err != nil {
   188  		return modules.WriteNegotiationRejection(conn, err)
   189  	}
   190  	err = modules.WriteNegotiationAcceptance(conn)
   191  	if err != nil {
   192  		return err
   193  	}
   194  	// The host sends the transaction signatures to the renter, followed by the
   195  	// revision signature. Negotiation is complete.
   196  	err = encoding.WriteObject(conn, hostTxnSignatures)
   197  	if err != nil {
   198  		return err
   199  	}
   200  	return encoding.WriteObject(conn, hostRevisionSignature)
   201  }
   202  
   203  // managedVerifyRenewedContract checks that the contract renewal matches the
   204  // previous contract and makes all of the appropriate payments.
   205  func (h *Host) managedVerifyRenewedContract(so *storageObligation, txnSet []types.Transaction, renterPK crypto.PublicKey) error {
   206  	// Check that the transaction set is not empty.
   207  	if len(txnSet) < 1 {
   208  		return errEmptyFileContractTransactionSet
   209  	}
   210  	// Check that the transaction set has a file contract.
   211  	if len(txnSet[len(txnSet)-1].FileContracts) < 1 {
   212  		return errNoFileContract
   213  	}
   214  
   215  	h.mu.RLock()
   216  	blockHeight := h.blockHeight
   217  	externalSettings := h.externalSettings()
   218  	internalSettings := h.settings
   219  	lockedStorageCollateral := h.financialMetrics.LockedStorageCollateral
   220  	publicKey := h.publicKey
   221  	unlockHash := h.unlockHash
   222  	h.mu.RUnlock()
   223  	fc := txnSet[len(txnSet)-1].FileContracts[0]
   224  
   225  	// The file size and merkle root must match the file size and merkle root
   226  	// from the previous file contract.
   227  	if fc.FileSize != so.fileSize() {
   228  		return errBadFileSize
   229  	}
   230  	if fc.FileMerkleRoot != so.merkleRoot() {
   231  		return errBadFileMerkleRoot
   232  	}
   233  	// The WindowStart must be at least revisionSubmissionBuffer blocks into
   234  	// the future.
   235  	if fc.WindowStart <= blockHeight+revisionSubmissionBuffer {
   236  		return errWindowStartTooSoon
   237  	}
   238  	// WindowEnd must be at least settings.WindowSize blocks after WindowStart.
   239  	if fc.WindowEnd < fc.WindowStart+externalSettings.WindowSize {
   240  		return errWindowSizeTooSmall
   241  	}
   242  	// The WindowEnd for the new file contract must be further in the future
   243  	// than the WindowEnd for the existing file contract.
   244  	if fc.WindowEnd <= so.proofDeadline() {
   245  		return errRenewDoesNotExtend
   246  	}
   247  
   248  	// ValidProofOutputs shoud have 2 outputs (renter + host) and missed
   249  	// outputs should have 3 (renter + host + void)
   250  	if len(fc.ValidProofOutputs) != 2 || len(fc.MissedProofOutputs) != 3 {
   251  		return errBadPayoutsLen
   252  	}
   253  	// The unlock hashes of the valid and missed proof outputs for the host
   254  	// must match the host's unlock hash. The third missed output should point
   255  	// to the void.
   256  	if fc.ValidProofOutputs[1].UnlockHash != unlockHash || fc.MissedProofOutputs[1].UnlockHash != unlockHash || fc.MissedProofOutputs[2].UnlockHash != (types.UnlockHash{}) {
   257  		return errBadPayoutsUnlockHashes
   258  	}
   259  
   260  	// Check that the collateral does not exceed the maximum amount of
   261  	// collateral allowed.
   262  	expectedCollateral := renewContractCollateral(so, externalSettings, fc)
   263  	if expectedCollateral.Cmp(externalSettings.MaxCollateral) > 0 {
   264  		return errMaxCollateralReached
   265  	}
   266  	// Check that the host has enough room in the collateral budget to add this
   267  	// collateral.
   268  	if lockedStorageCollateral.Add(expectedCollateral).Cmp(internalSettings.CollateralBudget) > 0 {
   269  		return errCollateralBudgetExceeded
   270  	}
   271  	// Check that the missed proof outputs contains enough money, and that the
   272  	// void contains enough money. Before calculating the expected value, check
   273  	// that the subtraction won't cause a negative currency.
   274  	basePrice := renewBasePrice(so, externalSettings, fc)
   275  	baseCollateral := renewBaseCollateral(so, externalSettings, fc)
   276  	if fc.ValidProofOutputs[1].Value.Cmp(basePrice.Add(baseCollateral)) < 0 {
   277  		return errBadPayoutsAmounts
   278  	}
   279  	expectedHostMissedOutput := fc.ValidProofOutputs[1].Value.Sub(basePrice).Sub(baseCollateral)
   280  	if fc.MissedProofOutputs[1].Value.Cmp(expectedHostMissedOutput) != 0 {
   281  		return errBadPayoutsAmounts
   282  	}
   283  	// Check that the void output has the correct value.
   284  	expectedVoidOutput := basePrice.Add(baseCollateral)
   285  	if fc.MissedProofOutputs[2].Value.Cmp(expectedVoidOutput) != 0 {
   286  		return errBadPayoutsAmounts
   287  	}
   288  
   289  	// The unlock hash for the file contract must match the unlock hash that
   290  	// the host knows how to spend.
   291  	expectedUH := types.UnlockConditions{
   292  		PublicKeys: []types.SiaPublicKey{
   293  			{
   294  				Algorithm: types.SignatureEd25519,
   295  				Key:       renterPK[:],
   296  			},
   297  			publicKey,
   298  		},
   299  		SignaturesRequired: 2,
   300  	}.UnlockHash()
   301  	if fc.UnlockHash != expectedUH {
   302  		return errBadContractUnlockHash
   303  	}
   304  
   305  	// Check that the transaction set has enough fees on it to get into the
   306  	// blockchain.
   307  	setFee := modules.CalculateFee(txnSet)
   308  	minFee, _ := h.tpool.FeeEstimation()
   309  	if setFee.Cmp(minFee) < 0 {
   310  		return errLowFees
   311  	}
   312  	return nil
   313  }