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

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