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

     1  package renter
     2  
     3  import (
     4  	"context"
     5  	"strings"
     6  	"time"
     7  
     8  	"github.com/opentracing/opentracing-go"
     9  	"gitlab.com/SkynetLabs/skyd/build"
    10  	"go.sia.tech/siad/modules"
    11  	"go.sia.tech/siad/types"
    12  
    13  	"gitlab.com/NebulousLabs/errors"
    14  )
    15  
    16  const (
    17  	// jobUpdateRegistryPerformanceDecay defines how much the average
    18  	// performance is decayed each time a new datapoint is added. The jobs use
    19  	// an exponential weighted average.
    20  	jobUpdateRegistryPerformanceDecay = 0.9
    21  
    22  	// minUpdateRegistryEntryTypeVersion is the minimum host version that
    23  	// supports updating the registry with a registry entry type and
    24  	// registry update version.
    25  	minUpdateRegistryEntryTypeVersion = "1.5.7"
    26  )
    27  
    28  // errHostOutdatedProof is returned if the host provides a proof that has a
    29  // valid signature but is still invalid due to its revision number.
    30  var errHostOutdatedProof = errors.New("host returned proof with invalid revision number")
    31  
    32  // errHostCheating is returned when a host is known to be in possession of a
    33  // more recent registry value but returned an outdated one.
    34  var errHostCheating = errors.New("host is cheating by returning an outdated entry")
    35  
    36  type (
    37  	// jobUpdateRegistry contains information about a UpdateRegistry query.
    38  	jobUpdateRegistry struct {
    39  		staticSiaPublicKey        types.SiaPublicKey
    40  		staticSignedRegistryValue modules.SignedRegistryValue
    41  		staticSpan                opentracing.Span
    42  
    43  		staticResponseChan chan *jobUpdateRegistryResponse // Channel to send a response down
    44  
    45  		jobGeneric
    46  	}
    47  
    48  	// jobUpdateRegistryQueue is a list of UpdateRegistry jobs that have been
    49  	// assigned to the worker.
    50  	jobUpdateRegistryQueue struct {
    51  		// These variables contain an exponential weighted average of the
    52  		// worker's recent performance for jobUpdateRegistryQueue.
    53  		weightedJobTime float64
    54  
    55  		*jobGenericQueue
    56  	}
    57  
    58  	// jobUpdateRegistryResponse contains the result of a UpdateRegistry query.
    59  	jobUpdateRegistryResponse struct {
    60  		srv          *modules.SignedRegistryValue // only sent on ErrLowerRevNum and ErrSameRevNum
    61  		staticErr    error
    62  		staticWorker *worker
    63  	}
    64  )
    65  
    66  // newJobUpdateRegistry is a helper method to create a new UpdateRegistry job.
    67  func (w *worker) newJobUpdateRegistry(ctx context.Context, span opentracing.Span, responseChan chan *jobUpdateRegistryResponse, spk types.SiaPublicKey, srv modules.SignedRegistryValue) *jobUpdateRegistry {
    68  	jobSpan := opentracing.StartSpan("UpdateRegistryJob", opentracing.ChildOf(span.Context()))
    69  	jobSpan.SetTag("host", w.staticHostPubKeyStr)
    70  	return &jobUpdateRegistry{
    71  		staticSiaPublicKey:        spk,
    72  		staticSignedRegistryValue: srv,
    73  		staticResponseChan:        responseChan,
    74  		staticSpan:                jobSpan,
    75  		jobGeneric:                newJobGeneric(ctx, w.staticJobUpdateRegistryQueue, nil),
    76  	}
    77  }
    78  
    79  // callDiscard will discard a job, sending the provided error.
    80  func (j *jobUpdateRegistry) callDiscard(err error) {
    81  	// Log info and finish span.
    82  	j.staticSpan.LogKV("callDiscard", err)
    83  	j.staticSpan.SetTag("success", false)
    84  	defer j.staticSpan.Finish()
    85  
    86  	w := j.staticQueue.staticWorker()
    87  	errLaunch := w.staticTG.Launch(func() {
    88  		response := &jobUpdateRegistryResponse{
    89  			srv:          nil,
    90  			staticErr:    err,
    91  			staticWorker: j.staticQueue.staticWorker(),
    92  		}
    93  		select {
    94  		case j.staticResponseChan <- response:
    95  		case <-j.staticCtx.Done():
    96  		case <-w.staticTG.StopChan():
    97  		}
    98  	})
    99  	if errLaunch != nil {
   100  		w.staticRenter.staticLog.Debugln("callDiscard: launch failed", errLaunch)
   101  	}
   102  }
   103  
   104  // callExecute will run the UpdateRegistry job.
   105  func (j *jobUpdateRegistry) callExecute() (err error) {
   106  	w := j.staticQueue.staticWorker()
   107  	rid := modules.DeriveRegistryEntryID(j.staticSiaPublicKey, j.staticSignedRegistryValue.Tweak)
   108  
   109  	// Set the execute time
   110  	j.externExecuteTime = time.Now()
   111  
   112  	// Finish job span at the end.
   113  	defer j.staticSpan.Finish()
   114  
   115  	// Capture callExecute in new span.
   116  	span := opentracing.GlobalTracer().StartSpan("callExecute", opentracing.ChildOf(j.staticSpan.Context()))
   117  	defer span.Finish()
   118  
   119  	// Prepare a method to send a response asynchronously.
   120  	sendResponse := func(srv *modules.SignedRegistryValue, err error) {
   121  		errLaunch := w.staticTG.Launch(func() {
   122  			response := &jobUpdateRegistryResponse{
   123  				srv:          srv,
   124  				staticErr:    err,
   125  				staticWorker: j.staticQueue.staticWorker(),
   126  			}
   127  			select {
   128  			case j.staticResponseChan <- response:
   129  			case <-j.staticCtx.Done():
   130  			case <-w.staticTG.StopChan():
   131  			}
   132  		})
   133  		if errLaunch != nil {
   134  			w.staticRenter.staticLog.Debugln("callExececute: launch failed", errLaunch)
   135  		}
   136  	}
   137  
   138  	// update the rv. We ignore ErrSameRevNum and ErrLowerRevNum to not put the
   139  	// host on a cooldown for something that's not necessarily its fault. We
   140  	// might want to add another argument to the job that disables this behavior
   141  	// in the future in case we are certain that a host can't contain those
   142  	// errors.
   143  	var rv modules.SignedRegistryValue
   144  	rv, err = j.managedUpdateRegistry()
   145  	if modules.IsRegistryEntryExistErr(err) {
   146  		// Report the failure if the host can't provide a signed registry entry
   147  		// with the error.
   148  		if errVerify := rv.Verify(j.staticSiaPublicKey.ToPublicKey()); errVerify != nil {
   149  			sendResponse(nil, errVerify)
   150  			j.staticQueue.callReportFailure(errVerify, j.externExecuteTime, time.Now())
   151  			span.LogKV("error", errVerify)
   152  			j.staticSpan.SetTag("success", false)
   153  			return
   154  		}
   155  		// If the entry is valid, check if our suggested can actually not be
   156  		// used to update rv.
   157  		shouldUpdate, shouldUpdateErr := rv.ShouldUpdateWith(&j.staticSignedRegistryValue.RegistryValue, w.staticHostPubKey)
   158  		if shouldUpdate {
   159  			sendResponse(nil, errHostOutdatedProof)
   160  			j.staticQueue.callReportFailure(errHostOutdatedProof, j.externExecuteTime, time.Now())
   161  			span.LogKV("error", errHostOutdatedProof)
   162  			j.staticSpan.SetTag("success", false)
   163  			return
   164  		}
   165  		// If the entry is valid and the revision is also valid, check if we
   166  		// have a higher revision number in the cache than the provided one.
   167  		errCheating := w.managedCheckHostCheating(rid, &rv, true)
   168  		if errCheating != nil {
   169  			sendResponse(nil, errCheating)
   170  			j.staticQueue.callReportFailure(errCheating, j.externExecuteTime, time.Now())
   171  			span.LogKV("error", errCheating)
   172  			j.staticSpan.SetTag("success", false)
   173  			w.staticRegistryCache.Set(rid, rv, true) // adjust the cache
   174  			return
   175  		}
   176  		// If the entry is the same as as the one we want to set, consider this
   177  		// a success. Otherwise return the error.
   178  		if !errors.Contains(shouldUpdateErr, modules.ErrSameRevNum) {
   179  			// Don't call callReportFailure here. The host provided a valid
   180  			// proof and we don't want to punish it. We still return the error
   181  			// though.
   182  			sendResponse(&rv, err)
   183  			j.staticQueue.callReportSuccess()
   184  			span.LogKV("error", err)
   185  			j.staticSpan.SetTag("success", false)
   186  			return
   187  		}
   188  	} else if err != nil {
   189  		sendResponse(nil, err)
   190  		j.staticQueue.callReportFailure(err, j.externExecuteTime, time.Now())
   191  		span.LogKV("error", err)
   192  		j.staticSpan.SetTag("success", false)
   193  		return
   194  	}
   195  
   196  	// Success. We either confirmed the latest revision or updated the host
   197  	// successfully.
   198  	jobTime := time.Since(j.externExecuteTime)
   199  	j.staticSpan.SetTag("success", true)
   200  
   201  	// Update the registry cache.
   202  	w.staticRegistryCache.Set(rid, j.staticSignedRegistryValue, false)
   203  
   204  	// Send the response and report success.
   205  	sendResponse(nil, nil)
   206  	j.staticQueue.callReportSuccess()
   207  
   208  	// Update the performance stats on the queue.
   209  	jq := j.staticQueue.(*jobUpdateRegistryQueue)
   210  	jq.mu.Lock()
   211  	jq.weightedJobTime = expMovingAvgHotStart(jq.weightedJobTime, float64(jobTime), jobUpdateRegistryPerformanceDecay)
   212  	jq.mu.Unlock()
   213  
   214  	return
   215  }
   216  
   217  // callExpectedBandwidth returns the bandwidth that is expected to be consumed
   218  // by the job.
   219  func (j *jobUpdateRegistry) callExpectedBandwidth() (ul, dl uint64) {
   220  	return updateRegistryJobExpectedBandwidth()
   221  }
   222  
   223  // managedUpdateRegistry updates a registry entry on a host. If the error is
   224  // ErrLowerRevNum or ErrSameRevNum, a signed registry value should be returned
   225  // as proof.
   226  func (j *jobUpdateRegistry) managedUpdateRegistry() (modules.SignedRegistryValue, error) {
   227  	w := j.staticQueue.staticWorker()
   228  	// Create the program.
   229  	pt := w.staticPriceTable().staticPriceTable
   230  	pb := modules.NewProgramBuilder(&pt, 0) // 0 duration since UpdateRegistry doesn't depend on it.
   231  	version := modules.ReadRegistryVersionNoType
   232  	if build.VersionCmp(w.staticCache().staticHostVersion, "1.5.5") < 0 {
   233  		pb.V154AddUpdateRegistryInstruction(j.staticSiaPublicKey, j.staticSignedRegistryValue)
   234  	} else if build.VersionCmp(w.staticCache().staticHostVersion, minUpdateRegistryEntryTypeVersion) < 0 {
   235  		pb.V156AddUpdateRegistryInstruction(j.staticSiaPublicKey, j.staticSignedRegistryValue)
   236  	} else {
   237  		version = modules.ReadRegistryVersionWithType
   238  		pb.AddUpdateRegistryInstruction(j.staticSiaPublicKey, j.staticSignedRegistryValue)
   239  	}
   240  	program, programData := pb.Program()
   241  	cost, _, _ := pb.Cost(true)
   242  
   243  	// take into account bandwidth costs
   244  	ulBandwidth, dlBandwidth := j.callExpectedBandwidth()
   245  	bandwidthCost, bandwidthRefund := mdmBandwidthCost(pt, ulBandwidth, dlBandwidth)
   246  	cost = cost.Add(bandwidthCost)
   247  
   248  	// Execute the program and parse the responses.
   249  	var responses []programResponse
   250  	responses, _, err := w.managedExecuteProgram(program, programData, types.FileContractID{}, categoryRegistryWrite, cost, bandwidthRefund)
   251  	if err != nil {
   252  		return modules.SignedRegistryValue{}, errors.AddContext(err, "managedUpdateRegistry: Unable to execute program")
   253  	}
   254  	for _, resp := range responses {
   255  		// If a revision related error was returned, we try to parse the
   256  		// signed registry value from the response.
   257  		err = resp.Error
   258  		// Check for ErrLowerRevNum.
   259  		if err != nil && strings.Contains(err.Error(), modules.ErrLowerRevNum.Error()) {
   260  			err = modules.ErrLowerRevNum
   261  		}
   262  		if err != nil && strings.Contains(err.Error(), modules.ErrSameRevNum.Error()) {
   263  			err = modules.ErrSameRevNum
   264  		}
   265  		if err != nil && strings.Contains(err.Error(), modules.ErrInsufficientWork.Error()) {
   266  			err = modules.ErrInsufficientWork
   267  		}
   268  		if modules.IsRegistryEntryExistErr(err) {
   269  			// Parse the proof.
   270  			_, _, data, revision, sig, entryType, parseErr := parseSignedRegistryValueResponse(resp.Output, false, version)
   271  			rv := modules.NewSignedRegistryValue(j.staticSignedRegistryValue.Tweak, data, revision, sig, entryType)
   272  			return rv, errors.Compose(err, parseErr)
   273  		}
   274  		if err != nil {
   275  			return modules.SignedRegistryValue{}, errors.AddContext(resp.Error, "Output error")
   276  		}
   277  		break
   278  	}
   279  	if len(responses) != len(program) {
   280  		return modules.SignedRegistryValue{}, errors.New("received invalid number of responses but no error")
   281  	}
   282  	return modules.SignedRegistryValue{}, nil
   283  }
   284  
   285  // initJobUpdateRegistryQueue will init the queue for the UpdateRegistry jobs.
   286  func (w *worker) initJobUpdateRegistryQueue() {
   287  	// Sanity check that there is no existing job queue.
   288  	if w.staticJobUpdateRegistryQueue != nil {
   289  		w.staticRenter.staticLog.Critical("incorret call on initJobUpdateRegistryQueue")
   290  		return
   291  	}
   292  
   293  	w.staticJobUpdateRegistryQueue = &jobUpdateRegistryQueue{
   294  		jobGenericQueue: newJobGenericQueue(w),
   295  	}
   296  }
   297  
   298  // UpdateRegistry is a helper method to run a UpdateRegistry job on a worker.
   299  func (w *worker) UpdateRegistry(ctx context.Context, spk types.SiaPublicKey, rv modules.SignedRegistryValue) error {
   300  	updateRegistryRespChan := make(chan *jobUpdateRegistryResponse)
   301  	span := opentracing.GlobalTracer().StartSpan("UpdateRegistry")
   302  	defer span.Finish()
   303  
   304  	jur := w.newJobUpdateRegistry(ctx, span, updateRegistryRespChan, spk, rv)
   305  
   306  	// Add the job to the queue.
   307  	if !w.staticJobUpdateRegistryQueue.callAdd(jur) {
   308  		return errors.New("worker unavailable")
   309  	}
   310  
   311  	// Wait for the response.
   312  	var resp *jobUpdateRegistryResponse
   313  	select {
   314  	case <-ctx.Done():
   315  		return errors.New("UpdateRegistry interrupted")
   316  	case resp = <-updateRegistryRespChan:
   317  	}
   318  	return resp.staticErr
   319  }
   320  
   321  // updateRegistryUpdateJobExpectedBandwidth is a helper function that returns
   322  // the expected bandwidth consumption of a UpdateRegistry job. This helper
   323  // function enables getting at the expected bandwidth without having to
   324  // instantiate a job.
   325  func updateRegistryJobExpectedBandwidth() (ul, dl uint64) {
   326  	return ethernetMTU, ethernetMTU // a single frame each for upload and for download
   327  }