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 }