github.com/fozzysec/SiaPrime@v0.0.0-20190612043147-66c8e8d11fe3/modules/renter/proto/editor.go (about)

     1  package proto
     2  
     3  import (
     4  	"net"
     5  	"sync"
     6  	"time"
     7  
     8  	"SiaPrime/build"
     9  	"SiaPrime/crypto"
    10  	"SiaPrime/encoding"
    11  	"SiaPrime/modules"
    12  	"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  		return modules.RenterContract{}, crypto.Hash{}, errors.New("contract has insufficient collateral to support upload")
    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  			he.hdb.IncrementFailedInteractions(he.host.PublicKey)
   111  			err = errors.Extend(err, modules.ErrHostFault)
   112  		} else {
   113  			he.hdb.IncrementSuccessfulInteractions(he.host.PublicKey)
   114  		}
   115  
   116  		// reset deadline
   117  		extendDeadline(he.conn, time.Hour)
   118  	}()
   119  
   120  	// initiate revision
   121  	extendDeadline(he.conn, modules.NegotiateSettingsTime)
   122  	if err := startRevision(he.conn, he.host); err != nil {
   123  		return modules.RenterContract{}, crypto.Hash{}, err
   124  	}
   125  
   126  	// record the change we are about to make to the contract. If we lose power
   127  	// mid-revision, this allows us to restore either the pre-revision or
   128  	// post-revision contract.
   129  	walTxn, err := sc.recordUploadIntent(rev, sectorRoot, sectorStoragePrice, sectorBandwidthPrice)
   130  	if err != nil {
   131  		return modules.RenterContract{}, crypto.Hash{}, err
   132  	}
   133  
   134  	// send actions
   135  	extendDeadline(he.conn, modules.NegotiateFileContractRevisionTime)
   136  	if err := encoding.WriteObject(he.conn, actions); err != nil {
   137  		return modules.RenterContract{}, crypto.Hash{}, err
   138  	}
   139  
   140  	// Disrupt here before sending the signed revision to the host.
   141  	if he.deps.Disrupt("InterruptUploadBeforeSendingRevision") {
   142  		return modules.RenterContract{}, crypto.Hash{},
   143  			errors.New("InterruptUploadBeforeSendingRevision disrupt")
   144  	}
   145  
   146  	// send revision to host and exchange signatures
   147  	extendDeadline(he.conn, connTimeout)
   148  	signedTxn, err := negotiateRevision(he.conn, rev, contract.SecretKey, he.height)
   149  	if err == modules.ErrStopResponse {
   150  		// if host gracefully closed, close our connection as well; this will
   151  		// cause the next operation to fail
   152  		he.conn.Close()
   153  	} else if err != nil {
   154  		return modules.RenterContract{}, crypto.Hash{}, err
   155  	}
   156  
   157  	// Disrupt here before updating the contract.
   158  	if he.deps.Disrupt("InterruptUploadAfterSendingRevision") {
   159  		return modules.RenterContract{}, crypto.Hash{},
   160  			errors.New("InterruptUploadAfterSendingRevision disrupt")
   161  	}
   162  
   163  	// update contract
   164  	err = sc.commitUpload(walTxn, signedTxn, sectorRoot, sectorStoragePrice, sectorBandwidthPrice)
   165  	if err != nil {
   166  		return modules.RenterContract{}, crypto.Hash{}, err
   167  	}
   168  
   169  	return sc.Metadata(), sectorRoot, nil
   170  }
   171  
   172  // NewEditor initiates the contract revision process with a host, and returns
   173  // an Editor.
   174  func (cs *ContractSet) NewEditor(host modules.HostDBEntry, id types.FileContractID, currentHeight types.BlockHeight, hdb hostDB, cancel <-chan struct{}) (_ *Editor, err error) {
   175  	sc, ok := cs.Acquire(id)
   176  	if !ok {
   177  		return nil, errors.New("invalid contract")
   178  	}
   179  	defer cs.Return(sc)
   180  	contract := sc.header
   181  
   182  	// Increase Successful/Failed interactions accordingly
   183  	defer func() {
   184  		// a revision mismatch is not necessarily the host's fault
   185  		if err != nil && !IsRevisionMismatch(err) {
   186  			hdb.IncrementFailedInteractions(contract.HostPublicKey())
   187  			err = errors.Extend(err, modules.ErrHostFault)
   188  		} else if err == nil {
   189  			hdb.IncrementSuccessfulInteractions(contract.HostPublicKey())
   190  		}
   191  	}()
   192  
   193  	conn, closeChan, err := initiateRevisionLoop(host, sc, modules.RPCReviseContract, cancel, cs.rl)
   194  	if err != nil {
   195  		return nil, errors.AddContext(err, "failed to initiate revision loop")
   196  	}
   197  	// if we succeeded, we can safely discard the unappliedTxns
   198  	for _, txn := range sc.unappliedTxns {
   199  		txn.SignalUpdatesApplied()
   200  	}
   201  	sc.unappliedTxns = nil
   202  
   203  	// the host is now ready to accept revisions
   204  	return &Editor{
   205  		host:        host,
   206  		hdb:         hdb,
   207  		contractID:  id,
   208  		contractSet: cs,
   209  		conn:        conn,
   210  		closeChan:   closeChan,
   211  		deps:        cs.deps,
   212  
   213  		height: currentHeight,
   214  	}, nil
   215  }
   216  
   217  // initiateRevisionLoop initiates either the editor or downloader loop with
   218  // host, depending on which rpc was passed.
   219  func initiateRevisionLoop(host modules.HostDBEntry, contract *SafeContract, rpc types.Specifier, cancel <-chan struct{}, rl *ratelimit.RateLimit) (net.Conn, chan struct{}, error) {
   220  	c, err := (&net.Dialer{
   221  		Cancel:  cancel,
   222  		Timeout: 45 * time.Second, // TODO: Constant
   223  	}).Dial("tcp", string(host.NetAddress))
   224  	if err != nil {
   225  		return nil, nil, err
   226  	}
   227  	conn := ratelimit.NewRLConn(c, rl, cancel)
   228  
   229  	closeChan := make(chan struct{})
   230  	go func() {
   231  		select {
   232  		case <-cancel:
   233  			conn.Close()
   234  		case <-closeChan:
   235  		}
   236  	}()
   237  
   238  	// allot 2 minutes for RPC request + revision exchange
   239  	extendDeadline(conn, modules.NegotiateRecentRevisionTime)
   240  	defer extendDeadline(conn, time.Hour)
   241  	if err := encoding.WriteObject(conn, rpc); err != nil {
   242  		conn.Close()
   243  		close(closeChan)
   244  		return nil, closeChan, errors.New("couldn't initiate RPC: " + err.Error())
   245  	}
   246  	if err := verifyRecentRevision(conn, contract, host.Version); err != nil {
   247  		conn.Close() // TODO: close gracefully if host has entered revision loop
   248  		close(closeChan)
   249  		return nil, closeChan, err
   250  	}
   251  	return conn, closeChan, nil
   252  }