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

     1  package contractor
     2  
     3  import (
     4  	"errors"
     5  	"sync"
     6  
     7  	"gitlab.com/SiaPrime/SiaPrime/build"
     8  	"gitlab.com/SiaPrime/SiaPrime/crypto"
     9  	"gitlab.com/SiaPrime/SiaPrime/modules"
    10  	"gitlab.com/SiaPrime/SiaPrime/modules/renter/proto"
    11  	"gitlab.com/SiaPrime/SiaPrime/types"
    12  )
    13  
    14  var errInvalidEditor = errors.New("editor has been invalidated because its contract is being renewed")
    15  
    16  // the contractor will cap host's MaxCollateral setting to this value
    17  var maxUploadCollateral = types.SiacoinPrecision.Mul64(1e3).Div(modules.BlockBytesPerMonthTerabyte) // 1k SC / TB / Month
    18  
    19  // An Editor modifies a Contract by communicating with a host. It uses the
    20  // contract revision protocol to send modification requests to the host.
    21  // Editors are the means by which the renter uploads data to hosts.
    22  type Editor interface {
    23  	// Upload revises the underlying contract to store the new data. It
    24  	// returns the Merkle root of the data.
    25  	Upload(data []byte) (root crypto.Hash, err error)
    26  
    27  	// Address returns the address of the host.
    28  	Address() modules.NetAddress
    29  
    30  	// ContractID returns the FileContractID of the contract.
    31  	ContractID() types.FileContractID
    32  
    33  	// EndHeight returns the height at which the contract ends.
    34  	EndHeight() types.BlockHeight
    35  
    36  	// Close terminates the connection to the host.
    37  	Close() error
    38  }
    39  
    40  // A hostEditor modifies a Contract by calling the revise RPC on a host. It
    41  // implements the Editor interface. hostEditors are safe for use by
    42  // multiple goroutines.
    43  type hostEditor struct {
    44  	clients    int // safe to Close when 0
    45  	contractor *Contractor
    46  	editor     *proto.Editor
    47  	endHeight  types.BlockHeight
    48  	id         types.FileContractID
    49  	invalid    bool // true if invalidate has been called
    50  	netAddress modules.NetAddress
    51  
    52  	mu sync.Mutex
    53  }
    54  
    55  // invalidate sets the invalid flag and closes the underlying proto.Editor.
    56  // Once invalidate returns, the hostEditor is guaranteed to not further revise
    57  // its contract. This is used during contract renewal to prevent an Editor
    58  // from revising a contract mid-renewal.
    59  func (he *hostEditor) invalidate() {
    60  	he.mu.Lock()
    61  	defer he.mu.Unlock()
    62  	if !he.invalid {
    63  		he.editor.Close()
    64  		he.invalid = true
    65  	}
    66  	he.contractor.mu.Lock()
    67  	delete(he.contractor.editors, he.id)
    68  	he.contractor.mu.Unlock()
    69  }
    70  
    71  // Address returns the NetAddress of the host.
    72  func (he *hostEditor) Address() modules.NetAddress { return he.netAddress }
    73  
    74  // ContractID returns the id of the contract being revised.
    75  func (he *hostEditor) ContractID() types.FileContractID { return he.id }
    76  
    77  // EndHeight returns the height at which the host is no longer obligated to
    78  // store the file.
    79  func (he *hostEditor) EndHeight() types.BlockHeight { return he.endHeight }
    80  
    81  // Close cleanly terminates the revision loop with the host and closes the
    82  // connection.
    83  func (he *hostEditor) Close() error {
    84  	he.mu.Lock()
    85  	defer he.mu.Unlock()
    86  	he.clients--
    87  	// Close is a no-op if invalidate has been called, or if there are other
    88  	// clients still using the hostEditor.
    89  	if he.invalid || he.clients > 0 {
    90  		return nil
    91  	}
    92  	he.invalid = true
    93  	he.contractor.mu.Lock()
    94  	delete(he.contractor.editors, he.id)
    95  	he.contractor.mu.Unlock()
    96  	return he.editor.Close()
    97  }
    98  
    99  // Upload negotiates a revision that adds a sector to a file contract.
   100  func (he *hostEditor) Upload(data []byte) (_ crypto.Hash, err error) {
   101  	he.mu.Lock()
   102  	defer he.mu.Unlock()
   103  	if he.invalid {
   104  		return crypto.Hash{}, errInvalidEditor
   105  	}
   106  
   107  	// Perform the upload.
   108  	_, sectorRoot, err := he.editor.Upload(data)
   109  	if err != nil {
   110  		return crypto.Hash{}, err
   111  	}
   112  	return sectorRoot, nil
   113  }
   114  
   115  // Editor returns a Editor object that can be used to upload, modify, and
   116  // delete sectors on a host.
   117  func (c *Contractor) Editor(pk types.SiaPublicKey, cancel <-chan struct{}) (_ Editor, err error) {
   118  	c.mu.RLock()
   119  	id, gotID := c.pubKeysToContractID[pk.String()]
   120  	cachedEditor, haveEditor := c.editors[id]
   121  	cachedSession, haveSession := c.sessions[id]
   122  	height := c.blockHeight
   123  	renewing := c.renewing[id]
   124  	c.mu.RUnlock()
   125  	if !gotID {
   126  		return nil, errors.New("failed to get filecontract id from key")
   127  	}
   128  	if renewing {
   129  		// Cannot use the editor if the contract is being renewed.
   130  		return nil, errors.New("currently renewing that contract")
   131  	} else if haveEditor {
   132  		// This editor already exists. Mark that there are now two routines
   133  		// using the editor, and then return the editor that already exists.
   134  		cachedEditor.mu.Lock()
   135  		cachedEditor.clients++
   136  		cachedEditor.mu.Unlock()
   137  		return cachedEditor, nil
   138  	} else if haveSession {
   139  		// This session already exists.
   140  		cachedSession.mu.Lock()
   141  		cachedSession.clients++
   142  		cachedSession.mu.Unlock()
   143  		return cachedSession, nil
   144  	}
   145  
   146  	// Check that the contract and host are both available, and run some brief
   147  	// sanity checks to see that the host is not swindling us.
   148  	contract, haveContract := c.staticContracts.View(id)
   149  	if !haveContract {
   150  		return nil, errors.New("no record of that contract")
   151  	}
   152  	host, haveHost := c.hdb.Host(contract.HostPublicKey)
   153  	if height > contract.EndHeight {
   154  		return nil, errors.New("contract has already ended")
   155  	} else if !haveHost {
   156  		return nil, errors.New("no record of that host")
   157  	} else if host.Filtered {
   158  		return nil, errors.New("host is blacklisted")
   159  	} else if host.StoragePrice.Cmp(maxStoragePrice) > 0 {
   160  		return nil, errTooExpensive
   161  	} else if host.UploadBandwidthPrice.Cmp(maxUploadPrice) > 0 {
   162  		return nil, errTooExpensive
   163  	}
   164  
   165  	// If host is >= 1.4.0, use the new renter-host protocol.
   166  	if build.VersionCmp(host.Version, "1.4.0") >= 0 {
   167  		return c.Session(pk, cancel)
   168  	}
   169  
   170  	// Create the editor.
   171  	e, err := c.staticContracts.NewEditor(host, contract.ID, height, c.hdb, cancel)
   172  	if err != nil {
   173  		return nil, err
   174  	}
   175  
   176  	// cache editor
   177  	he := &hostEditor{
   178  		clients:    1,
   179  		contractor: c,
   180  		editor:     e,
   181  		endHeight:  contract.EndHeight,
   182  		id:         id,
   183  		netAddress: host.NetAddress,
   184  	}
   185  	c.mu.Lock()
   186  	c.editors[contract.ID] = he
   187  	c.mu.Unlock()
   188  
   189  	return he, nil
   190  }