github.com/fozzysec/SiaPrime@v0.0.0-20190612043147-66c8e8d11fe3/modules/host/negotiaterenewcontract.go (about)

     1  package host
     2  
     3  import (
     4  	"errors"
     5  	"net"
     6  	"time"
     7  
     8  	"SiaPrime/crypto"
     9  	"SiaPrime/encoding"
    10  	"SiaPrime/modules"
    11  	"SiaPrime/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  	if fc.WindowEnd <= so.proofDeadline() {
    24  		return types.NewCurrency64(0)
    25  	}
    26  	timeExtension := fc.WindowEnd - so.proofDeadline()
    27  	return settings.Collateral.Mul64(fc.FileSize).Mul64(uint64(timeExtension))
    28  }
    29  
    30  // renewBasePrice returns the base cost of the storage in the file contract,
    31  // using the host external settings and the starting file contract.
    32  func renewBasePrice(so storageObligation, settings modules.HostExternalSettings, fc types.FileContract) types.Currency {
    33  	if fc.WindowEnd <= so.proofDeadline() {
    34  		return types.NewCurrency64(0)
    35  	}
    36  	timeExtension := fc.WindowEnd - so.proofDeadline()
    37  	return settings.StoragePrice.Mul64(fc.FileSize).Mul64(uint64(timeExtension))
    38  }
    39  
    40  // renewContractCollateral returns the amount of collateral that the host is
    41  // expected to add to the file contract based on the file contract and host
    42  // settings.
    43  func renewContractCollateral(so storageObligation, settings modules.HostExternalSettings, fc types.FileContract) types.Currency {
    44  	return fc.ValidProofOutputs[1].Value.Sub(settings.ContractPrice).Sub(renewBasePrice(so, settings, fc))
    45  }
    46  
    47  // managedAddRenewCollateral adds the host's collateral to the renewed file
    48  // contract.
    49  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) {
    50  	txn := txnSet[len(txnSet)-1]
    51  	parents := txnSet[:len(txnSet)-1]
    52  	fc := txn.FileContracts[0]
    53  	hostPortion := renewContractCollateral(so, settings, fc)
    54  	builder, err = h.wallet.RegisterTransaction(txn, parents)
    55  	if err != nil {
    56  		return
    57  	}
    58  	err = builder.FundSiacoins(hostPortion)
    59  	if err != nil {
    60  		builder.Drop()
    61  		return nil, nil, nil, nil, extendErr("could not add collateral: ", ErrorInternal(err.Error()))
    62  	}
    63  
    64  	// Return which inputs and outputs have been added by the collateral call.
    65  	newParentIndices, newInputIndices, newOutputIndices, _ := builder.ViewAdded()
    66  	updatedTxn, updatedParents := builder.View()
    67  	for _, parentIndex := range newParentIndices {
    68  		newParents = append(newParents, updatedParents[parentIndex])
    69  	}
    70  	for _, inputIndex := range newInputIndices {
    71  		newInputs = append(newInputs, updatedTxn.SiacoinInputs[inputIndex])
    72  	}
    73  	for _, outputIndex := range newOutputIndices {
    74  		newOutputs = append(newOutputs, updatedTxn.SiacoinOutputs[outputIndex])
    75  	}
    76  	return builder, newParents, newInputs, newOutputs, nil
    77  }
    78  
    79  // managedRenewContract accepts a request to renew a file contract.
    80  func (h *Host) managedRPCRenewContract(conn net.Conn) error {
    81  	// Perform the recent revision protocol to get the file contract being
    82  	// revised.
    83  	_, so, err := h.managedRPCRecentRevision(conn)
    84  	if err != nil {
    85  		return extendErr("failed RPCRecentRevision during RPCRenewContract: ", err)
    86  	}
    87  	// The storage obligation is received with a lock. Defer a call to unlock
    88  	// the storage obligation.
    89  	defer func() {
    90  		h.managedUnlockStorageObligation(so.id())
    91  	}()
    92  
    93  	// Perform the host settings exchange with the renter.
    94  	err = h.managedRPCSettings(conn)
    95  	if err != nil {
    96  		return extendErr("RPCSettings failed: ", err)
    97  	}
    98  
    99  	// Set the renewal deadline.
   100  	conn.SetDeadline(time.Now().Add(modules.NegotiateRenewContractTime))
   101  
   102  	// The renter will either accept or reject the host's settings.
   103  	err = modules.ReadNegotiationAcceptance(conn)
   104  	if err != nil {
   105  		return extendErr("renter rejected the host settings: ", ErrorCommunication(err.Error()))
   106  	}
   107  	// If the renter sends an acceptance of the settings, it will be followed
   108  	// by an unsigned transaction containing funding from the renter and a file
   109  	// contract which matches what the final file contract should look like.
   110  	// After the file contract, the renter will send a public key which is the
   111  	// renter's public key in the unlock conditions that protect the file
   112  	// contract from revision.
   113  	var txnSet []types.Transaction
   114  	var renterPK crypto.PublicKey
   115  	err = encoding.ReadObject(conn, &txnSet, modules.NegotiateMaxFileContractSetLen)
   116  	if err != nil {
   117  		return extendErr("unable to read transaction set: ", ErrorConnection(err.Error()))
   118  	}
   119  	err = encoding.ReadObject(conn, &renterPK, modules.NegotiateMaxSiaPubkeySize)
   120  	if err != nil {
   121  		return extendErr("unable to read renter public key: ", ErrorConnection(err.Error()))
   122  	}
   123  
   124  	h.mu.Lock()
   125  	settings := h.externalSettings()
   126  	h.mu.Unlock()
   127  
   128  	// Verify that the transaction coming over the wire is a proper renewal.
   129  	err = h.managedVerifyRenewedContract(so, txnSet, renterPK)
   130  	if err != nil {
   131  		modules.WriteNegotiationRejection(conn, err) // Error is ignored to preserve type for extendErr
   132  		return extendErr("verification of renewal failed: ", err)
   133  	}
   134  	txnBuilder, newParents, newInputs, newOutputs, err := h.managedAddRenewCollateral(so, settings, txnSet)
   135  	if err != nil {
   136  		modules.WriteNegotiationRejection(conn, err) // Error is ignored to preserve type for extendErr
   137  		return extendErr("failed to add collateral: ", 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 extendErr("failed to write acceptance: ", ErrorConnection(err.Error()))
   144  	}
   145  	err = encoding.WriteObject(conn, newParents)
   146  	if err != nil {
   147  		return extendErr("failed to write new parents: ", ErrorConnection(err.Error()))
   148  	}
   149  	err = encoding.WriteObject(conn, newInputs)
   150  	if err != nil {
   151  		return extendErr("failed to write new inputs: ", ErrorConnection(err.Error()))
   152  	}
   153  	err = encoding.WriteObject(conn, newOutputs)
   154  	if err != nil {
   155  		return extendErr("failed to write new outputs: ", ErrorConnection(err.Error()))
   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 extendErr("renter rejected collateral extension: ", ErrorCommunication(err.Error()))
   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 extendErr("failed to read renter transaction signatures: ", ErrorConnection(err.Error()))
   172  	}
   173  	err = encoding.ReadObject(conn, &renterRevisionSignature, modules.NegotiateMaxTransactionSignatureSize)
   174  	if err != nil {
   175  		return extendErr("failed to read renter revision signatures: ", ErrorConnection(err.Error()))
   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 signatures 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  	h.mu.RLock()
   187  	fc := txnSet[len(txnSet)-1].FileContracts[0]
   188  	renewCollateral := renewContractCollateral(so, settings, fc)
   189  	renewRevenue := renewBasePrice(so, settings, fc)
   190  	renewRisk := renewBaseCollateral(so, settings, fc)
   191  	h.mu.RUnlock()
   192  	hostTxnSignatures, hostRevisionSignature, newSOID, err := h.managedFinalizeContract(txnBuilder, renterPK, renterTxnSignatures, renterRevisionSignature, so.SectorRoots, renewCollateral, renewRevenue, renewRisk, settings)
   193  	if err != nil {
   194  		modules.WriteNegotiationRejection(conn, err) // Error is ignored to preserve type for extendErr
   195  		return extendErr("failed to finalize contract: ", err)
   196  	}
   197  	defer h.managedUnlockStorageObligation(newSOID)
   198  	err = modules.WriteNegotiationAcceptance(conn)
   199  	if err != nil {
   200  		return extendErr("failed to write acceptance: ", ErrorConnection(err.Error()))
   201  	}
   202  	// The host sends the transaction signatures to the renter, followed by the
   203  	// revision signature. Negotiation is complete.
   204  	err = encoding.WriteObject(conn, hostTxnSignatures)
   205  	if err != nil {
   206  		return extendErr("failed to write transaction signatures: ", ErrorConnection(err.Error()))
   207  	}
   208  	err = encoding.WriteObject(conn, hostRevisionSignature)
   209  	if err != nil {
   210  		return extendErr("failed to write revision signature: ", ErrorConnection(err.Error()))
   211  	}
   212  	return nil
   213  }
   214  
   215  // managedVerifyRenewedContract checks that the contract renewal matches the
   216  // previous contract and makes all of the appropriate payments.
   217  func (h *Host) managedVerifyRenewedContract(so storageObligation, txnSet []types.Transaction, renterPK crypto.PublicKey) error {
   218  	// Check that the transaction set is not empty.
   219  	if len(txnSet) < 1 {
   220  		return extendErr("zero-length transaction set: ", errEmptyObject)
   221  	}
   222  	// Check that the transaction set has a file contract.
   223  	if len(txnSet[len(txnSet)-1].FileContracts) < 1 {
   224  		return extendErr("transaction without file contract: ", errEmptyObject)
   225  	}
   226  
   227  	h.mu.Lock()
   228  	blockHeight := h.blockHeight
   229  	externalSettings := h.externalSettings()
   230  	internalSettings := h.settings
   231  	lockedStorageCollateral := h.financialMetrics.LockedStorageCollateral
   232  	publicKey := h.publicKey
   233  	unlockHash := h.unlockHash
   234  	h.mu.Unlock()
   235  	fc := txnSet[len(txnSet)-1].FileContracts[0]
   236  
   237  	// The file size and merkle root must match the file size and merkle root
   238  	// from the previous file contract.
   239  	if fc.FileSize != so.fileSize() {
   240  		return errBadFileSize
   241  	}
   242  	if fc.FileMerkleRoot != so.merkleRoot() {
   243  		return errBadFileMerkleRoot
   244  	}
   245  	// The WindowStart must be at least revisionSubmissionBuffer blocks into
   246  	// the future.
   247  	if fc.WindowStart <= blockHeight+revisionSubmissionBuffer {
   248  		return errEarlyWindow
   249  	}
   250  	// WindowEnd must be at least settings.WindowSize blocks after WindowStart.
   251  	if fc.WindowEnd < fc.WindowStart+externalSettings.WindowSize {
   252  		return errSmallWindow
   253  	}
   254  	// WindowStart must not be more than settings.MaxDuration blocks into the
   255  	// future.
   256  	if fc.WindowStart > blockHeight+externalSettings.MaxDuration {
   257  		return errLongDuration
   258  	}
   259  
   260  	// ValidProofOutputs shoud have 2 outputs (renter + host) and missed
   261  	// outputs should have 3 (renter + host + void)
   262  	if len(fc.ValidProofOutputs) != 2 || len(fc.MissedProofOutputs) != 3 {
   263  		return errBadContractOutputCounts
   264  	}
   265  	// The unlock hashes of the valid and missed proof outputs for the host
   266  	// must match the host's unlock hash. The third missed output should point
   267  	// to the void.
   268  	if fc.ValidProofOutputs[1].UnlockHash != unlockHash || fc.MissedProofOutputs[1].UnlockHash != unlockHash || fc.MissedProofOutputs[2].UnlockHash != (types.UnlockHash{}) {
   269  		return errBadPayoutUnlockHashes
   270  	}
   271  
   272  	// Check that the collateral does not exceed the maximum amount of
   273  	// collateral allowed.
   274  	expectedCollateral := renewContractCollateral(so, externalSettings, fc)
   275  	if expectedCollateral.Cmp(externalSettings.MaxCollateral) > 0 {
   276  		return errMaxCollateralReached
   277  	}
   278  	// Check that the host has enough room in the collateral budget to add this
   279  	// collateral.
   280  	if lockedStorageCollateral.Add(expectedCollateral).Cmp(internalSettings.CollateralBudget) > 0 {
   281  		return errCollateralBudgetExceeded
   282  	}
   283  	// Check that the missed proof outputs contain enough money, and that the
   284  	// void output contains enough money.
   285  	basePrice := renewBasePrice(so, externalSettings, fc)
   286  	baseCollateral := renewBaseCollateral(so, externalSettings, fc)
   287  	if fc.ValidProofOutputs[1].Value.Cmp(basePrice.Add(baseCollateral)) < 0 {
   288  		return errLowHostValidOutput
   289  	}
   290  	expectedHostMissedOutput := fc.ValidProofOutputs[1].Value.Sub(basePrice).Sub(baseCollateral)
   291  	if fc.MissedProofOutputs[1].Value.Cmp(expectedHostMissedOutput) < 0 {
   292  		return errLowHostMissedOutput
   293  	}
   294  	// Check that the void output has the correct value.
   295  	expectedVoidOutput := basePrice.Add(baseCollateral)
   296  	if fc.MissedProofOutputs[2].Value.Cmp(expectedVoidOutput) > 0 {
   297  		return errLowVoidOutput
   298  	}
   299  
   300  	// The unlock hash for the file contract must match the unlock hash that
   301  	// the host knows how to spend.
   302  	expectedUH := types.UnlockConditions{
   303  		PublicKeys: []types.SiaPublicKey{
   304  			types.Ed25519PublicKey(renterPK),
   305  			publicKey,
   306  		},
   307  		SignaturesRequired: 2,
   308  	}.UnlockHash()
   309  	if fc.UnlockHash != expectedUH {
   310  		return errBadUnlockHash
   311  	}
   312  
   313  	// Check that the transaction set has enough fees on it to get into the
   314  	// blockchain.
   315  	setFee := modules.CalculateFee(txnSet)
   316  	minFee, _ := h.tpool.FeeEstimation()
   317  	if setFee.Cmp(minFee) < 0 {
   318  		return errLowTransactionFees
   319  	}
   320  	return nil
   321  }