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

     1  package proto
     2  
     3  import (
     4  	"net"
     5  	"sync"
     6  	"time"
     7  
     8  	"gitlab.com/NebulousLabs/errors"
     9  	"gitlab.com/NebulousLabs/ratelimit"
    10  
    11  	"gitlab.com/NebulousLabs/encoding"
    12  	"gitlab.com/SkynetLabs/skyd/build"
    13  	"gitlab.com/SkynetLabs/skyd/skymodules"
    14  	"go.sia.tech/siad/crypto"
    15  	"go.sia.tech/siad/modules"
    16  	"go.sia.tech/siad/types"
    17  )
    18  
    19  // cachedMerkleRoot calculates the root of a set of existing Merkle roots.
    20  func cachedMerkleRoot(roots []crypto.Hash) crypto.Hash {
    21  	tree := crypto.NewCachedTree(sectorHeight) // NOTE: height is not strictly necessary here
    22  	for _, h := range roots {
    23  		tree.Push(h)
    24  	}
    25  	return tree.Root()
    26  }
    27  
    28  // A Editor modifies a Contract by calling the revise RPC on a host. It
    29  // Editors are NOT thread-safe; calls to Upload must happen in serial.
    30  type Editor struct {
    31  	contractID  types.FileContractID
    32  	contractSet *ContractSet
    33  	conn        net.Conn
    34  	closeChan   chan struct{}
    35  	deps        modules.Dependencies
    36  	hdb         hostDB
    37  	host        skymodules.HostDBEntry
    38  	once        sync.Once
    39  
    40  	height types.BlockHeight
    41  }
    42  
    43  // shutdown terminates the revision loop and signals the goroutine spawned in
    44  // NewEditor to return.
    45  func (he *Editor) shutdown() {
    46  	extendDeadline(he.conn, modules.NegotiateSettingsTime)
    47  	// don't care about these errors
    48  	_, _ = verifySettings(he.conn, he.host)
    49  	_ = modules.WriteNegotiationStop(he.conn)
    50  	close(he.closeChan)
    51  }
    52  
    53  // Close cleanly terminates the revision loop with the host and closes the
    54  // connection.
    55  func (he *Editor) Close() error {
    56  	// using once ensures that Close is idempotent
    57  	he.once.Do(he.shutdown)
    58  	return he.conn.Close()
    59  }
    60  
    61  // HostSettings returns the settings that are active in the current session.
    62  func (he *Editor) HostSettings() modules.HostExternalSettings {
    63  	return he.host.HostExternalSettings
    64  }
    65  
    66  // Upload negotiates a revision that adds a sector to a file contract.
    67  func (he *Editor) Upload(data []byte) (_ skymodules.RenterContract, _ crypto.Hash, err error) {
    68  	// Acquire the contract.
    69  	sc, haveContract := he.contractSet.Acquire(he.contractID)
    70  	if !haveContract {
    71  		return skymodules.RenterContract{}, crypto.Hash{}, errors.New("contract not present in contract set")
    72  	}
    73  	defer he.contractSet.Return(sc)
    74  	contract := sc.header // for convenience
    75  
    76  	// calculate price
    77  	// TODO: height is never updated, so we'll wind up overpaying on long-running uploads
    78  	blockBytes := types.NewCurrency64(modules.SectorSize * uint64(contract.LastRevision().NewWindowEnd-he.height))
    79  	sectorStoragePrice := he.host.StoragePrice.Mul(blockBytes)
    80  	sectorBandwidthPrice := he.host.UploadBandwidthPrice.Mul64(modules.SectorSize)
    81  	sectorCollateral := he.host.Collateral.Mul(blockBytes)
    82  
    83  	// to mitigate small errors (e.g. differing block heights), fudge the
    84  	// price and collateral by 0.2%. This is only applied to hosts above
    85  	// v1.0.1; older hosts use stricter math.
    86  	if build.VersionCmp(he.host.Version, "1.0.1") > 0 {
    87  		sectorStoragePrice = sectorStoragePrice.MulFloat(1 + hostPriceLeeway)
    88  		sectorBandwidthPrice = sectorBandwidthPrice.MulFloat(1 + hostPriceLeeway)
    89  		sectorCollateral = sectorCollateral.MulFloat(1 - hostPriceLeeway)
    90  	}
    91  
    92  	sectorPrice := sectorStoragePrice.Add(sectorBandwidthPrice)
    93  	if contract.RenterFunds().Cmp(sectorPrice) < 0 {
    94  		return skymodules.RenterContract{}, crypto.Hash{}, errors.New("contract has insufficient funds to support upload")
    95  	}
    96  	if contract.LastRevision().MissedHostOutput().Value.Cmp(sectorCollateral) < 0 {
    97  		sectorCollateral = contract.LastRevision().MissedHostOutput().Value
    98  	}
    99  
   100  	// calculate the new Merkle root
   101  	sectorRoot := crypto.MerkleRoot(data)
   102  	merkleRoot := sc.merkleRoots.checkNewRoot(sectorRoot)
   103  
   104  	// create the action and revision
   105  	actions := []modules.RevisionAction{{
   106  		Type:        modules.ActionInsert,
   107  		SectorIndex: uint64(sc.merkleRoots.len()),
   108  		Data:        data,
   109  	}}
   110  	rev, err := newUploadRevision(contract.LastRevision(), merkleRoot, sectorPrice, sectorCollateral)
   111  	if err != nil {
   112  		return skymodules.RenterContract{}, crypto.Hash{}, errors.AddContext(err, "Error creating new upload revision")
   113  	}
   114  
   115  	// run the revision iteration
   116  	defer func() {
   117  		// Increase Successful/Failed interactions accordingly
   118  		if err != nil {
   119  			// If the host was OOS, we update the contract utility.
   120  			if modules.IsOOSErr(err) {
   121  				u := sc.Utility()
   122  				u.GoodForUpload = false // Stop uploading to such a host immediately.
   123  				u.LastOOSErr = he.height
   124  				err = errors.Compose(err, sc.UpdateUtility(u))
   125  			}
   126  			he.hdb.IncrementFailedInteractions(he.host.PublicKey)
   127  			err = errors.Extend(err, skymodules.ErrHostFault)
   128  		} else {
   129  			he.hdb.IncrementSuccessfulInteractions(he.host.PublicKey)
   130  		}
   131  
   132  		// reset deadline
   133  		extendDeadline(he.conn, time.Hour)
   134  	}()
   135  
   136  	// initiate revision
   137  	extendDeadline(he.conn, modules.NegotiateSettingsTime)
   138  	if err := startRevision(he.conn, he.host); err != nil {
   139  		return skymodules.RenterContract{}, crypto.Hash{}, err
   140  	}
   141  
   142  	// record the change we are about to make to the contract. If we lose power
   143  	// mid-revision, this allows us to restore either the pre-revision or
   144  	// post-revision contract.
   145  	walTxn, err := sc.managedRecordRootUpdates(rev, map[uint64]rootUpdate{
   146  		uint64(sc.merkleRoots.len()): newRootUpdateAppendRoot(sectorRoot),
   147  	}, sectorStoragePrice, sectorBandwidthPrice)
   148  	if err != nil {
   149  		return skymodules.RenterContract{}, crypto.Hash{}, err
   150  	}
   151  
   152  	// send actions
   153  	extendDeadline(he.conn, modules.NegotiateFileContractRevisionTime)
   154  	if err := encoding.WriteObject(he.conn, actions); err != nil {
   155  		return skymodules.RenterContract{}, crypto.Hash{}, err
   156  	}
   157  
   158  	// Disrupt here before sending the signed revision to the host.
   159  	if he.deps.Disrupt("InterruptUploadBeforeSendingRevision") {
   160  		return skymodules.RenterContract{}, crypto.Hash{},
   161  			errors.New("InterruptUploadBeforeSendingRevision disrupt")
   162  	}
   163  
   164  	// send revision to host and exchange signatures
   165  	extendDeadline(he.conn, connTimeout)
   166  	signedTxn, err := negotiateRevision(he.conn, rev, contract.SecretKey, he.height)
   167  	if errors.Contains(err, modules.ErrStopResponse) {
   168  		// if host gracefully closed, close our connection as well; this will
   169  		// cause the next operation to fail
   170  		he.conn.Close()
   171  	} else if err != nil {
   172  		return skymodules.RenterContract{}, crypto.Hash{}, err
   173  	}
   174  
   175  	// Disrupt here before updating the contract.
   176  	if he.deps.Disrupt("InterruptUploadAfterSendingRevision") {
   177  		return skymodules.RenterContract{}, crypto.Hash{},
   178  			errors.New("InterruptUploadAfterSendingRevision disrupt")
   179  	}
   180  
   181  	// update contract
   182  	err = sc.managedCommitAppend(walTxn, signedTxn, sectorStoragePrice, sectorBandwidthPrice)
   183  	if err != nil {
   184  		return skymodules.RenterContract{}, crypto.Hash{}, err
   185  	}
   186  
   187  	// Sanity check: Make sure the contract on disk has the right root.
   188  	if build.Release == "testing" {
   189  		// Check cached root first.
   190  		if sc.merkleRoots.root() != merkleRoot {
   191  			build.Critical("write: cached root mismatch")
   192  		}
   193  		// Check on-disk root.
   194  		roots, err := sc.merkleRoots.merkleRoots()
   195  		if err != nil {
   196  			build.Critical("failed to fetch roots for sanity check")
   197  		}
   198  		if cachedMerkleRoot(roots) != merkleRoot {
   199  			build.Critical("write: root mismatch")
   200  		}
   201  	}
   202  
   203  	return sc.Metadata(), sectorRoot, nil
   204  }
   205  
   206  // NewEditor initiates the contract revision process with a host, and returns
   207  // an Editor.
   208  func (cs *ContractSet) NewEditor(host skymodules.HostDBEntry, id types.FileContractID, currentHeight types.BlockHeight, hdb hostDB, cancel <-chan struct{}) (_ *Editor, err error) {
   209  	sc, ok := cs.Acquire(id)
   210  	if !ok {
   211  		return nil, errors.New("new editor unable to find contract in contract set")
   212  	}
   213  	defer cs.Return(sc)
   214  	contract := sc.header
   215  
   216  	// Increase Successful/Failed interactions accordingly
   217  	defer func() {
   218  		// a revision mismatch is not necessarily the host's fault
   219  		if err != nil && !IsRevisionMismatch(err) {
   220  			hdb.IncrementFailedInteractions(contract.HostPublicKey())
   221  			err = errors.Extend(err, skymodules.ErrHostFault)
   222  		} else if err == nil {
   223  			hdb.IncrementSuccessfulInteractions(contract.HostPublicKey())
   224  		}
   225  	}()
   226  
   227  	conn, closeChan, err := initiateRevisionLoop(host, sc, modules.RPCReviseContract, cancel, cs.staticRL)
   228  	if err != nil {
   229  		return nil, errors.AddContext(err, "failed to initiate revision loop")
   230  	}
   231  	// if we succeeded, we can safely discard the unappliedTxns
   232  	if err := sc.clearUnappliedTxns(); err != nil {
   233  		return nil, errors.AddContext(err, "failed to clear unapplied txns")
   234  	}
   235  
   236  	// the host is now ready to accept revisions
   237  	return &Editor{
   238  		host:        host,
   239  		hdb:         hdb,
   240  		contractID:  id,
   241  		contractSet: cs,
   242  		conn:        conn,
   243  		closeChan:   closeChan,
   244  		deps:        cs.staticDeps,
   245  
   246  		height: currentHeight,
   247  	}, nil
   248  }
   249  
   250  // initiateRevisionLoop initiates either the editor or downloader loop with
   251  // host, depending on which rpc was passed.
   252  func initiateRevisionLoop(host skymodules.HostDBEntry, contract *SafeContract, rpc types.Specifier, cancel <-chan struct{}, rl *ratelimit.RateLimit) (net.Conn, chan struct{}, error) {
   253  	c, err := (&net.Dialer{
   254  		Cancel:  cancel,
   255  		Timeout: 45 * time.Second, // TODO: Constant
   256  	}).Dial("tcp", string(host.NetAddress))
   257  	if err != nil {
   258  		return nil, nil, err
   259  	}
   260  	// Apply the local ratelimit.
   261  	conn := ratelimit.NewRLConn(c, rl, cancel)
   262  	// Apply the global ratelimit.
   263  	conn = ratelimit.NewRLConn(conn, skymodules.GlobalRateLimits, cancel)
   264  
   265  	closeChan := make(chan struct{})
   266  	go func() {
   267  		select {
   268  		case <-cancel:
   269  			conn.Close()
   270  		case <-closeChan:
   271  		}
   272  	}()
   273  
   274  	// allot 2 minutes for RPC request + revision exchange
   275  	extendDeadline(conn, modules.NegotiateRecentRevisionTime)
   276  	defer extendDeadline(conn, time.Hour)
   277  	if err := encoding.WriteObject(conn, rpc); err != nil {
   278  		conn.Close()
   279  		close(closeChan)
   280  		return nil, closeChan, errors.New("couldn't initiate RPC: " + err.Error())
   281  	}
   282  	if err := verifyRecentRevision(conn, contract, host.Version); err != nil {
   283  		conn.Close() // TODO: close gracefully if host has entered revision loop
   284  		close(closeChan)
   285  		return nil, closeChan, errors.AddContext(err, "verifyRecentRevision failed")
   286  	}
   287  	return conn, closeChan, nil
   288  }