gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/contractor/session.go (about) 1 package contractor 2 3 import ( 4 "sync" 5 6 "gitlab.com/NebulousLabs/errors" 7 8 "gitlab.com/SkynetLabs/skyd/skymodules/renter/proto" 9 "go.sia.tech/siad/crypto" 10 "go.sia.tech/siad/modules" 11 "go.sia.tech/siad/types" 12 ) 13 14 var errInvalidSession = errors.New("session has been invalidated because its contract is being renewed") 15 16 // A Session modifies a Contract by communicating with a host. It uses the 17 // renter-host protocol to send modification requests to the host. Among other 18 // things, Sessions are the means by which the renter transfers file data to 19 // and from hosts. 20 type Session interface { 21 // Address returns the address of the host. 22 Address() modules.NetAddress 23 24 // Close terminates the connection to the host. 25 Close() error 26 27 // ContractID returns the FileContractID of the contract. 28 ContractID() types.FileContractID 29 30 // Download requests the specified sector data. 31 Download(root crypto.Hash, offset, length uint32) ([]byte, error) 32 33 // DownloadIndex requests data from the sector with the specified index 34 // within the contract. 35 DownloadIndex(index uint64, offset, length uint32) ([]byte, error) 36 37 // EndHeight returns the height at which the contract ends. 38 EndHeight() types.BlockHeight 39 40 // Replace replaces the sector at the specified index with data. The old 41 // sector is swapped to the end of the contract data, and is deleted if the 42 // trim flag is set. 43 Replace(data []byte, sectorIndex uint64, trim bool) (crypto.Hash, error) 44 45 // HostSettings will return the currently active host settings for a 46 // session, which allows the workers to check for price gouging and 47 // determine whether or not an operation should continue. 48 HostSettings() modules.HostExternalSettings 49 50 // Settings calls the Session RPC and updates the active host settings. 51 Settings() (modules.HostExternalSettings, error) 52 53 // Upload revises the underlying contract to store the new data. It 54 // returns the Merkle root of the data. 55 Upload(data []byte) (crypto.Hash, error) 56 } 57 58 // A hostSession modifies a Contract via the renter-host RPC loop. It 59 // implements the Session interface. hostSessions are safe for use by multiple 60 // goroutines. 61 type hostSession struct { 62 clients int // safe to Close when 0 63 invalid bool // true if invalidate has been called 64 65 // Static Fields 66 staticContractor *Contractor 67 staticEndHeight types.BlockHeight 68 staticID types.FileContractID 69 staticNetAddress modules.NetAddress 70 staticSession *proto.Session 71 72 mu sync.Mutex 73 } 74 75 // callInvalidate sets the invalid flag and closes the underlying proto.Session. 76 // Once callInvalidate returns, the hostSession is guaranteed to not further 77 // revise its contract. This is used during contract renewal to prevent an 78 // Session from revising a contract mid-renewal. 79 func (hs *hostSession) callInvalidate() { 80 hs.mu.Lock() 81 defer hs.mu.Unlock() 82 if hs.invalid { 83 return 84 } 85 hs.staticSession.Close() 86 hs.staticContractor.mu.Lock() 87 delete(hs.staticContractor.sessions, hs.staticID) 88 hs.staticContractor.mu.Unlock() 89 hs.invalid = true 90 } 91 92 // Address returns the NetAddress of the host. 93 func (hs *hostSession) Address() modules.NetAddress { return hs.staticNetAddress } 94 95 // Close cleanly terminates the revision loop with the host and closes the 96 // connection. 97 func (hs *hostSession) Close() error { 98 hs.mu.Lock() 99 defer hs.mu.Unlock() 100 hs.clients-- 101 // Close is a no-op if invalidate has been called, or if there are other 102 // clients still using the hostSession. 103 if hs.invalid || hs.clients > 0 { 104 return nil 105 } 106 hs.invalid = true 107 hs.staticContractor.mu.Lock() 108 delete(hs.staticContractor.sessions, hs.staticID) 109 hs.staticContractor.mu.Unlock() 110 111 return hs.staticSession.Close() 112 } 113 114 // ContractID returns the ID of the contract being revised. 115 func (hs *hostSession) ContractID() types.FileContractID { return hs.staticID } 116 117 // Download retrieves the sector with the specified Merkle root, and revises 118 // the underlying contract to pay the host proportionally to the data 119 // retrieved. 120 func (hs *hostSession) Download(root crypto.Hash, offset, length uint32) ([]byte, error) { 121 hs.mu.Lock() 122 defer hs.mu.Unlock() 123 if hs.invalid { 124 return nil, errInvalidSession 125 } 126 127 // Download the data. 128 _, data, err := hs.staticSession.ReadSection(root, offset, length) 129 if err != nil { 130 return nil, err 131 } 132 return data, nil 133 } 134 135 // DownloadIndex retrieves the sector with the specified index. 136 func (hs *hostSession) DownloadIndex(index uint64, offset, length uint32) ([]byte, error) { 137 hs.mu.Lock() 138 defer hs.mu.Unlock() 139 if hs.invalid { 140 return nil, errInvalidSession 141 } 142 143 // Retrieve the Merkle root for the index. 144 _, roots, err := hs.staticSession.SectorRoots(modules.LoopSectorRootsRequest{ 145 RootOffset: index, 146 NumRoots: 1, 147 }) 148 if err != nil { 149 return nil, err 150 } 151 152 // Download the data. 153 _, data, err := hs.staticSession.ReadSection(roots[0], offset, length) 154 if err != nil { 155 return nil, err 156 } 157 return data, nil 158 } 159 160 // EndHeight returns the height at which the host is no longer obligated to 161 // store the file. 162 func (hs *hostSession) EndHeight() types.BlockHeight { return hs.staticEndHeight } 163 164 // Upload negotiates a revision that adds a sector to a file contract. 165 func (hs *hostSession) Upload(data []byte) (crypto.Hash, error) { 166 hs.mu.Lock() 167 defer hs.mu.Unlock() 168 if hs.invalid { 169 return crypto.Hash{}, errInvalidSession 170 } 171 172 // Perform the upload. 173 _, sectorRoot, err := hs.staticSession.Append(data) 174 if err != nil { 175 // Return the sector root so that it can be logged and used for 176 // debugging in the event of an error. 177 return sectorRoot, err 178 } 179 return sectorRoot, nil 180 } 181 182 // Replace replaces the sector at the specified index with data. 183 func (hs *hostSession) Replace(data []byte, sectorIndex uint64, trim bool) (crypto.Hash, error) { 184 hs.mu.Lock() 185 defer hs.mu.Unlock() 186 if hs.invalid { 187 return crypto.Hash{}, errInvalidSession 188 } 189 190 _, sectorRoot, err := hs.staticSession.Replace(data, sectorIndex, trim) 191 if err != nil { 192 return crypto.Hash{}, errors.AddContext(err, "unable to perform replace operation in session") 193 } 194 return sectorRoot, nil 195 } 196 197 // HostSettings returns the currently active host settings for the session. 198 func (hs *hostSession) HostSettings() modules.HostExternalSettings { 199 return hs.staticSession.HostSettings() 200 } 201 202 // Settings calls the Session RPC and updates the active host settings. 203 func (hs *hostSession) Settings() (modules.HostExternalSettings, error) { 204 return hs.staticSession.Settings() 205 } 206 207 // Session returns a Session object that can be used to upload, modify, and 208 // delete sectors on a host. 209 func (c *Contractor) Session(pk types.SiaPublicKey, cancel <-chan struct{}) (_ Session, err error) { 210 c.mu.RLock() 211 id, gotID := c.pubKeysToContractID[pk.String()] 212 cachedSession, haveSession := c.sessions[id] 213 height := c.blockHeight 214 renewing := c.renewing[id] 215 c.mu.RUnlock() 216 if !gotID { 217 return nil, errors.New("failed to get filecontract id from key") 218 } 219 if renewing { 220 // Cannot use the session if the contract is being renewed. 221 return nil, ErrContractRenewing 222 } else if haveSession { 223 // This session already exists. Mark that there are now two routines 224 // using the session, and then return the session that already exists. 225 cachedSession.mu.Lock() 226 cachedSession.clients++ 227 cachedSession.mu.Unlock() 228 return cachedSession, nil 229 } 230 231 // Check that the contract and host are both available, and run some brief 232 // sanity checks to see that the host is not swindling us. 233 contract, haveContract := c.staticContracts.View(id) 234 if !haveContract { 235 return nil, errors.New("contract not found in the renter contract set") 236 } 237 host, haveHost, err := c.staticHDB.Host(contract.HostPublicKey) 238 if err != nil { 239 return nil, errors.AddContext(err, "error getting host from hostdb:") 240 } else if height > contract.EndHeight { 241 return nil, errContractEnded 242 } else if !haveHost { 243 return nil, errHostNotFound 244 } else if host.Filtered { 245 return nil, errHostBlocked 246 } else if host.StoragePrice.Cmp(maxStoragePrice) > 0 { 247 return nil, errTooExpensive 248 } else if host.UploadBandwidthPrice.Cmp(maxUploadPrice) > 0 { 249 return nil, errTooExpensive 250 } 251 252 // Create the session. 253 s, err := c.staticContracts.NewSession(host, id, height, c.staticHDB, c.staticLog.Logger, cancel) 254 if modules.IsContractNotRecognizedErr(err) { 255 err = errors.Compose(err, c.MarkContractBad(id)) 256 } 257 if err != nil { 258 return nil, err 259 } 260 261 // cache session 262 hs := &hostSession{ 263 clients: 1, 264 staticContractor: c, 265 staticSession: s, 266 staticEndHeight: contract.EndHeight, 267 staticID: id, 268 staticNetAddress: host.NetAddress, 269 } 270 c.mu.Lock() 271 c.sessions[contract.ID] = hs 272 c.mu.Unlock() 273 274 return hs, nil 275 }