gitlab.com/SiaPrime/SiaPrime@v1.4.1/modules/renter/contractor/session.go (about)

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