gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/contractor/editor.go (about)

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