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

     1  package renter
     2  
     3  import (
     4  	"context"
     5  	"encoding/hex"
     6  	"fmt"
     7  	"io"
     8  	"sync"
     9  	"sync/atomic"
    10  	"time"
    11  	"unsafe"
    12  
    13  	"gitlab.com/NebulousLabs/errors"
    14  	"gitlab.com/NebulousLabs/fastrand"
    15  	"gitlab.com/NebulousLabs/siamux"
    16  	"gitlab.com/NebulousLabs/threadgroup"
    17  	"gitlab.com/SkynetLabs/skyd/build"
    18  	"gitlab.com/SkynetLabs/skyd/skymodules"
    19  	"gitlab.com/SkynetLabs/skyd/skymodules/gouging"
    20  	"go.sia.tech/siad/modules"
    21  	"go.sia.tech/siad/types"
    22  )
    23  
    24  // TODO: (f/u) cooldown testing
    25  
    26  var (
    27  	// initialSubscriptionBudget is the initial budget withdrawn for a
    28  	// subscription. After using up 50% of it, the worker refills the budget
    29  	// again to match the initial budget.
    30  	initialSubscriptionBudget = modules.DefaultMaxEphemeralAccountBalance.Div64(10) // 10% of the max
    31  
    32  	// subscriptionCooldownResetInterval is the time after which we consider an
    33  	// ongoing subscription to be healthy enough to reset the consecutive
    34  	// failures.
    35  	subscriptionCooldownResetInterval = build.Select(build.Var{
    36  		Testing:  time.Second * 5,
    37  		Dev:      time.Minute,
    38  		Standard: time.Hour,
    39  	}).(time.Duration)
    40  
    41  	// subscriptionLoopInterval is the interval after which the subscription
    42  	// loop checks for work when it's idle. Idle means the staticWakeChan isn't
    43  	// signaling new work.
    44  	subscriptionLoopInterval = time.Second
    45  
    46  	// stopSubscriptionGracePeriod is the period of time we wait after signaling
    47  	// the host that we want to stop the subscription. All the incoming
    48  	// bandwidth within this period will be accounted for correctly and help us
    49  	// to keep our EA balance expectations in sync with the host's.
    50  	stopSubscriptionGracePeriod = build.Select(build.Var{
    51  		Testing:  time.Second,
    52  		Dev:      3 * time.Second,
    53  		Standard: 3 * time.Second,
    54  	}).(time.Duration)
    55  
    56  	// priceTableRetryInterval is the interval the subscription loop waits for
    57  	// the maintenance to update the price table before checking again.
    58  	priceTableRetryInterval = time.Second
    59  )
    60  
    61  // minSubscriptionVersion is the min version required for a host to support the
    62  // subscription protocol.
    63  const minSubscriptionVersion = "1.5.5"
    64  
    65  // minSubscribeByRIDVersion is the min version required for a host to support
    66  // the subscribe-by-entryid protocol upgrade.
    67  const minSubscribeByRIDVersion = "1.5.8"
    68  
    69  type (
    70  	// subscriptionInfos contains all of the registry subscription related
    71  	// information of a worker.
    72  	subscriptionInfos struct {
    73  		// subscriptions is the map of subscriptions that the worker is supposed
    74  		// to subscribe to. The worker might not be subscribed to these values
    75  		// at all times due to interruptions but it will try to resubscribe as
    76  		// soon as possible.
    77  		// The worker will also try to unsubscribe from all subscriptions that
    78  		// it currently has which it is not supposed to be subscribed to.
    79  		subscriptions map[modules.RegistryEntryID]*subscription
    80  
    81  		// staticWakeChan is a channel to tell the subscription loop that more
    82  		// work is available.
    83  		staticWakeChan chan struct{}
    84  
    85  		// stats
    86  		atomicExtensions uint64
    87  
    88  		// cooldown
    89  		cooldownUntil       time.Time
    90  		consecutiveFailures uint64
    91  
    92  		// staticManager manages the subscriptions across workers.
    93  		staticManager subscriptionManager
    94  
    95  		// utility fields
    96  		mu sync.Mutex
    97  	}
    98  
    99  	// subscription is a struct that provides additional information around a
   100  	// subscription.
   101  	subscription struct {
   102  		staticRequest *skymodules.SubscriptionRequest
   103  
   104  		// subscribe indicates whether the subscription should be kept active.
   105  		// If it is 'true', the worker will try to resubscribe if the session is
   106  		// interrupted.
   107  		subscribe bool
   108  
   109  		// subscribed is closed as soon as the corresponding entry is subscribed
   110  		// to and indicates that the worker is actively listening for updates.
   111  		// It's also closed when a subscription is deleted from the map due to
   112  		// no longer being necessary.
   113  		subscribed chan struct{}
   114  
   115  		// latestRV is kept up-to-date with the latest known value for a
   116  		// subscribed entry and should only be checked if 'subscribed' is
   117  		// closed. It may be 'nil' even though a subscription is active in case
   118  		// the host doesn't know the subscribed entry. If the host does know,
   119  		// the initial value should be set before closing 'subscribed'.
   120  		latestRV *modules.SignedRegistryValue
   121  	}
   122  
   123  	// notificationHandler is a helper type that contains some information
   124  	// relevant to notification pricing and updating price tables.
   125  	notificationHandler struct {
   126  		staticStream io.Closer // stream of the main subscription thread
   127  		staticWorker *worker
   128  
   129  		staticPTUpdateChan  chan struct{} // used by notification thread to signal subscription thread to update the price table
   130  		staticPTUpdatedChan chan struct{} // used by subscription thread to signal notification thread that price table update is done
   131  
   132  		notificationCost types.Currency
   133  		mu               sync.Mutex
   134  	}
   135  )
   136  
   137  // newSubscription creates a new subscription.
   138  func newSubscription(request *skymodules.SubscriptionRequest) *subscription {
   139  	return &subscription{
   140  		staticRequest: request,
   141  		subscribed:    make(chan struct{}),
   142  		subscribe:     true,
   143  	}
   144  }
   145  
   146  // active returns 'true' if the subscription is currently active. That means the
   147  // subscribed channel was closed after a successful subscription request.
   148  func (sub *subscription) active() bool {
   149  	select {
   150  	case <-sub.subscribed:
   151  		return true
   152  	default:
   153  	}
   154  	return false
   155  }
   156  
   157  // managedHandleRegistryEntry is called by managedHandleNotification to handle a
   158  // notification about an updated registry entry.
   159  func (nh *notificationHandler) managedHandleRegistryEntry(stream siamux.Stream, budget *modules.RPCBudget, limit *modules.BudgetLimit) (err error) {
   160  	w := nh.staticWorker
   161  	subInfo := w.staticSubscriptionInfo
   162  
   163  	// Add a limit to the stream.
   164  	err = stream.SetLimit(limit)
   165  	if err != nil {
   166  		return errors.AddContext(err, "failed to set limit on notification stream")
   167  	}
   168  
   169  	// Withdraw notification cost.
   170  	nh.mu.Lock()
   171  	ok := budget.Withdraw(nh.notificationCost)
   172  	nh.mu.Unlock()
   173  	if !ok {
   174  		return errors.New("failed to withdraw notification cost")
   175  	}
   176  
   177  	// Read the update.
   178  	var sneu modules.RPCRegistrySubscriptionNotificationEntryUpdate
   179  	err = modules.RPCRead(stream, &sneu)
   180  	if err != nil {
   181  		return errors.AddContext(err, "failed to read entry update")
   182  	}
   183  
   184  	// Starting here we close the main subscription stream if an error happens
   185  	// because it will be the host trying to cheat us.
   186  	defer func() {
   187  		if err != nil {
   188  			err = errors.Compose(err, nh.staticStream.Close())
   189  		}
   190  	}()
   191  
   192  	// Verify the signature.
   193  	err = sneu.Entry.Verify(sneu.PubKey.ToPublicKey())
   194  	if err != nil {
   195  		return errors.AddContext(err, "failed to verify signature")
   196  	}
   197  
   198  	// Check if the host was trying to cheat us with an outdated entry.
   199  	rid := modules.DeriveRegistryEntryID(sneu.PubKey, sneu.Entry.Tweak)
   200  	err = w.managedCheckHostCheating(rid, &sneu.Entry, false)
   201  	if err != nil {
   202  		return errors.AddContext(err, "host provided outdated entry")
   203  	}
   204  
   205  	// Check if the host sent us an update we are not subsribed to. This might
   206  	// not seem bad, but the host might want to spam us with valid entries that
   207  	// we are not interested in simply to have us pay for bandwidth.
   208  	subInfo.mu.Lock()
   209  	sub, exists := subInfo.subscriptions[modules.DeriveRegistryEntryID(sneu.PubKey, sneu.Entry.Tweak)]
   210  	if !exists {
   211  		subInfo.mu.Unlock()
   212  		return fmt.Errorf("subscription not found")
   213  	}
   214  	var shouldUpdate bool
   215  	if sub.latestRV != nil {
   216  		shouldUpdate, _ = sub.latestRV.ShouldUpdateWith(&sneu.Entry.RegistryValue, w.staticHostPubKey)
   217  		if !shouldUpdate {
   218  			subInfo.mu.Unlock()
   219  			return fmt.Errorf("host sent an outdated revision %v >= %v", sub.latestRV.Revision, sneu.Entry.Revision)
   220  		}
   221  	}
   222  
   223  	// Update the subscription.
   224  	sub.latestRV = &sneu.Entry
   225  	subInfo.mu.Unlock()
   226  	subInfo.staticManager.Notify(w.staticHostPubKey, []skymodules.SubscriptionRequest{
   227  		{
   228  			EntryID: modules.DeriveRegistryEntryID(sneu.PubKey, sneu.Entry.Tweak),
   229  			PubKey:  &sneu.PubKey,
   230  			Tweak:   &sneu.Entry.Tweak,
   231  		},
   232  	}, sneu)
   233  	return nil
   234  }
   235  
   236  // managedHandleSubscriptionSuccess is called by managedHandleNotification to
   237  // handle a subscription success notification.
   238  func (nh *notificationHandler) managedHandleSubscriptionSuccess(stream siamux.Stream, limit *modules.BudgetLimit) error {
   239  	// Tell the subscription thread to update the limits using the new price
   240  	// table.
   241  	select {
   242  	case <-nh.staticWorker.staticTG.StopChan():
   243  		return nil // shutdown
   244  	case nh.staticPTUpdateChan <- struct{}{}:
   245  	}
   246  	// Wait for the subscription thread to be done updating the limits.
   247  	select {
   248  	case <-nh.staticWorker.staticTG.StopChan():
   249  		return nil // shutdown
   250  	case <-nh.staticPTUpdatedChan:
   251  	}
   252  	// Since this stream uses the new costs we set the limit after updating
   253  	// the costs.
   254  	err := stream.SetLimit(limit)
   255  	if err != nil {
   256  		return errors.AddContext(err, "extension 'ok' was received but failed to update limit on stream")
   257  	}
   258  	return nil
   259  }
   260  
   261  // managedHandleNotification handles incoming notifications from the host. It
   262  // verifies notifications and updates the worker's internal state accordingly.
   263  // Since it's registered as a handle which is called in a separate goroutine it
   264  // doesn't return an error.
   265  func (nh *notificationHandler) managedHandleNotification(stream siamux.Stream, budget *modules.RPCBudget, limit *modules.BudgetLimit) {
   266  	w := nh.staticWorker
   267  	// Close the stream when done.
   268  	defer func() {
   269  		if err := stream.Close(); err != nil {
   270  			w.staticRenter.staticLog.Print("managedHandleNotification: failed to close stream: ", err)
   271  		}
   272  	}()
   273  
   274  	// The stream should have a sane deadline.
   275  	err := stream.SetDeadline(time.Now().Add(defaultNewStreamTimeout))
   276  	if err != nil {
   277  		w.staticRenter.staticLog.Print("managedHandleNotification: failed to set deadlien on stream: ", err)
   278  		return
   279  	}
   280  
   281  	// Read the notification type.
   282  	var snt modules.RPCRegistrySubscriptionNotificationType
   283  	err = modules.RPCRead(stream, &snt)
   284  	if err != nil {
   285  		w.staticRenter.staticLog.Print("managedHandleNotification: failed to read notification type: ", err)
   286  		return
   287  	}
   288  
   289  	// Handle the notification.
   290  	switch snt.Type {
   291  	case modules.SubscriptionResponseSubscriptionSuccess:
   292  		if err := nh.managedHandleSubscriptionSuccess(stream, limit); err != nil {
   293  			w.staticRenter.staticLog.Print("managedHAndleSubscriptionSuccess:", err)
   294  		}
   295  		return
   296  	case modules.SubscriptionResponseRegistryValue:
   297  		if err := nh.managedHandleRegistryEntry(stream, budget, limit); err != nil {
   298  			w.staticRenter.staticLog.Print("managedHandleRegistryEntry:", err)
   299  		}
   300  		return
   301  	default:
   302  	}
   303  
   304  	// TODO: (f/u) Punish the host by adding a subscription cooldown.
   305  	w.staticRenter.staticLog.Print("managedHandleNotification: unknown notification type")
   306  	if err := nh.staticStream.Close(); err != nil {
   307  		w.staticRenter.staticLog.Debugln("managedHandleNotification: failed to close subscription:", err)
   308  	}
   309  }
   310  
   311  // managedClearSubscription replaces the channels of all subscriptions of the
   312  // subInfo. This unblocks anyone waiting for a subscription to be established.
   313  func (subInfo *subscriptionInfos) managedClearSubscriptions() {
   314  	subInfo.mu.Lock()
   315  	defer subInfo.mu.Unlock()
   316  	for _, sub := range subInfo.subscriptions {
   317  		// Replace channels.
   318  		select {
   319  		case <-sub.subscribed:
   320  		default:
   321  			close(sub.subscribed)
   322  		}
   323  		sub.subscribed = make(chan struct{})
   324  	}
   325  }
   326  
   327  // managedIncrementCooldown increments the subscription cooldown.
   328  func (subInfo *subscriptionInfos) managedIncrementCooldown() {
   329  	subInfo.mu.Lock()
   330  	defer subInfo.mu.Unlock()
   331  
   332  	// If the last cooldown ended a while ago, we reset the consecutive
   333  	// failures.
   334  	if time.Now().Sub(subInfo.cooldownUntil) > subscriptionCooldownResetInterval {
   335  		subInfo.consecutiveFailures = 0
   336  	}
   337  
   338  	// Increment the cooldown.
   339  	subInfo.cooldownUntil = cooldownUntil(subInfo.consecutiveFailures)
   340  	subInfo.consecutiveFailures++
   341  }
   342  
   343  // managedOnCooldown returns whether the subscription cooldown is active and its
   344  // remaining time.
   345  func (subInfo *subscriptionInfos) managedOnCooldown() (time.Duration, bool) {
   346  	subInfo.mu.Lock()
   347  	defer subInfo.mu.Unlock()
   348  	return time.Until(subInfo.cooldownUntil), time.Now().Before(subInfo.cooldownUntil)
   349  }
   350  
   351  // managedSubscriptionDiff returns the difference between the desired
   352  // subscriptions and the active subscriptions. It also returns a slice of
   353  // channels which need to be closed when the corresponding desired subscription
   354  // was established.
   355  func (subInfo *subscriptionInfos) managedSubscriptionDiff() (toSubscribe, toUnsubscribe []skymodules.SubscriptionRequest, subChans []chan struct{}) {
   356  	subInfo.mu.Lock()
   357  	defer subInfo.mu.Unlock()
   358  	for sid, sub := range subInfo.subscriptions {
   359  		if !sub.subscribe && !sub.active() {
   360  			// Delete the subscription. We are neither supposed to subscribe
   361  			// to it nor are we subscribed to it.
   362  			delete(subInfo.subscriptions, sid)
   363  			// Close its channel.
   364  			close(sub.subscribed)
   365  		} else if sub.active() && !sub.subscribe {
   366  			// Unsubscribe from the entry.
   367  			toUnsubscribe = append(toUnsubscribe, *sub.staticRequest)
   368  		} else if !sub.active() && sub.subscribe {
   369  			// Subscribe and remember the channel to close it later.
   370  			toSubscribe = append(toSubscribe, *sub.staticRequest)
   371  			subChans = append(subChans, sub.subscribed)
   372  		}
   373  	}
   374  	return
   375  }
   376  
   377  // managedExtendSubscriptionPeriod extends the ongoing subscription with a host
   378  // and adjusts the deadline on the stream.
   379  func (w *worker) managedExtendSubscriptionPeriod(stream siamux.Stream, budget *modules.RPCBudget, limit *modules.BudgetLimit, oldDeadline time.Time, oldPT *modules.RPCPriceTable, nh *notificationHandler) (*modules.RPCPriceTable, time.Time, error) {
   380  	subInfo := w.staticSubscriptionInfo
   381  
   382  	// Get a pricetable that is valid until the new deadline.
   383  	newDeadline := oldDeadline.Add(modules.SubscriptionPeriod)
   384  	newPT := w.managedPriceTableForSubscription(time.Until(newDeadline))
   385  	if newPT == nil {
   386  		return nil, time.Time{}, threadgroup.ErrStopped // shutdown
   387  	}
   388  
   389  	// Try extending the subscription.
   390  	err := modules.RPCExtendSubscription(stream, newPT)
   391  	if err != nil {
   392  		return nil, time.Time{}, errors.AddContext(err, "failed to extend subscription")
   393  	}
   394  
   395  	// Wait for "ok".
   396  	select {
   397  	case <-w.staticTG.StopChan():
   398  		return nil, time.Time{}, threadgroup.ErrStopped
   399  	case <-time.After(time.Until(newDeadline)):
   400  		return nil, time.Time{}, errors.New("never received the 'ok' response for extending the subscription")
   401  	case <-nh.staticPTUpdateChan:
   402  	}
   403  
   404  	// Update limit and notification cost.
   405  	limit.UpdateCosts(newPT.DownloadBandwidthCost, newPT.UploadBandwidthCost)
   406  	nh.mu.Lock()
   407  	nh.notificationCost = newPT.SubscriptionNotificationCost
   408  	nh.mu.Unlock()
   409  
   410  	// Tell the notification goroutine that the limit was updated.
   411  	select {
   412  	case <-w.staticTG.StopChan():
   413  		return nil, time.Time{}, threadgroup.ErrStopped
   414  	case <-time.After(time.Until(newDeadline)):
   415  		return nil, time.Time{}, errors.New("notification thread never read the update signal")
   416  	case nh.staticPTUpdatedChan <- struct{}{}:
   417  	}
   418  
   419  	// Count the number of active subscriptions.
   420  	var nSubs uint64
   421  	for _, sub := range subInfo.subscriptions {
   422  		if sub.active() {
   423  			nSubs++
   424  		}
   425  	}
   426  
   427  	// Withdraw from budget.
   428  	if !budget.Withdraw(modules.MDMSubscriptionMemoryCost(newPT, nSubs)) {
   429  		return nil, time.Time{}, errors.New("failed to withdraw subscription extension cost from budget")
   430  	}
   431  
   432  	// Set the stream deadline to the new subscription deadline.
   433  	err = stream.SetDeadline(newDeadline)
   434  	if err != nil {
   435  		return nil, time.Time{}, errors.AddContext(err, "failed to set stream deadlien to subscription deadline")
   436  	}
   437  
   438  	// Increment stats for extending the subscription.
   439  	atomic.AddUint64(&subInfo.atomicExtensions, 1)
   440  	return newPT, newDeadline, nil
   441  }
   442  
   443  // managedRefillSubscription refills the subscription up until expectedBudget.
   444  func (w *worker) managedRefillSubscription(stream siamux.Stream, pt *modules.RPCPriceTable, expectedBudget types.Currency, budget *modules.RPCBudget) (err error) {
   445  	fundAmt := expectedBudget.Sub(budget.Remaining())
   446  
   447  	// Track the withdrawal.
   448  	w.accountSyncMu.Lock()
   449  	defer w.accountSyncMu.Unlock()
   450  	w.staticAccount.managedTrackWithdrawal(fundAmt)
   451  
   452  	// Defer a commit.
   453  	defer func() {
   454  		w.staticAccount.managedCommitWithdrawal(categorySubscription, fundAmt, types.ZeroCurrency, err)
   455  	}()
   456  
   457  	// Fund the subscription.
   458  	err = w.managedFundSubscription(stream, pt, fundAmt)
   459  	if err != nil {
   460  		return errors.AddContext(err, "failed to fund subscription")
   461  	}
   462  
   463  	// Success. Add the funds to the budget and signal to the account
   464  	// that the withdrawal was successful.
   465  	budget.Deposit(fundAmt)
   466  	return nil
   467  }
   468  
   469  // managedSubscriptionCleanup cleans up a subscription by signalling the host
   470  // that we would like to stop the subscription and resetting the subscription
   471  // related fields in the subscription info.
   472  func (w *worker) managedSubscriptionCleanup(stream siamux.Stream, subscriber string) (err error) {
   473  	subInfo := w.staticSubscriptionInfo
   474  
   475  	// Close the stream gracefully.
   476  	err = modules.RPCStopSubscription(stream)
   477  
   478  	// After signalling to shut down the subscription, we wait for a short
   479  	// grace period to allow for incoming streams which were already read by
   480  	// the siamux but did not have the handler called upon them yet. This
   481  	// makes sure that our bandwidth expectations don't drift apart from the
   482  	// host's. We want to always wait for this even upon shutdown to make
   483  	// sure we refund our account correctly.
   484  	time.Sleep(stopSubscriptionGracePeriod)
   485  
   486  	// Close the handler.
   487  	err = errors.Compose(err, w.staticRenter.staticMux.CloseListener(subscriber))
   488  
   489  	// Clear the active subscriptions at the end of this method.
   490  	subInfo.managedClearSubscriptions()
   491  	return err
   492  }
   493  
   494  // managedUnsubscribeFromRVs unsubscribes the worker from multiple ongoing
   495  // subscriptions.
   496  func (w *worker) managedUnsubscribeFromRVs(stream siamux.Stream, toUnsubscribe []skymodules.SubscriptionRequest) error {
   497  	subInfo := w.staticSubscriptionInfo
   498  	// Unsubscribe.
   499  	err := rpcUnsubscribeFromRVs(stream, toUnsubscribe, w.staticCache().staticHostVersion)
   500  	if err != nil {
   501  		return errors.AddContext(err, "failed to unsubscribe from registry values")
   502  	}
   503  	// Reset the subscription's channel to signal that it's no longer
   504  	// active.
   505  	subInfo.mu.Lock()
   506  	defer subInfo.mu.Unlock()
   507  	for _, req := range toUnsubscribe {
   508  		sub, exists := subInfo.subscriptions[req.EntryID]
   509  		if !exists {
   510  			err = errors.New("managedSubscriptionLoop: missing subscription - subscriptions should only be deleted in this thread so this shouldn't be the case")
   511  			build.Critical(err)
   512  			return err
   513  		}
   514  		sub.subscribed = make(chan struct{})
   515  	}
   516  	return nil
   517  }
   518  
   519  // managedCheckHostCheating is a helper method that checks a registry entry
   520  // against the cache to determine whether the host is cheating or not. It also
   521  // updates the cache accordingly.
   522  func (w *worker) managedCheckHostCheating(rid modules.RegistryEntryID, srv *modules.SignedRegistryValue, overwrite bool) error {
   523  	// Check if we have an entry in the cache already.
   524  	ce, exists := w.staticRegistryCache.Get(rid)
   525  	if !exists {
   526  		// If not we update the cache and are done. If srv is nil,
   527  		// that's also fine.
   528  		if srv != nil {
   529  			w.staticRegistryCache.Set(rid, *srv, false)
   530  		}
   531  		return nil
   532  	}
   533  
   534  	// The host has returned a revision in the past. If it returns 'nil' now
   535  	// that's a lie.
   536  	if srv == nil {
   537  		return errHostCheating
   538  	}
   539  
   540  	// If it is cached, check if the host's entry is better than our own.
   541  	better, err := ce.ShouldUpdateWith(&srv.RegistryValue, w.staticHostPubKey)
   542  
   543  	// If it is better, the host isn't cheating. So we update the cache.
   544  	if better && err == nil {
   545  		w.staticRegistryCache.Set(rid, *srv, false)
   546  		return nil
   547  	}
   548  
   549  	// If it is not better, we expect it to be at least equal to our own.
   550  	sameRevNum := errors.Contains(err, modules.ErrSameRevNum)
   551  	if sameRevNum && ce.IsPrimaryEntry(w.staticHostPubKey) == srv.IsPrimaryEntry(w.staticHostPubKey) {
   552  		return nil
   553  	}
   554  
   555  	// The revision the host provided is worse than the one we already know
   556  	// it had. The host is cheating us. We force update the cache to reset
   557  	// our knowledge of what we think is the host's most recent revision if
   558  	// overwrite is specified.
   559  	w.staticRegistryCache.Set(rid, *srv, overwrite)
   560  	return errors.Compose(errHostCheating, err)
   561  }
   562  
   563  // managedSubscribeToRVs subscribes the workers to multiple registry values.
   564  func (w *worker) managedSubscribeToRVs(stream siamux.Stream, toSubscribe []skymodules.SubscriptionRequest, subChans []chan struct{}, budget *modules.RPCBudget, pt *modules.RPCPriceTable) error {
   565  	subInfo := w.staticSubscriptionInfo
   566  	// Subscribe.
   567  	rvs, err := rpcSubscribeToRVs(stream, toSubscribe, w.staticCache().staticHostVersion)
   568  	if err != nil {
   569  		return errors.AddContext(err, "failed to subscribe to registry values")
   570  	}
   571  	// Check that the initial values are not outdated and update the cache.
   572  	for _, rv := range rvs {
   573  		rid := modules.DeriveRegistryEntryID(rv.PubKey, rv.Entry.Tweak)
   574  		errCheating := w.managedCheckHostCheating(rid, &rv.Entry, false)
   575  		if errCheating != nil {
   576  			return errors.AddContext(errCheating, "managedSubscribeToRVs: host is cheating")
   577  		}
   578  	}
   579  	// Withdraw from budget.
   580  	if !budget.Withdraw(modules.MDMSubscribeCost(pt, uint64(len(rvs)), uint64(len(toSubscribe)))) {
   581  		return errors.New("failed to withdraw subscription payment from budget")
   582  	}
   583  	// Update the subscriptions with the received values.
   584  	subInfo.mu.Lock()
   585  	for _, rv := range rvs {
   586  		subInfo.subscriptions[modules.DeriveRegistryEntryID(rv.PubKey, rv.Entry.Tweak)].latestRV = &rv.Entry
   587  	}
   588  	// Close the channels to signal that the subscription is done.
   589  	for _, c := range subChans {
   590  		close(c)
   591  	}
   592  	subInfo.mu.Unlock()
   593  	// Tell the subscription manager.
   594  	subInfo.staticManager.Notify(w.staticHostPubKey, toSubscribe, rvs...)
   595  	return nil
   596  }
   597  
   598  // managedSubscriptionLoop handles an existing subscription session. It will add
   599  // subscriptions, remove subscriptions, fund the subscription and extend it
   600  // indefinitely.
   601  func (w *worker) managedSubscriptionLoop(stream siamux.Stream, pt *modules.RPCPriceTable, deadline time.Time, budget *modules.RPCBudget, expectedBudget types.Currency, subscriber string) (err error) {
   602  	if w.staticRenter.staticDeps.Disrupt("InterruptSubscriptionLoop") {
   603  		return errors.New("interrupted")
   604  	}
   605  
   606  	// Set the bandwidth limiter on the stream.
   607  	limit := modules.NewBudgetLimit(budget, pt.DownloadBandwidthCost, pt.UploadBandwidthCost)
   608  	err = stream.SetLimit(limit)
   609  	if err != nil {
   610  		return errors.AddContext(err, "failed to set bandwidth limiter on the stream")
   611  	}
   612  
   613  	// Register the handler. This can happen after beginning the subscription
   614  	// since we are not expecting any notifications yet.
   615  	nh := &notificationHandler{
   616  		staticStream:        stream,
   617  		staticWorker:        w,
   618  		staticPTUpdateChan:  make(chan struct{}),
   619  		staticPTUpdatedChan: make(chan struct{}),
   620  		notificationCost:    pt.SubscriptionNotificationCost,
   621  	}
   622  	err = w.staticRenter.staticMux.NewListenerSerial(subscriber, func(stream siamux.Stream) {
   623  		nh.managedHandleNotification(stream, budget, limit)
   624  	})
   625  	if err != nil {
   626  		return errors.AddContext(err, "failed to register listener")
   627  	}
   628  
   629  	// Register some cleanup.
   630  	defer func() {
   631  		err = errors.Compose(err, w.managedSubscriptionCleanup(stream, subscriber))
   632  	}()
   633  
   634  	// Set the stream deadline to the subscription deadline.
   635  	err = stream.SetDeadline(deadline)
   636  	if err != nil {
   637  		return errors.AddContext(err, "failed to set stream deadlien to subscription deadline")
   638  	}
   639  
   640  	for {
   641  		// If the budget is half empty, fund it.
   642  		if budget.Remaining().Cmp(expectedBudget.Div64(2)) < 0 {
   643  			err = w.managedRefillSubscription(stream, pt, expectedBudget, budget)
   644  			if err != nil {
   645  				return err
   646  			}
   647  		}
   648  
   649  		// If the subscription period is halfway over, extend it.
   650  		if time.Until(deadline) < modules.SubscriptionPeriod/2 {
   651  			pt, deadline, err = w.managedExtendSubscriptionPeriod(stream, budget, limit, deadline, pt, nh)
   652  			if err != nil {
   653  				return err
   654  			}
   655  		}
   656  
   657  		// Create a diff between the active subscriptions and the desired
   658  		// ones.
   659  		subInfo := w.staticSubscriptionInfo
   660  		toSubscribe, toUnsubscribe, subChans := subInfo.managedSubscriptionDiff()
   661  
   662  		// Unsubscribe from unnecessary subscriptions.
   663  		if len(toUnsubscribe) > 0 {
   664  			err = w.managedUnsubscribeFromRVs(stream, toUnsubscribe)
   665  			if err != nil {
   666  				return err
   667  			}
   668  		}
   669  
   670  		// Subscribe to any missing values.
   671  		if len(toSubscribe) > 0 {
   672  			err = w.managedSubscribeToRVs(stream, toSubscribe, subChans, budget, pt)
   673  			if err != nil {
   674  				return err
   675  			}
   676  		}
   677  
   678  		// Wait until some time passed or until there is new work.
   679  		ctx, cancel := context.WithTimeout(context.Background(), subscriptionLoopInterval)
   680  		select {
   681  		case <-w.staticTG.StopChan():
   682  			cancel()
   683  			return threadgroup.ErrStopped // shutdown
   684  		case <-ctx.Done():
   685  			// continue right away since the timer is drained.
   686  			cancel()
   687  			continue
   688  		case <-subInfo.staticWakeChan:
   689  			cancel()
   690  		}
   691  	}
   692  }
   693  
   694  // managedPriceTableForSubscription will fetch a price table that is valid for
   695  // the provided duration. If the current price table of the worker isn't valid
   696  // for that long, it will change its update time to trigger an update.
   697  func (w *worker) managedPriceTableForSubscription(duration time.Duration) *modules.RPCPriceTable {
   698  	for {
   699  		// Check for shutdown.
   700  		select {
   701  		case _ = <-w.staticTG.StopChan():
   702  			w.staticRenter.staticLog.Debugln("managedPriceTableForSubscription: abort due to shutdown")
   703  			return nil // shutdown
   704  		default:
   705  		}
   706  
   707  		// Get most recent price table.
   708  		pt := w.staticPriceTable()
   709  
   710  		// Check for gouging.
   711  		allowance := w.staticRenter.staticHostContractor.Allowance()
   712  		if err := gouging.CheckSubscription(allowance, pt.staticPriceTable); err != nil {
   713  			w.staticRenter.staticLog.Debugf("WARN: worker %v failed subscription gouging: %v", w.staticHostPubKeyStr, err)
   714  			sleepTime := time.Until(pt.staticUpdateTime)
   715  			if sleepTime < priceTableRetryInterval {
   716  				sleepTime = priceTableRetryInterval
   717  			}
   718  			// Wait a bit before checking again.
   719  			select {
   720  			case <-w.staticRenter.tg.StopChan():
   721  				return nil // shutdown
   722  			case <-time.After(sleepTime):
   723  				continue // check next price table
   724  			}
   725  		}
   726  
   727  		// If the price table is valid, return it.
   728  		if pt.staticValidFor(duration) {
   729  			return &pt.staticPriceTable
   730  		}
   731  
   732  		// NOTE: The price table is not valid for the subsription. This
   733  		// theoretically should not happen a lot.
   734  		// The reason why it shouldn't happen often is that a price table is
   735  		// valid for rpcPriceGuaranteePeriod. That period is 10 minutes in
   736  		// production and gets renewed every 5 minutes. So we should always have
   737  		// a price table that is at least valid for another 5 minutes. The
   738  		// SubscriptionPeriod also happens to be 5 minutes but we renew 2.5
   739  		// minutes before it ends.
   740  		w.staticRenter.staticLog.Debugf("managedPriceTableForSubscription: pt not ready yet for worker %v", w.staticHostPubKeyStr)
   741  
   742  		// Trigger an update by setting the update time to now and calling
   743  		// 'staticWake'.
   744  		newPT := *pt
   745  		newPT.staticUpdateTime = time.Time{}
   746  		oldPT := (*workerPriceTable)(atomic.SwapPointer(&w.atomicPriceTable, unsafe.Pointer(&newPT)))
   747  		w.staticWake()
   748  
   749  		// The old table's UID should be the same. Otherwise we just swapped out
   750  		// a new table and need to try again. This condition can be false when
   751  		// pricetable got updated between now and when we fetched it at the
   752  		// beginning of this iteration.
   753  		if oldPT.staticPriceTable.UID != pt.staticPriceTable.UID {
   754  			w.staticSetPriceTable(oldPT) // set back to the old one
   755  			continue
   756  		}
   757  
   758  		// Wait a bit before checking again.
   759  		select {
   760  		case _ = <-w.staticTG.StopChan():
   761  			w.staticRenter.staticLog.Debugln("managedPriceTableForSubscription: abort due to shutdown")
   762  			return nil // shutdown
   763  		case <-time.After(priceTableRetryInterval):
   764  		}
   765  	}
   766  }
   767  
   768  // managedBeginSubscription begins a subscription on a new stream and returns
   769  // it.
   770  func (w *worker) managedBeginSubscription(initialBudget types.Currency, fundAcc modules.AccountID, subscriber types.Specifier) (_ siamux.Stream, err error) {
   771  	stream, err := w.staticNewStream()
   772  	if err != nil {
   773  		return nil, errors.AddContext(err, "managedBeginSubscription: failed to create stream")
   774  	}
   775  	defer func() {
   776  		if err != nil {
   777  			err = errors.Compose(err, stream.Close())
   778  		}
   779  	}()
   780  	bh, _ := w.managedSyncInfo()
   781  	return stream, modules.RPCBeginSubscription(stream, w.staticHostPubKey, &w.staticPriceTable().staticPriceTable, w.staticAccount.staticID, w.staticAccount.staticSecretKey, initialBudget, bh, subscriber)
   782  }
   783  
   784  // managedFundSubscription pays the host to increase the subscription budget.
   785  func (w *worker) managedFundSubscription(stream siamux.Stream, pt *modules.RPCPriceTable, fundAmt types.Currency) error {
   786  	return modules.RPCFundSubscription(stream, w.staticHostPubKey, w.staticAccount.staticID, w.staticAccount.staticSecretKey, pt.HostBlockHeight, fundAmt)
   787  }
   788  
   789  // threadedSubscriptionLoop is the main subscription loop. It opens a
   790  // subscription with the host and then calls managedSubscriptionLoop to keep the
   791  // subscription alive. If the subscription dies, threadedSubscriptionLoop will
   792  // start it again.
   793  func (w *worker) threadedSubscriptionLoop() {
   794  	if err := w.staticTG.Add(); err != nil {
   795  		return
   796  	}
   797  	defer w.staticTG.Done()
   798  
   799  	// No need to run loop if the host doesn't support it.
   800  	if build.VersionCmp(w.staticCache().staticHostVersion, minSubscriptionVersion) < 0 {
   801  		return
   802  	}
   803  
   804  	// Disable loop if necessary.
   805  	if w.staticRenter.staticDeps.Disrupt("DisableSubscriptionLoop") {
   806  		return
   807  	}
   808  
   809  	// Convenience var.
   810  	subInfo := w.staticSubscriptionInfo
   811  
   812  	for {
   813  		// Clear potential subscriptions before establishing a new loop.
   814  		subInfo.managedClearSubscriptions()
   815  
   816  		// Check for shutdown
   817  		select {
   818  		case <-w.staticTG.StopChan():
   819  			return // shutdown
   820  		default:
   821  		}
   822  
   823  		// Nothing to do if there are no subscriptions.
   824  		subInfo.mu.Lock()
   825  		nSubs := len(subInfo.subscriptions)
   826  		subInfo.mu.Unlock()
   827  		if nSubs == 0 {
   828  			select {
   829  			case <-subInfo.staticWakeChan:
   830  				// Wait for work
   831  			case <-w.staticTG.StopChan():
   832  				return // shutdown
   833  			}
   834  			// Check if we got work after waking up.
   835  			subInfo.mu.Lock()
   836  			nSubs = len(subInfo.subscriptions)
   837  			subInfo.mu.Unlock()
   838  			if nSubs == 0 {
   839  				continue
   840  			}
   841  		}
   842  
   843  		// If the worker is on a cooldown, block until it is over before trying
   844  		// to establish a new subscriptoin session.
   845  		if w.managedOnMaintenanceCooldown() {
   846  			cooldownTime := w.callStatus().MaintenanceCoolDownTime
   847  			w.staticTG.Sleep(cooldownTime)
   848  			continue // try again
   849  		}
   850  		if cooldownTime, onCooldown := subInfo.managedOnCooldown(); onCooldown {
   851  			w.staticTG.Sleep(cooldownTime)
   852  			continue // try again
   853  		}
   854  
   855  		// Get a valid price table.
   856  		pt := w.managedPriceTableForSubscription(modules.SubscriptionPeriod)
   857  		if pt == nil {
   858  			return // shutdown
   859  		}
   860  
   861  		// Compute the initial deadline.
   862  		deadline := time.Now().Add(modules.SubscriptionPeriod)
   863  
   864  		// Set the initial budget.
   865  		initialBudget := initialSubscriptionBudget
   866  		budget := modules.NewBudget(initialBudget)
   867  
   868  		// Track the withdrawal.
   869  		w.accountSyncMu.Lock()
   870  		w.staticAccount.managedTrackWithdrawal(initialBudget)
   871  
   872  		// Prepare a unique handler for the host to subscribe to.
   873  		var subscriber types.Specifier
   874  		fastrand.Read(subscriber[:])
   875  		subscriberStr := hex.EncodeToString(subscriber[:])
   876  
   877  		// Begin the subscription session.
   878  		stream, err := w.managedBeginSubscription(initialBudget, w.staticAccount.staticID, subscriber)
   879  		if err != nil {
   880  			// Mark withdrawal as failed.
   881  			w.staticAccount.managedCommitWithdrawal(categorySubscription, initialBudget, types.ZeroCurrency, err)
   882  			w.accountSyncMu.Unlock()
   883  
   884  			// Log error and increment cooldown.
   885  			w.staticRenter.staticLog.Printf("Worker %v: failed to begin subscription: %v", w.staticHostPubKeyStr, err)
   886  			subInfo.managedIncrementCooldown()
   887  			continue
   888  		}
   889  
   890  		// Mark withdrawal as successful.
   891  		w.staticAccount.managedCommitWithdrawal(categorySubscription, initialBudget, types.ZeroCurrency, nil)
   892  		w.accountSyncMu.Unlock()
   893  
   894  		// Run the subscription. The error is  checked after closing the handler
   895  		// and the refund.
   896  		errSubscription := w.managedSubscriptionLoop(stream, pt, deadline, budget, initialBudget, subscriberStr)
   897  
   898  		// Commit the refund.
   899  		w.staticAccount.managedCommitRefund(categorySubscription, budget.Remaining())
   900  
   901  		// Handle the error.
   902  		w.staticHandleError(errSubscription)
   903  
   904  		// Check the error.
   905  		if errors.Contains(errSubscription, threadgroup.ErrStopped) {
   906  			return // shutdown
   907  		}
   908  		if errSubscription != nil {
   909  			w.staticRenter.staticLog.Printf("Worker %v: subscription got interrupted: %v", w.staticHostPubKeyStr, errSubscription)
   910  			subInfo.managedIncrementCooldown()
   911  			continue
   912  		}
   913  	}
   914  }
   915  
   916  // UpdateSubscriptions updates the entries the worker is subscribed to.
   917  func (w *worker) UpdateSubscriptions(requests ...skymodules.SubscriptionRequest) {
   918  	subInfo := w.staticSubscriptionInfo
   919  	subInfo.mu.Lock()
   920  	defer subInfo.mu.Unlock()
   921  
   922  	// Create a map of the values we should subscribe to.
   923  	requestMap := make(map[modules.RegistryEntryID]*skymodules.SubscriptionRequest)
   924  	for i := range requests {
   925  		req := requests[i]
   926  		requestMap[req.EntryID] = &req
   927  	}
   928  
   929  	// Check the subscriptions we already have.
   930  	for sid, sub := range subInfo.subscriptions {
   931  		// Change the subscribe field of existing subscriptions depending on
   932  		// whether we want it or not.
   933  		_, wanted := requestMap[sid]
   934  		sub.subscribe = wanted
   935  
   936  		// Remove the sid from the requestMap since we handled it.
   937  		delete(requestMap, sid)
   938  	}
   939  
   940  	// For the remaining requests we create new entries.
   941  	for sid, req := range requestMap {
   942  		sub := newSubscription(req)
   943  		subInfo.subscriptions[sid] = sub
   944  	}
   945  
   946  	// Notify the worker.
   947  	select {
   948  	case subInfo.staticWakeChan <- struct{}{}:
   949  	default:
   950  	}
   951  }
   952  
   953  // rpcUnsubscribeToRVs executes the unsubscribe-protocol on the given stream.
   954  func rpcUnsubscribeFromRVs(stream siamux.Stream, toUnsubscribe []skymodules.SubscriptionRequest, hostVersion string) error {
   955  	if build.VersionCmp(hostVersion, minSubscribeByRIDVersion) < 0 {
   956  		requests := make([]modules.RPCRegistrySubscriptionRequest, 0, len(toUnsubscribe))
   957  		for _, ts := range toUnsubscribe {
   958  			if !ts.HasKeyAndTweak() {
   959  				continue // can't subscribe by entry id on this host
   960  			}
   961  			requests = append(requests, modules.RPCRegistrySubscriptionRequest{
   962  				PubKey: *ts.PubKey,
   963  				Tweak:  *ts.Tweak,
   964  			})
   965  		}
   966  		return modules.RPCUnsubscribeFromRVs(stream, requests)
   967  	}
   968  	requests := make([]modules.RPCRegistrySubscriptionByRIDRequest, 0, len(toUnsubscribe))
   969  	for _, ts := range toUnsubscribe {
   970  		requests = append(requests, modules.RPCRegistrySubscriptionByRIDRequest{
   971  			EntryID: ts.EntryID,
   972  		})
   973  	}
   974  	return modules.RPCUnsubscribeFromRVsByRID(stream, requests)
   975  }
   976  
   977  // rpcSubscribeToRVs executes the subscribe-protocol on the given stream.
   978  func rpcSubscribeToRVs(stream siamux.Stream, toSubscribe []skymodules.SubscriptionRequest, hostVersion string) ([]modules.RPCRegistrySubscriptionNotificationEntryUpdate, error) {
   979  	if build.VersionCmp(hostVersion, minSubscribeByRIDVersion) < 0 {
   980  		requests := make([]modules.RPCRegistrySubscriptionRequest, 0, len(toSubscribe))
   981  		for _, ts := range toSubscribe {
   982  			if !ts.HasKeyAndTweak() {
   983  				continue // can't subscribe by entry id on this host
   984  			}
   985  			requests = append(requests, modules.RPCRegistrySubscriptionRequest{
   986  				PubKey: *ts.PubKey,
   987  				Tweak:  *ts.Tweak,
   988  			})
   989  		}
   990  		return modules.RPCSubscribeToRVs(stream, requests)
   991  	}
   992  	requests := make([]modules.RPCRegistrySubscriptionByRIDRequest, 0, len(toSubscribe))
   993  	for _, ts := range toSubscribe {
   994  		requests = append(requests, modules.RPCRegistrySubscriptionByRIDRequest{
   995  			EntryID: ts.EntryID,
   996  		})
   997  	}
   998  	return modules.RPCSubscribeToRVsByRID(stream, requests)
   999  }