github.com/nebulouslabs/sia@v1.3.7/modules/renter/proto/negotiate.go (about)

     1  package proto
     2  
     3  import (
     4  	"errors"
     5  	"net"
     6  	"time"
     7  
     8  	"github.com/NebulousLabs/Sia/build"
     9  	"github.com/NebulousLabs/Sia/crypto"
    10  	"github.com/NebulousLabs/Sia/encoding"
    11  	"github.com/NebulousLabs/Sia/modules"
    12  	"github.com/NebulousLabs/Sia/types"
    13  )
    14  
    15  // extendDeadline is a helper function for extending the connection timeout.
    16  func extendDeadline(conn net.Conn, d time.Duration) { _ = conn.SetDeadline(time.Now().Add(d)) }
    17  
    18  // startRevision is run at the beginning of each revision iteration. It reads
    19  // the host's settings confirms that the values are acceptable, and writes an acceptance.
    20  func startRevision(conn net.Conn, host modules.HostDBEntry) error {
    21  	// verify the host's settings and confirm its identity
    22  	_, err := verifySettings(conn, host)
    23  	if err != nil {
    24  		return err
    25  	}
    26  	return modules.WriteNegotiationAcceptance(conn)
    27  }
    28  
    29  // startDownload is run at the beginning of each download iteration. It reads
    30  // the host's settings confirms that the values are acceptable, and writes an acceptance.
    31  func startDownload(conn net.Conn, host modules.HostDBEntry) error {
    32  	// verify the host's settings and confirm its identity
    33  	_, err := verifySettings(conn, host)
    34  	if err != nil {
    35  		return err
    36  	}
    37  	return modules.WriteNegotiationAcceptance(conn)
    38  }
    39  
    40  // verifySettings reads a signed HostSettings object from conn, validates the
    41  // signature, and checks for discrepancies between the known settings and the
    42  // received settings. If there is a discrepancy, the hostDB is notified. The
    43  // received settings are returned.
    44  func verifySettings(conn net.Conn, host modules.HostDBEntry) (modules.HostDBEntry, error) {
    45  	// convert host key (types.SiaPublicKey) to a crypto.PublicKey
    46  	if host.PublicKey.Algorithm != types.SignatureEd25519 || len(host.PublicKey.Key) != crypto.PublicKeySize {
    47  		build.Critical("hostdb did not filter out host with wrong signature algorithm:", host.PublicKey.Algorithm)
    48  		return modules.HostDBEntry{}, errors.New("host used unsupported signature algorithm")
    49  	}
    50  	var pk crypto.PublicKey
    51  	copy(pk[:], host.PublicKey.Key)
    52  
    53  	// read signed host settings
    54  	var recvSettings modules.HostExternalSettings
    55  	if err := crypto.ReadSignedObject(conn, &recvSettings, modules.NegotiateMaxHostExternalSettingsLen, pk); err != nil {
    56  		return modules.HostDBEntry{}, errors.New("couldn't read host's settings: " + err.Error())
    57  	}
    58  	// TODO: check recvSettings against host.HostExternalSettings. If there is
    59  	// a discrepancy, write the error to conn.
    60  	if recvSettings.NetAddress != host.NetAddress {
    61  		// for now, just overwrite the NetAddress, since we know that
    62  		// host.NetAddress works (it was the one we dialed to get conn)
    63  		recvSettings.NetAddress = host.NetAddress
    64  	}
    65  	host.HostExternalSettings = recvSettings
    66  	return host, nil
    67  }
    68  
    69  // verifyRecentRevision confirms that the host and contractor agree upon the current
    70  // state of the contract being revised.
    71  func verifyRecentRevision(conn net.Conn, contract contractHeader, hostVersion string) error {
    72  	// send contract ID
    73  	if err := encoding.WriteObject(conn, contract.ID()); err != nil {
    74  		return errors.New("couldn't send contract ID: " + err.Error())
    75  	}
    76  	// read challenge
    77  	var challenge crypto.Hash
    78  	if err := encoding.ReadObject(conn, &challenge, 32); err != nil {
    79  		return errors.New("couldn't read challenge: " + err.Error())
    80  	}
    81  	if build.VersionCmp(hostVersion, "1.3.0") >= 0 {
    82  		crypto.SecureWipe(challenge[:16])
    83  	}
    84  	// sign and return
    85  	sig := crypto.SignHash(challenge, contract.SecretKey)
    86  	if err := encoding.WriteObject(conn, sig); err != nil {
    87  		return errors.New("couldn't send challenge response: " + err.Error())
    88  	}
    89  	// read acceptance
    90  	if err := modules.ReadNegotiationAcceptance(conn); err != nil {
    91  		return errors.New("host did not accept revision request: " + err.Error())
    92  	}
    93  	// read last revision and signatures
    94  	var lastRevision types.FileContractRevision
    95  	var hostSignatures []types.TransactionSignature
    96  	if err := encoding.ReadObject(conn, &lastRevision, 2048); err != nil {
    97  		return errors.New("couldn't read last revision: " + err.Error())
    98  	}
    99  	if err := encoding.ReadObject(conn, &hostSignatures, 2048); err != nil {
   100  		return errors.New("couldn't read host signatures: " + err.Error())
   101  	}
   102  	// Check that the unlock hashes match; if they do not, something is
   103  	// seriously wrong. Otherwise, check that the revision numbers match.
   104  	ourRev := contract.LastRevision()
   105  	if lastRevision.UnlockConditions.UnlockHash() != ourRev.UnlockConditions.UnlockHash() {
   106  		return errors.New("unlock conditions do not match")
   107  	} else if lastRevision.NewRevisionNumber != ourRev.NewRevisionNumber {
   108  		return &recentRevisionError{ourRev.NewRevisionNumber, lastRevision.NewRevisionNumber}
   109  	}
   110  	// NOTE: we can fake the blockheight here because it doesn't affect
   111  	// verification; it just needs to be above the fork height and below the
   112  	// contract expiration (which was checked earlier).
   113  	return modules.VerifyFileContractRevisionTransactionSignatures(lastRevision, hostSignatures, contract.EndHeight()-1)
   114  }
   115  
   116  // negotiateRevision sends a revision and actions to the host for approval,
   117  // completing one iteration of the revision loop.
   118  func negotiateRevision(conn net.Conn, rev types.FileContractRevision, secretKey crypto.SecretKey) (types.Transaction, error) {
   119  	// create transaction containing the revision
   120  	signedTxn := types.Transaction{
   121  		FileContractRevisions: []types.FileContractRevision{rev},
   122  		TransactionSignatures: []types.TransactionSignature{{
   123  			ParentID:       crypto.Hash(rev.ParentID),
   124  			CoveredFields:  types.CoveredFields{FileContractRevisions: []uint64{0}},
   125  			PublicKeyIndex: 0, // renter key is always first -- see formContract
   126  		}},
   127  	}
   128  	// sign the transaction
   129  	encodedSig := crypto.SignHash(signedTxn.SigHash(0), secretKey)
   130  	signedTxn.TransactionSignatures[0].Signature = encodedSig[:]
   131  
   132  	// send the revision
   133  	if err := encoding.WriteObject(conn, rev); err != nil {
   134  		return types.Transaction{}, errors.New("couldn't send revision: " + err.Error())
   135  	}
   136  	// read acceptance
   137  	if err := modules.ReadNegotiationAcceptance(conn); err != nil {
   138  		return types.Transaction{}, errors.New("host did not accept revision: " + err.Error())
   139  	}
   140  
   141  	// send the new transaction signature
   142  	if err := encoding.WriteObject(conn, signedTxn.TransactionSignatures[0]); err != nil {
   143  		return types.Transaction{}, errors.New("couldn't send transaction signature: " + err.Error())
   144  	}
   145  	// read the host's acceptance and transaction signature
   146  	// NOTE: if the host sends ErrStopResponse, we should continue processing
   147  	// the revision, but return the error anyway.
   148  	responseErr := modules.ReadNegotiationAcceptance(conn)
   149  	if responseErr != nil && responseErr != modules.ErrStopResponse {
   150  		return types.Transaction{}, errors.New("host did not accept transaction signature: " + responseErr.Error())
   151  	}
   152  	var hostSig types.TransactionSignature
   153  	if err := encoding.ReadObject(conn, &hostSig, 16e3); err != nil {
   154  		return types.Transaction{}, errors.New("couldn't read host's signature: " + err.Error())
   155  	}
   156  
   157  	// add the signature to the transaction and verify it
   158  	// NOTE: we can fake the blockheight here because it doesn't affect
   159  	// verification; it just needs to be above the fork height and below the
   160  	// contract expiration (which was checked earlier).
   161  	verificationHeight := rev.NewWindowStart - 1
   162  	signedTxn.TransactionSignatures = append(signedTxn.TransactionSignatures, hostSig)
   163  	if err := signedTxn.StandaloneValid(verificationHeight); err != nil {
   164  		return types.Transaction{}, err
   165  	}
   166  
   167  	// if the host sent ErrStopResponse, return it
   168  	return signedTxn, responseErr
   169  }
   170  
   171  // newRevision creates a copy of current with its revision number incremented,
   172  // and with cost transferred from the renter to the host.
   173  func newRevision(current types.FileContractRevision, cost types.Currency) types.FileContractRevision {
   174  	rev := current
   175  
   176  	// need to manually copy slice memory
   177  	rev.NewValidProofOutputs = make([]types.SiacoinOutput, 2)
   178  	rev.NewMissedProofOutputs = make([]types.SiacoinOutput, 3)
   179  	copy(rev.NewValidProofOutputs, current.NewValidProofOutputs)
   180  	copy(rev.NewMissedProofOutputs, current.NewMissedProofOutputs)
   181  
   182  	// move valid payout from renter to host
   183  	rev.NewValidProofOutputs[0].Value = current.NewValidProofOutputs[0].Value.Sub(cost)
   184  	rev.NewValidProofOutputs[1].Value = current.NewValidProofOutputs[1].Value.Add(cost)
   185  
   186  	// move missed payout from renter to void
   187  	rev.NewMissedProofOutputs[0].Value = current.NewMissedProofOutputs[0].Value.Sub(cost)
   188  	rev.NewMissedProofOutputs[2].Value = current.NewMissedProofOutputs[2].Value.Add(cost)
   189  
   190  	// increment revision number
   191  	rev.NewRevisionNumber++
   192  
   193  	return rev
   194  }
   195  
   196  // newDownloadRevision revises the current revision to cover the cost of
   197  // downloading data.
   198  func newDownloadRevision(current types.FileContractRevision, downloadCost types.Currency) types.FileContractRevision {
   199  	return newRevision(current, downloadCost)
   200  }
   201  
   202  // newUploadRevision revises the current revision to cover the cost of
   203  // uploading a sector.
   204  func newUploadRevision(current types.FileContractRevision, merkleRoot crypto.Hash, price, collateral types.Currency) types.FileContractRevision {
   205  	rev := newRevision(current, price)
   206  
   207  	// move collateral from host to void
   208  	rev.NewMissedProofOutputs[1].Value = rev.NewMissedProofOutputs[1].Value.Sub(collateral)
   209  	rev.NewMissedProofOutputs[2].Value = rev.NewMissedProofOutputs[2].Value.Add(collateral)
   210  
   211  	// set new filesize and Merkle root
   212  	rev.NewFileSize += modules.SectorSize
   213  	rev.NewFileMerkleRoot = merkleRoot
   214  	return rev
   215  }
   216  
   217  // newDeleteRevision revises the current revision to cover the cost of
   218  // deleting a sector.
   219  func newDeleteRevision(current types.FileContractRevision, merkleRoot crypto.Hash) types.FileContractRevision {
   220  	rev := newRevision(current, types.ZeroCurrency)
   221  	rev.NewFileSize -= modules.SectorSize
   222  	rev.NewFileMerkleRoot = merkleRoot
   223  	return rev
   224  }
   225  
   226  // newModifyRevision revises the current revision to cover the cost of
   227  // modifying a sector.
   228  func newModifyRevision(current types.FileContractRevision, merkleRoot crypto.Hash, uploadCost types.Currency) types.FileContractRevision {
   229  	rev := newRevision(current, uploadCost)
   230  	rev.NewFileMerkleRoot = merkleRoot
   231  	return rev
   232  }