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 }