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  }