gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/proto/formcontract.go (about)

     1  package proto
     2  
     3  import (
     4  	"gitlab.com/NebulousLabs/errors"
     5  
     6  	"gitlab.com/SkynetLabs/skyd/build"
     7  	"gitlab.com/SkynetLabs/skyd/skymodules"
     8  	"go.sia.tech/siad/crypto"
     9  	"go.sia.tech/siad/modules"
    10  	"go.sia.tech/siad/types"
    11  	"go.sia.tech/siad/types/typesutil"
    12  )
    13  
    14  // FormContract forms a contract with a host and submits the contract
    15  // transaction to tpool. The contract is added to the ContractSet and its
    16  // metadata is returned.
    17  func (cs *ContractSet) FormContract(params skymodules.ContractParams, txnBuilder transactionBuilder, tpool transactionPool, hdb hostDB, cancel <-chan struct{}) (rc skymodules.RenterContract, formationTxnSet []types.Transaction, sweepTxn types.Transaction, sweepParents []types.Transaction, err error) {
    18  	// Check that the host version is high enough. This should never happen
    19  	// because hosts with old versions should be filtered / blocked by the
    20  	// contractor anyway.
    21  	if build.VersionCmp(params.Host.Version, modules.MinimumSupportedRenterHostProtocolVersion) < 0 {
    22  		return skymodules.RenterContract{}, nil, types.Transaction{}, nil, ErrBadHostVersion
    23  	}
    24  
    25  	// Extract vars from params, for convenience.
    26  	allowance, host, funding, startHeight, endHeight, refundAddress := params.Allowance, params.Host, params.Funding, params.StartHeight, params.EndHeight, params.RefundAddress
    27  
    28  	// Calculate the anticipated transaction fee.
    29  	_, maxFee := tpool.FeeEstimation()
    30  	txnFee := maxFee.Mul64(skymodules.EstimatedFileContractTransactionSetSize)
    31  
    32  	// Calculate the payouts for the renter, host, and whole contract.
    33  	period := endHeight - startHeight
    34  	expectedStorage := allowance.ExpectedStorage / allowance.Hosts
    35  	renterPayout, hostPayout, _, err := skymodules.RenterPayoutsPreTax(host, funding, txnFee, types.ZeroCurrency, types.ZeroCurrency, period, expectedStorage)
    36  	if err != nil {
    37  		return skymodules.RenterContract{}, nil, types.Transaction{}, nil, err
    38  	}
    39  	totalPayout := renterPayout.Add(hostPayout)
    40  
    41  	// Check for negative currency.
    42  	if types.PostTax(startHeight, totalPayout).Cmp(hostPayout) < 0 {
    43  		return skymodules.RenterContract{}, nil, types.Transaction{}, nil, errors.New("not enough money to pay both siafund fee and also host payout")
    44  	}
    45  	// Fund the transaction.
    46  	err = txnBuilder.FundSiacoins(funding)
    47  	if err != nil {
    48  		return skymodules.RenterContract{}, nil, types.Transaction{}, nil, err
    49  	}
    50  
    51  	// Make a copy of the transaction builder so far, to be used to by the watchdog
    52  	// to double spend these inputs in case the contract never appears on chain.
    53  	sweepBuilder := txnBuilder.Copy()
    54  	// Add an output that sends all fund back to the refundAddress.
    55  	// Note that in order to send this transaction, a miner fee will have to be subtracted.
    56  	output := types.SiacoinOutput{
    57  		Value:      funding,
    58  		UnlockHash: refundAddress,
    59  	}
    60  	sweepBuilder.AddSiacoinOutput(output)
    61  	sweepTxn, sweepParents = sweepBuilder.View()
    62  
    63  	// Add FileContract identifier.
    64  	fcTxn, _ := txnBuilder.View()
    65  	si, hk := skymodules.PrefixedSignedIdentifier(params.RenterSeed, fcTxn, host.PublicKey)
    66  	_ = txnBuilder.AddArbitraryData(append(si[:], hk[:]...))
    67  	// Create our key.
    68  	ourSK, ourPK := skymodules.GenerateContractKeyPair(params.RenterSeed, fcTxn)
    69  	// Create unlock conditions.
    70  	uc := types.UnlockConditions{
    71  		PublicKeys: []types.SiaPublicKey{
    72  			types.Ed25519PublicKey(ourPK),
    73  			host.PublicKey,
    74  		},
    75  		SignaturesRequired: 2,
    76  	}
    77  
    78  	// Create file contract.
    79  	renterPostTaxPayout := types.PostTax(startHeight, totalPayout).Sub(hostPayout)
    80  	fc := types.FileContract{
    81  		FileSize:       0,
    82  		FileMerkleRoot: crypto.Hash{}, // no proof possible without data
    83  		WindowStart:    endHeight,
    84  		WindowEnd:      endHeight + host.WindowSize,
    85  		Payout:         totalPayout,
    86  		UnlockHash:     uc.UnlockHash(),
    87  		RevisionNumber: 0,
    88  		ValidProofOutputs: []types.SiacoinOutput{
    89  			// Outputs need to account for tax.
    90  			{Value: renterPostTaxPayout, UnlockHash: refundAddress}, // This is the renter payout, but with tax applied.
    91  			// Collateral is returned to host.
    92  			{Value: hostPayout, UnlockHash: host.UnlockHash},
    93  		},
    94  		MissedProofOutputs: []types.SiacoinOutput{
    95  			// Same as above.
    96  			{Value: renterPostTaxPayout, UnlockHash: refundAddress},
    97  			// Same as above.
    98  			{Value: hostPayout, UnlockHash: host.UnlockHash},
    99  			// Once we start doing revisions, we'll move some coins to the host and some to the void.
   100  			{Value: types.ZeroCurrency, UnlockHash: types.UnlockHash{}},
   101  		},
   102  	}
   103  
   104  	// Add file contract.
   105  	txnBuilder.AddFileContract(fc)
   106  	// Add miner fee.
   107  	txnBuilder.AddMinerFee(txnFee)
   108  
   109  	// Create initial transaction set.
   110  	txn, parentTxns := txnBuilder.View()
   111  	unconfirmedParents, err := txnBuilder.UnconfirmedParents()
   112  	if err != nil {
   113  		return skymodules.RenterContract{}, nil, types.Transaction{}, nil, err
   114  	}
   115  	txnSet := append(unconfirmedParents, append(parentTxns, txn)...)
   116  	txnSet = typesutil.MinimumTransactionSet([]types.Transaction{txn}, txnSet)
   117  
   118  	// Increase Successful/Failed interactions accordingly
   119  	defer func() {
   120  		if err != nil {
   121  			hdb.IncrementFailedInteractions(host.PublicKey)
   122  			err = errors.Extend(err, skymodules.ErrHostFault)
   123  		} else {
   124  			hdb.IncrementSuccessfulInteractions(host.PublicKey)
   125  		}
   126  	}()
   127  
   128  	// Initiate protocol.
   129  	s, err := cs.NewRawSession(host, startHeight, hdb, cancel)
   130  	if err != nil {
   131  		return skymodules.RenterContract{}, nil, types.Transaction{}, nil, err
   132  	}
   133  	defer func() {
   134  		err = errors.Compose(err, s.Close())
   135  	}()
   136  
   137  	// Send the FormContract request.
   138  	req := modules.LoopFormContractRequest{
   139  		Transactions: txnSet,
   140  		RenterKey:    uc.PublicKeys[0],
   141  	}
   142  	if err := s.writeRequest(modules.RPCLoopFormContract, req); err != nil {
   143  		return skymodules.RenterContract{}, nil, types.Transaction{}, nil, err
   144  	}
   145  
   146  	// Read the host's response.
   147  	var resp modules.LoopContractAdditions
   148  	if err := s.readResponse(&resp, modules.RPCMinLen); err != nil {
   149  		return skymodules.RenterContract{}, nil, types.Transaction{}, nil, err
   150  	}
   151  
   152  	// Incorporate host's modifications.
   153  	txnBuilder.AddParents(resp.Parents)
   154  	for _, input := range resp.Inputs {
   155  		txnBuilder.AddSiacoinInput(input)
   156  	}
   157  	for _, output := range resp.Outputs {
   158  		txnBuilder.AddSiacoinOutput(output)
   159  	}
   160  
   161  	// Sign the txn.
   162  	signedTxnSet, err := txnBuilder.Sign(true)
   163  	if err != nil {
   164  		err = errors.New("failed to sign transaction: " + err.Error())
   165  		modules.WriteRPCResponse(s.conn, s.aead, nil, err)
   166  		return skymodules.RenterContract{}, nil, types.Transaction{}, nil, err
   167  	}
   168  
   169  	// Calculate signatures added by the transaction builder.
   170  	var addedSignatures []types.TransactionSignature
   171  	_, _, _, addedSignatureIndices := txnBuilder.ViewAdded()
   172  	for _, i := range addedSignatureIndices {
   173  		addedSignatures = append(addedSignatures, signedTxnSet[len(signedTxnSet)-1].TransactionSignatures[i])
   174  	}
   175  
   176  	// create initial (no-op) revision, transaction, and signature
   177  	initRevision := types.FileContractRevision{
   178  		ParentID:          signedTxnSet[len(signedTxnSet)-1].FileContractID(0),
   179  		UnlockConditions:  uc,
   180  		NewRevisionNumber: 1,
   181  
   182  		NewFileSize:           fc.FileSize,
   183  		NewFileMerkleRoot:     fc.FileMerkleRoot,
   184  		NewWindowStart:        fc.WindowStart,
   185  		NewWindowEnd:          fc.WindowEnd,
   186  		NewValidProofOutputs:  fc.ValidProofOutputs,
   187  		NewMissedProofOutputs: fc.MissedProofOutputs,
   188  		NewUnlockHash:         fc.UnlockHash,
   189  	}
   190  	renterRevisionSig := types.TransactionSignature{
   191  		ParentID:       crypto.Hash(initRevision.ParentID),
   192  		PublicKeyIndex: 0,
   193  		CoveredFields: types.CoveredFields{
   194  			FileContractRevisions: []uint64{0},
   195  		},
   196  	}
   197  	revisionTxn := types.Transaction{
   198  		FileContractRevisions: []types.FileContractRevision{initRevision},
   199  		TransactionSignatures: []types.TransactionSignature{renterRevisionSig},
   200  	}
   201  	encodedSig := crypto.SignHash(revisionTxn.SigHash(0, startHeight), ourSK)
   202  	revisionTxn.TransactionSignatures[0].Signature = encodedSig[:]
   203  
   204  	// Send acceptance and signatures.
   205  	renterSigs := modules.LoopContractSignatures{
   206  		ContractSignatures: addedSignatures,
   207  		RevisionSignature:  revisionTxn.TransactionSignatures[0],
   208  	}
   209  	if err := modules.WriteRPCResponse(s.conn, s.aead, renterSigs, nil); err != nil {
   210  		return skymodules.RenterContract{}, nil, types.Transaction{}, nil, err
   211  	}
   212  
   213  	// Read the host acceptance and signatures.
   214  	var hostSigs modules.LoopContractSignatures
   215  	if err := s.readResponse(&hostSigs, modules.RPCMinLen); err != nil {
   216  		return skymodules.RenterContract{}, nil, types.Transaction{}, nil, err
   217  	}
   218  	for _, sig := range hostSigs.ContractSignatures {
   219  		txnBuilder.AddTransactionSignature(sig)
   220  	}
   221  	revisionTxn.TransactionSignatures = append(revisionTxn.TransactionSignatures, hostSigs.RevisionSignature)
   222  
   223  	// Construct the final transaction, and then grab the minimum necessary
   224  	// final set to submit to the transaction pool. Minimizing the set will
   225  	// greatly improve the chances of the transaction propagating through an
   226  	// actively attacked network.
   227  	txn, parentTxns = txnBuilder.View()
   228  	minSet := typesutil.MinimumTransactionSet([]types.Transaction{txn}, parentTxns)
   229  
   230  	// Submit to blockchain.
   231  	err = tpool.AcceptTransactionSet(minSet)
   232  	if errors.Contains(err, modules.ErrDuplicateTransactionSet) {
   233  		// As long as it made it into the transaction pool, we're good.
   234  		err = nil
   235  	}
   236  	if err != nil {
   237  		return skymodules.RenterContract{}, nil, types.Transaction{}, nil, err
   238  	}
   239  
   240  	// Construct contract header.
   241  	header := contractHeader{
   242  		Transaction: revisionTxn,
   243  		SecretKey:   ourSK,
   244  		StartHeight: startHeight,
   245  		TotalCost:   funding,
   246  		ContractFee: host.ContractPrice,
   247  		TxnFee:      txnFee,
   248  		SiafundFee:  types.Tax(startHeight, fc.Payout),
   249  		Utility: skymodules.ContractUtility{
   250  			GoodForUpload:  true,
   251  			GoodForRefresh: true,
   252  			GoodForRenew:   true,
   253  		},
   254  	}
   255  
   256  	// Add contract to set.
   257  	meta, err := cs.managedInsertContract(header, nil) // no Merkle roots yet
   258  	if err != nil {
   259  		return skymodules.RenterContract{}, nil, types.Transaction{}, nil, err
   260  	}
   261  	return meta, minSet, sweepTxn, sweepParents, nil
   262  }