gitlab.com/SiaPrime/SiaPrime@v1.4.1/modules/renter/proto/negotiate.go (about)

     1  package proto
     2  
     3  import (
     4  	"crypto/cipher"
     5  	"net"
     6  	"time"
     7  
     8  	"golang.org/x/crypto/chacha20poly1305"
     9  
    10  	"gitlab.com/NebulousLabs/errors"
    11  	"gitlab.com/SiaPrime/SiaPrime/build"
    12  	"gitlab.com/SiaPrime/SiaPrime/crypto"
    13  	"gitlab.com/SiaPrime/SiaPrime/encoding"
    14  	"gitlab.com/SiaPrime/SiaPrime/modules"
    15  	"gitlab.com/SiaPrime/SiaPrime/types"
    16  )
    17  
    18  // extendDeadline is a helper function for extending the connection timeout.
    19  func extendDeadline(conn net.Conn, d time.Duration) { _ = conn.SetDeadline(time.Now().Add(d)) }
    20  
    21  // startRevision is run at the beginning of each revision iteration. It reads
    22  // the host's settings confirms that the values are acceptable, and writes an acceptance.
    23  func startRevision(conn net.Conn, host modules.HostDBEntry) error {
    24  	// verify the host's settings and confirm its identity
    25  	_, err := verifySettings(conn, host)
    26  	if err != nil {
    27  		return err
    28  	}
    29  	return modules.WriteNegotiationAcceptance(conn)
    30  }
    31  
    32  // startDownload is run at the beginning of each download iteration. It reads
    33  // the host's settings confirms that the values are acceptable, and writes an acceptance.
    34  func startDownload(conn net.Conn, host modules.HostDBEntry) error {
    35  	// verify the host's settings and confirm its identity
    36  	_, err := verifySettings(conn, host)
    37  	if err != nil {
    38  		return err
    39  	}
    40  	return modules.WriteNegotiationAcceptance(conn)
    41  }
    42  
    43  // verifySettings reads a signed HostSettings object from conn, validates the
    44  // signature, and checks for discrepancies between the known settings and the
    45  // received settings. If there is a discrepancy, the hostDB is notified. The
    46  // received settings are returned.
    47  func verifySettings(conn net.Conn, host modules.HostDBEntry) (modules.HostDBEntry, error) {
    48  	// convert host key (types.SiaPublicKey) to a crypto.PublicKey
    49  	if host.PublicKey.Algorithm != types.SignatureEd25519 || len(host.PublicKey.Key) != crypto.PublicKeySize {
    50  		build.Critical("hostdb did not filter out host with wrong signature algorithm:", host.PublicKey.Algorithm)
    51  		return modules.HostDBEntry{}, errors.New("host used unsupported signature algorithm")
    52  	}
    53  	var pk crypto.PublicKey
    54  	copy(pk[:], host.PublicKey.Key)
    55  
    56  	// read signed host settings
    57  	var recvSettings modules.HostOldExternalSettings
    58  	if err := crypto.ReadSignedObject(conn, &recvSettings, modules.NegotiateMaxHostExternalSettingsLen, pk); err != nil {
    59  		return modules.HostDBEntry{}, errors.New("couldn't read host's settings: " + err.Error())
    60  	}
    61  	// TODO: check recvSettings against host.HostExternalSettings. If there is
    62  	// a discrepancy, write the error to conn.
    63  	if recvSettings.NetAddress != host.NetAddress {
    64  		// for now, just overwrite the NetAddress, since we know that
    65  		// host.NetAddress works (it was the one we dialed to get conn)
    66  		recvSettings.NetAddress = host.NetAddress
    67  	}
    68  	host.HostExternalSettings = modules.HostExternalSettings{
    69  		AcceptingContracts:     recvSettings.AcceptingContracts,
    70  		MaxDownloadBatchSize:   recvSettings.MaxDownloadBatchSize,
    71  		MaxDuration:            recvSettings.MaxDuration,
    72  		MaxReviseBatchSize:     recvSettings.MaxReviseBatchSize,
    73  		NetAddress:             recvSettings.NetAddress,
    74  		RemainingStorage:       recvSettings.RemainingStorage,
    75  		SectorSize:             recvSettings.SectorSize,
    76  		TotalStorage:           recvSettings.TotalStorage,
    77  		UnlockHash:             recvSettings.UnlockHash,
    78  		WindowSize:             recvSettings.WindowSize,
    79  		Collateral:             recvSettings.Collateral,
    80  		MaxCollateral:          recvSettings.MaxCollateral,
    81  		ContractPrice:          recvSettings.ContractPrice,
    82  		DownloadBandwidthPrice: recvSettings.DownloadBandwidthPrice,
    83  		StoragePrice:           recvSettings.StoragePrice,
    84  		UploadBandwidthPrice:   recvSettings.UploadBandwidthPrice,
    85  		RevisionNumber:         recvSettings.RevisionNumber,
    86  		Version:                recvSettings.Version,
    87  		// New fields are set to zero.
    88  		BaseRPCPrice:      types.ZeroCurrency,
    89  		SectorAccessPrice: types.ZeroCurrency,
    90  	}
    91  	return host, nil
    92  }
    93  
    94  // verifyRecentRevision confirms that the host and contractor agree upon the current
    95  // state of the contract being revised.
    96  func verifyRecentRevision(conn net.Conn, contract *SafeContract, hostVersion string) error {
    97  	// send contract ID
    98  	if err := encoding.WriteObject(conn, contract.header.ID()); err != nil {
    99  		return errors.New("couldn't send contract ID: " + err.Error())
   100  	}
   101  	// read challenge
   102  	var challenge crypto.Hash
   103  	if err := encoding.ReadObject(conn, &challenge, 32); err != nil {
   104  		return errors.New("couldn't read challenge: " + err.Error())
   105  	}
   106  	if build.VersionCmp(hostVersion, "1.3.0") >= 0 {
   107  		crypto.SecureWipe(challenge[:16])
   108  	}
   109  	// sign and return
   110  	sig := crypto.SignHash(challenge, contract.header.SecretKey)
   111  	if err := encoding.WriteObject(conn, sig); err != nil {
   112  		return errors.New("couldn't send challenge response: " + err.Error())
   113  	}
   114  	// read acceptance
   115  	if err := modules.ReadNegotiationAcceptance(conn); err != nil {
   116  		return errors.New("host did not accept revision request: " + err.Error())
   117  	}
   118  	// read last revision and signatures
   119  	var lastRevision types.FileContractRevision
   120  	var hostSignatures []types.TransactionSignature
   121  	if err := encoding.ReadObject(conn, &lastRevision, 2048); err != nil {
   122  		return errors.New("couldn't read last revision: " + err.Error())
   123  	}
   124  	if err := encoding.ReadObject(conn, &hostSignatures, 2048); err != nil {
   125  		return errors.New("couldn't read host signatures: " + err.Error())
   126  	}
   127  	// Check that the unlock hashes match; if they do not, something is
   128  	// seriously wrong. Otherwise, check that the revision numbers match.
   129  	ourRev := contract.header.LastRevision()
   130  	if lastRevision.UnlockConditions.UnlockHash() != ourRev.UnlockConditions.UnlockHash() {
   131  		return errors.New("unlock conditions do not match")
   132  	} else if lastRevision.NewRevisionNumber != ourRev.NewRevisionNumber {
   133  		// If the revision number doesn't match try to commit potential
   134  		// unapplied transactions and check again.
   135  		if err := contract.managedCommitTxns(); err != nil {
   136  			return errors.AddContext(err, "failed to commit transactions")
   137  		}
   138  		ourRev = contract.header.LastRevision()
   139  		if lastRevision.NewRevisionNumber != ourRev.NewRevisionNumber {
   140  			return &revisionNumberMismatchError{ourRev.NewRevisionNumber, lastRevision.NewRevisionNumber}
   141  		}
   142  	}
   143  	// NOTE: we can fake the blockheight here because it doesn't affect
   144  	// verification; it just needs to be above the fork height and below the
   145  	// contract expiration (which was checked earlier).
   146  	return modules.VerifyFileContractRevisionTransactionSignatures(lastRevision, hostSignatures, contract.header.EndHeight()-1)
   147  }
   148  
   149  // negotiateRevision sends a revision and actions to the host for approval,
   150  // completing one iteration of the revision loop.
   151  func negotiateRevision(conn net.Conn, rev types.FileContractRevision, secretKey crypto.SecretKey, height types.BlockHeight) (types.Transaction, error) {
   152  	// create transaction containing the revision
   153  	signedTxn := types.Transaction{
   154  		FileContractRevisions: []types.FileContractRevision{rev},
   155  		TransactionSignatures: []types.TransactionSignature{{
   156  			ParentID:       crypto.Hash(rev.ParentID),
   157  			CoveredFields:  types.CoveredFields{FileContractRevisions: []uint64{0}},
   158  			PublicKeyIndex: 0, // renter key is always first -- see formContract
   159  		}},
   160  	}
   161  	// sign the transaction
   162  	encodedSig := crypto.SignHash(signedTxn.SigHash(0, height), secretKey)
   163  	signedTxn.TransactionSignatures[0].Signature = encodedSig[:]
   164  
   165  	// send the revision
   166  	if err := encoding.WriteObject(conn, rev); err != nil {
   167  		return types.Transaction{}, errors.New("couldn't send revision: " + err.Error())
   168  	}
   169  	// read acceptance
   170  	if err := modules.ReadNegotiationAcceptance(conn); err != nil {
   171  		return types.Transaction{}, errors.New("host did not accept revision: " + err.Error())
   172  	}
   173  
   174  	// send the new transaction signature
   175  	if err := encoding.WriteObject(conn, signedTxn.TransactionSignatures[0]); err != nil {
   176  		return types.Transaction{}, errors.New("couldn't send transaction signature: " + err.Error())
   177  	}
   178  	// read the host's acceptance and transaction signature
   179  	// NOTE: if the host sends ErrStopResponse, we should continue processing
   180  	// the revision, but return the error anyway.
   181  	responseErr := modules.ReadNegotiationAcceptance(conn)
   182  	if responseErr != nil && responseErr != modules.ErrStopResponse {
   183  		return types.Transaction{}, errors.New("host did not accept transaction signature: " + responseErr.Error())
   184  	}
   185  	var hostSig types.TransactionSignature
   186  	if err := encoding.ReadObject(conn, &hostSig, 16e3); err != nil {
   187  		return types.Transaction{}, errors.New("couldn't read host's signature: " + err.Error())
   188  	}
   189  
   190  	// add the signature to the transaction and verify it
   191  	// NOTE: we can fake the blockheight here because it doesn't affect
   192  	// verification; it just needs to be above the fork height and below the
   193  	// contract expiration (which was checked earlier).
   194  	verificationHeight := rev.NewWindowStart - 1
   195  	signedTxn.TransactionSignatures = append(signedTxn.TransactionSignatures, hostSig)
   196  	if err := signedTxn.StandaloneValid(verificationHeight); err != nil {
   197  		return types.Transaction{}, err
   198  	}
   199  
   200  	// if the host sent ErrStopResponse, return it
   201  	return signedTxn, responseErr
   202  }
   203  
   204  // newRevision creates a copy of current with its revision number incremented,
   205  // and with cost transferred from the renter to the host.
   206  func newRevision(current types.FileContractRevision, cost types.Currency) types.FileContractRevision {
   207  	rev := current
   208  
   209  	// need to manually copy slice memory
   210  	rev.NewValidProofOutputs = make([]types.SiacoinOutput, 2)
   211  	rev.NewMissedProofOutputs = make([]types.SiacoinOutput, 3)
   212  	copy(rev.NewValidProofOutputs, current.NewValidProofOutputs)
   213  	copy(rev.NewMissedProofOutputs, current.NewMissedProofOutputs)
   214  
   215  	// move valid payout from renter to host
   216  	rev.NewValidProofOutputs[0].Value = current.NewValidProofOutputs[0].Value.Sub(cost)
   217  	rev.NewValidProofOutputs[1].Value = current.NewValidProofOutputs[1].Value.Add(cost)
   218  
   219  	// move missed payout from renter to void
   220  	rev.NewMissedProofOutputs[0].Value = current.NewMissedProofOutputs[0].Value.Sub(cost)
   221  	rev.NewMissedProofOutputs[2].Value = current.NewMissedProofOutputs[2].Value.Add(cost)
   222  
   223  	// increment revision number
   224  	rev.NewRevisionNumber++
   225  
   226  	return rev
   227  }
   228  
   229  // newDownloadRevision revises the current revision to cover the cost of
   230  // downloading data.
   231  func newDownloadRevision(current types.FileContractRevision, downloadCost types.Currency) types.FileContractRevision {
   232  	return newRevision(current, downloadCost)
   233  }
   234  
   235  // newUploadRevision revises the current revision to cover the cost of
   236  // uploading a sector.
   237  func newUploadRevision(current types.FileContractRevision, merkleRoot crypto.Hash, price, collateral types.Currency) types.FileContractRevision {
   238  	rev := newRevision(current, price)
   239  
   240  	// move collateral from host to void
   241  	rev.NewMissedProofOutputs[1].Value = rev.NewMissedProofOutputs[1].Value.Sub(collateral)
   242  	rev.NewMissedProofOutputs[2].Value = rev.NewMissedProofOutputs[2].Value.Add(collateral)
   243  
   244  	// set new filesize and Merkle root
   245  	rev.NewFileSize += modules.SectorSize
   246  	rev.NewFileMerkleRoot = merkleRoot
   247  	return rev
   248  }
   249  
   250  // newDeleteRevision revises the current revision to cover the cost of
   251  // deleting a sector.
   252  func newDeleteRevision(current types.FileContractRevision, merkleRoot crypto.Hash) types.FileContractRevision {
   253  	rev := newRevision(current, types.ZeroCurrency)
   254  	rev.NewFileSize -= modules.SectorSize
   255  	rev.NewFileMerkleRoot = merkleRoot
   256  	return rev
   257  }
   258  
   259  // newModifyRevision revises the current revision to cover the cost of
   260  // modifying a sector.
   261  func newModifyRevision(current types.FileContractRevision, merkleRoot crypto.Hash, uploadCost types.Currency) types.FileContractRevision {
   262  	rev := newRevision(current, uploadCost)
   263  	rev.NewFileMerkleRoot = merkleRoot
   264  	return rev
   265  }
   266  
   267  // performSessionHandshake conducts the initial handshake exchange of the
   268  // renter-host protocol. During the handshake, a shared secret is established,
   269  // which is used to initialize an AEAD cipher. This cipher must be used to
   270  // encrypt subsequent RPCs.
   271  func performSessionHandshake(conn net.Conn, hostPublicKey types.SiaPublicKey) (cipher.AEAD, modules.LoopChallengeRequest, error) {
   272  	// generate a session key
   273  	xsk, xpk := crypto.GenerateX25519KeyPair()
   274  
   275  	// send our half of the key exchange
   276  	req := modules.LoopKeyExchangeRequest{
   277  		PublicKey: xpk,
   278  		Ciphers:   []types.Specifier{modules.CipherChaCha20Poly1305},
   279  	}
   280  	extendDeadline(conn, modules.NegotiateSettingsTime)
   281  	if err := encoding.NewEncoder(conn).EncodeAll(modules.RPCLoopEnter, req); err != nil {
   282  		return nil, modules.LoopChallengeRequest{}, err
   283  	}
   284  	// read host's half of the key exchange
   285  	var resp modules.LoopKeyExchangeResponse
   286  	if err := encoding.NewDecoder(conn, encoding.DefaultAllocLimit).Decode(&resp); err != nil {
   287  		return nil, modules.LoopChallengeRequest{}, err
   288  	}
   289  	// validate the signature before doing anything else; don't want to punish
   290  	// the "host" if we're talking to an imposter
   291  	var hpk crypto.PublicKey
   292  	copy(hpk[:], hostPublicKey.Key)
   293  	var sig crypto.Signature
   294  	copy(sig[:], resp.Signature)
   295  	if err := crypto.VerifyHash(crypto.HashAll(req.PublicKey, resp.PublicKey), hpk, sig); err != nil {
   296  		return nil, modules.LoopChallengeRequest{}, err
   297  	}
   298  	// check for compatible cipher
   299  	if resp.Cipher != modules.CipherChaCha20Poly1305 {
   300  		return nil, modules.LoopChallengeRequest{}, errors.New("host selected unsupported cipher")
   301  	}
   302  	// derive shared secret, which we'll use as an encryption key
   303  	cipherKey := crypto.DeriveSharedSecret(xsk, resp.PublicKey)
   304  
   305  	// use cipherKey to initialize an AEAD cipher
   306  	aead, err := chacha20poly1305.New(cipherKey[:])
   307  	if err != nil {
   308  		build.Critical("could not create cipher")
   309  		return nil, modules.LoopChallengeRequest{}, err
   310  	}
   311  
   312  	// read host's challenge
   313  	var challengeReq modules.LoopChallengeRequest
   314  	if err := modules.ReadRPCMessage(conn, aead, &challengeReq, modules.RPCMinLen); err != nil {
   315  		return nil, modules.LoopChallengeRequest{}, err
   316  	}
   317  	return aead, challengeReq, nil
   318  }