gitlab.com/jokerrs1/Sia@v1.3.2/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  	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 = h.wallet.RegisterTransaction(txn, parents)
    55  	err = builder.FundSiacoins(hostPortion)
    56  	if err != nil {
    57  		builder.Drop()
    58  		return nil, nil, nil, nil, extendErr("could not add collateral: ", ErrorInternal(err.Error()))
    59  	}
    60  
    61  	// Return which inputs and outputs have been added by the collateral call.
    62  	newParentIndices, newInputIndices, newOutputIndices, _ := builder.ViewAdded()
    63  	updatedTxn, updatedParents := builder.View()
    64  	for _, parentIndex := range newParentIndices {
    65  		newParents = append(newParents, updatedParents[parentIndex])
    66  	}
    67  	for _, inputIndex := range newInputIndices {
    68  		newInputs = append(newInputs, updatedTxn.SiacoinInputs[inputIndex])
    69  	}
    70  	for _, outputIndex := range newOutputIndices {
    71  		newOutputs = append(newOutputs, updatedTxn.SiacoinOutputs[outputIndex])
    72  	}
    73  	return builder, newParents, newInputs, newOutputs, nil
    74  }
    75  
    76  // managedRenewContract accepts a request to renew a file contract.
    77  func (h *Host) managedRPCRenewContract(conn net.Conn) error {
    78  	// Perform the recent revision protocol to get the file contract being
    79  	// revised.
    80  	_, so, err := h.managedRPCRecentRevision(conn)
    81  	if err != nil {
    82  		return extendErr("failed RPCRecentRevision during RPCRenewContract: ", err)
    83  	}
    84  	// The storage obligation is received with a lock. Defer a call to unlock
    85  	// the storage obligation.
    86  	defer func() {
    87  		h.managedUnlockStorageObligation(so.id())
    88  	}()
    89  
    90  	// Perform the host settings exchange with the renter.
    91  	err = h.managedRPCSettings(conn)
    92  	if err != nil {
    93  		return extendErr("RPCSettings failed: ", err)
    94  	}
    95  
    96  	// Set the renewal deadline.
    97  	conn.SetDeadline(time.Now().Add(modules.NegotiateRenewContractTime))
    98  
    99  	// The renter will either accept or reject the host's settings.
   100  	err = modules.ReadNegotiationAcceptance(conn)
   101  	if err != nil {
   102  		return extendErr("renter rejected the host settings: ", ErrorCommunication(err.Error()))
   103  	}
   104  	// If the renter sends an acceptance of the settings, it will be followed
   105  	// by an unsigned transaction containing funding from the renter and a file
   106  	// contract which matches what the final file contract should look like.
   107  	// After the file contract, the renter will send a public key which is the
   108  	// renter's public key in the unlock conditions that protect the file
   109  	// contract from revision.
   110  	var txnSet []types.Transaction
   111  	var renterPK crypto.PublicKey
   112  	err = encoding.ReadObject(conn, &txnSet, modules.NegotiateMaxFileContractSetLen)
   113  	if err != nil {
   114  		return extendErr("unable to read transaction set: ", ErrorConnection(err.Error()))
   115  	}
   116  	err = encoding.ReadObject(conn, &renterPK, modules.NegotiateMaxSiaPubkeySize)
   117  	if err != nil {
   118  		return extendErr("unable to read renter public key: ", ErrorConnection(err.Error()))
   119  	}
   120  
   121  	h.mu.Lock()
   122  	settings := h.externalSettings()
   123  	h.mu.Unlock()
   124  
   125  	// Verify that the transaction coming over the wire is a proper renewal.
   126  	err = h.managedVerifyRenewedContract(so, txnSet, renterPK)
   127  	if err != nil {
   128  		modules.WriteNegotiationRejection(conn, err) // Error is ignored to preserve type for extendErr
   129  		return extendErr("verification of renewal failed: ", err)
   130  	}
   131  	txnBuilder, newParents, newInputs, newOutputs, err := h.managedAddRenewCollateral(so, settings, txnSet)
   132  	if err != nil {
   133  		modules.WriteNegotiationRejection(conn, err) // Error is ignored to preserve type for extendErr
   134  		return extendErr("failed to add collateral: ", err)
   135  	}
   136  	// The host indicates acceptance, then sends the new parents, inputs, and
   137  	// outputs to the transaction.
   138  	err = modules.WriteNegotiationAcceptance(conn)
   139  	if err != nil {
   140  		return extendErr("failed to write acceptance: ", ErrorConnection(err.Error()))
   141  	}
   142  	err = encoding.WriteObject(conn, newParents)
   143  	if err != nil {
   144  		return extendErr("failed to write new parents: ", ErrorConnection(err.Error()))
   145  	}
   146  	err = encoding.WriteObject(conn, newInputs)
   147  	if err != nil {
   148  		return extendErr("failed to write new inputs: ", ErrorConnection(err.Error()))
   149  	}
   150  	err = encoding.WriteObject(conn, newOutputs)
   151  	if err != nil {
   152  		return extendErr("failed to write new outputs: ", ErrorConnection(err.Error()))
   153  	}
   154  
   155  	// The renter will send a negotiation response, followed by transaction
   156  	// signatures for the file contract transaction in the case of acceptance.
   157  	// The transaction signatures will be followed by another transaction
   158  	// signature to sign the no-op file contract revision associated with the
   159  	// new file contract.
   160  	err = modules.ReadNegotiationAcceptance(conn)
   161  	if err != nil {
   162  		return extendErr("renter rejected collateral extension: ", ErrorCommunication(err.Error()))
   163  	}
   164  	var renterTxnSignatures []types.TransactionSignature
   165  	var renterRevisionSignature types.TransactionSignature
   166  	err = encoding.ReadObject(conn, &renterTxnSignatures, modules.NegotiateMaxTransactionSignatureSize)
   167  	if err != nil {
   168  		return extendErr("failed to read renter transaction signatures: ", ErrorConnection(err.Error()))
   169  	}
   170  	err = encoding.ReadObject(conn, &renterRevisionSignature, modules.NegotiateMaxTransactionSignatureSize)
   171  	if err != nil {
   172  		return extendErr("failed to read renter revision signatures: ", ErrorConnection(err.Error()))
   173  	}
   174  
   175  	// The host adds the renter transaction signatures, then signs the
   176  	// transaction and submits it to the blockchain, creating a storage
   177  	// obligation in the process. The host's part is now complete and the
   178  	// contract is finalized, but to give confidence to the renter the host
   179  	// will send the signatures so that the renter can immediately have the
   180  	// completed file contract.
   181  	//
   182  	// During finalization the signatures sent by the renter are all checked.
   183  	h.mu.RLock()
   184  	fc := txnSet[len(txnSet)-1].FileContracts[0]
   185  	renewCollateral := renewContractCollateral(so, settings, fc)
   186  	renewRevenue := renewBasePrice(so, settings, fc)
   187  	renewRisk := renewBaseCollateral(so, settings, fc)
   188  	h.mu.RUnlock()
   189  	hostTxnSignatures, hostRevisionSignature, newSOID, err := h.managedFinalizeContract(txnBuilder, renterPK, renterTxnSignatures, renterRevisionSignature, so.SectorRoots, renewCollateral, renewRevenue, renewRisk, settings)
   190  	if err != nil {
   191  		modules.WriteNegotiationRejection(conn, err) // Error is ignored to preserve type for extendErr
   192  		return extendErr("failed to finalize contract: ", err)
   193  	}
   194  	defer h.managedUnlockStorageObligation(newSOID)
   195  	err = modules.WriteNegotiationAcceptance(conn)
   196  	if err != nil {
   197  		return extendErr("failed to write acceptance: ", ErrorConnection(err.Error()))
   198  	}
   199  	// The host sends the transaction signatures to the renter, followed by the
   200  	// revision signature. Negotiation is complete.
   201  	err = encoding.WriteObject(conn, hostTxnSignatures)
   202  	if err != nil {
   203  		return extendErr("failed to write transaction signatures: ", ErrorConnection(err.Error()))
   204  	}
   205  	err = encoding.WriteObject(conn, hostRevisionSignature)
   206  	if err != nil {
   207  		return extendErr("failed to write revision signature: ", ErrorConnection(err.Error()))
   208  	}
   209  	return nil
   210  }
   211  
   212  // managedVerifyRenewedContract checks that the contract renewal matches the
   213  // previous contract and makes all of the appropriate payments.
   214  func (h *Host) managedVerifyRenewedContract(so storageObligation, txnSet []types.Transaction, renterPK crypto.PublicKey) error {
   215  	// Check that the transaction set is not empty.
   216  	if len(txnSet) < 1 {
   217  		return extendErr("zero-length transaction set: ", errEmptyObject)
   218  	}
   219  	// Check that the transaction set has a file contract.
   220  	if len(txnSet[len(txnSet)-1].FileContracts) < 1 {
   221  		return extendErr("transaction without file contract: ", errEmptyObject)
   222  	}
   223  
   224  	h.mu.Lock()
   225  	blockHeight := h.blockHeight
   226  	externalSettings := h.externalSettings()
   227  	internalSettings := h.settings
   228  	lockedStorageCollateral := h.financialMetrics.LockedStorageCollateral
   229  	publicKey := h.publicKey
   230  	unlockHash := h.unlockHash
   231  	h.mu.Unlock()
   232  	fc := txnSet[len(txnSet)-1].FileContracts[0]
   233  
   234  	// The file size and merkle root must match the file size and merkle root
   235  	// from the previous file contract.
   236  	if fc.FileSize != so.fileSize() {
   237  		return errBadFileSize
   238  	}
   239  	if fc.FileMerkleRoot != so.merkleRoot() {
   240  		return errBadFileMerkleRoot
   241  	}
   242  	// The WindowStart must be at least revisionSubmissionBuffer blocks into
   243  	// the future.
   244  	if fc.WindowStart <= blockHeight+revisionSubmissionBuffer {
   245  		return errEarlyWindow
   246  	}
   247  	// WindowEnd must be at least settings.WindowSize blocks after WindowStart.
   248  	if fc.WindowEnd < fc.WindowStart+externalSettings.WindowSize {
   249  		return errSmallWindow
   250  	}
   251  
   252  	// ValidProofOutputs shoud have 2 outputs (renter + host) and missed
   253  	// outputs should have 3 (renter + host + void)
   254  	if len(fc.ValidProofOutputs) != 2 || len(fc.MissedProofOutputs) != 3 {
   255  		return errBadContractOutputCounts
   256  	}
   257  	// The unlock hashes of the valid and missed proof outputs for the host
   258  	// must match the host's unlock hash. The third missed output should point
   259  	// to the void.
   260  	if fc.ValidProofOutputs[1].UnlockHash != unlockHash || fc.MissedProofOutputs[1].UnlockHash != unlockHash || fc.MissedProofOutputs[2].UnlockHash != (types.UnlockHash{}) {
   261  		return errBadPayoutUnlockHashes
   262  	}
   263  
   264  	// Check that the collateral does not exceed the maximum amount of
   265  	// collateral allowed.
   266  	expectedCollateral := renewContractCollateral(so, externalSettings, fc)
   267  	if expectedCollateral.Cmp(externalSettings.MaxCollateral) > 0 {
   268  		return errMaxCollateralReached
   269  	}
   270  	// Check that the host has enough room in the collateral budget to add this
   271  	// collateral.
   272  	if lockedStorageCollateral.Add(expectedCollateral).Cmp(internalSettings.CollateralBudget) > 0 {
   273  		return errCollateralBudgetExceeded
   274  	}
   275  	// Check that the missed proof outputs contain enough money, and that the
   276  	// void output contains enough money.
   277  	basePrice := renewBasePrice(so, externalSettings, fc)
   278  	baseCollateral := renewBaseCollateral(so, externalSettings, fc)
   279  	if fc.ValidProofOutputs[1].Value.Cmp(basePrice.Add(baseCollateral)) < 0 {
   280  		return errLowHostValidOutput
   281  	}
   282  	expectedHostMissedOutput := fc.ValidProofOutputs[1].Value.Sub(basePrice).Sub(baseCollateral)
   283  	if fc.MissedProofOutputs[1].Value.Cmp(expectedHostMissedOutput) < 0 {
   284  		return errLowHostMissedOutput
   285  	}
   286  	// Check that the void output has the correct value.
   287  	expectedVoidOutput := basePrice.Add(baseCollateral)
   288  	if fc.MissedProofOutputs[2].Value.Cmp(expectedVoidOutput) > 0 {
   289  		return errLowVoidOutput
   290  	}
   291  
   292  	// The unlock hash for the file contract must match the unlock hash that
   293  	// the host knows how to spend.
   294  	expectedUH := types.UnlockConditions{
   295  		PublicKeys: []types.SiaPublicKey{
   296  			types.Ed25519PublicKey(renterPK),
   297  			publicKey,
   298  		},
   299  		SignaturesRequired: 2,
   300  	}.UnlockHash()
   301  	if fc.UnlockHash != expectedUH {
   302  		return errBadUnlockHash
   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 errLowTransactionFees
   311  	}
   312  	return nil
   313  }