github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/network/p2p/scoring/subscription_provider.go (about)

     1  package scoring
     2  
     3  import (
     4  	"fmt"
     5  	"time"
     6  
     7  	"github.com/go-playground/validator/v10"
     8  	"github.com/libp2p/go-libp2p/core/peer"
     9  	"github.com/rs/zerolog"
    10  	"go.uber.org/atomic"
    11  
    12  	"github.com/onflow/flow-go/module"
    13  	"github.com/onflow/flow-go/module/component"
    14  	"github.com/onflow/flow-go/module/irrecoverable"
    15  	"github.com/onflow/flow-go/module/metrics"
    16  	"github.com/onflow/flow-go/network"
    17  	"github.com/onflow/flow-go/network/p2p"
    18  	p2pconfig "github.com/onflow/flow-go/network/p2p/config"
    19  	p2plogging "github.com/onflow/flow-go/network/p2p/logging"
    20  	"github.com/onflow/flow-go/network/p2p/scoring/internal"
    21  	"github.com/onflow/flow-go/utils/logging"
    22  )
    23  
    24  // SubscriptionProvider provides a list of topics a peer is subscribed to.
    25  type SubscriptionProvider struct {
    26  	component.Component
    27  	logger              zerolog.Logger
    28  	topicProviderOracle func() p2p.TopicProvider
    29  
    30  	// TODO: we should add an expiry time to this cache and clean up the cache periodically
    31  	// to avoid leakage of stale topics.
    32  	cache SubscriptionCache
    33  
    34  	// idProvider translates the peer ids to flow ids.
    35  	idProvider module.IdentityProvider
    36  
    37  	// allTopics is a list of all topics in the pubsub network that this node is subscribed to.
    38  	allTopicsUpdate         atomic.Bool   // whether a goroutine is already updating the list of topics
    39  	allTopicsUpdateInterval time.Duration // the interval for updating the list of topics in the pubsub network that this node has subscribed to.
    40  }
    41  
    42  type SubscriptionProviderConfig struct {
    43  	Logger                  zerolog.Logger                            `validate:"required"`
    44  	TopicProviderOracle     func() p2p.TopicProvider                  `validate:"required"`
    45  	IdProvider              module.IdentityProvider                   `validate:"required"`
    46  	HeroCacheMetricsFactory metrics.HeroCacheMetricsFactory           `validate:"required"`
    47  	Params                  *p2pconfig.SubscriptionProviderParameters `validate:"required"`
    48  	NetworkingType          network.NetworkingType                    `validate:"required"`
    49  }
    50  
    51  var _ p2p.SubscriptionProvider = (*SubscriptionProvider)(nil)
    52  
    53  func NewSubscriptionProvider(cfg *SubscriptionProviderConfig) (*SubscriptionProvider, error) {
    54  	if err := validator.New().Struct(cfg); err != nil {
    55  		return nil, fmt.Errorf("invalid subscription provider config: %w", err)
    56  	}
    57  
    58  	cacheMetrics := metrics.NewSubscriptionRecordCacheMetricsFactory(cfg.HeroCacheMetricsFactory, cfg.NetworkingType)
    59  	cache := internal.NewSubscriptionRecordCache(cfg.Params.CacheSize, cfg.Logger, cacheMetrics)
    60  
    61  	p := &SubscriptionProvider{
    62  		logger:                  cfg.Logger.With().Str("module", "subscription_provider").Logger(),
    63  		topicProviderOracle:     cfg.TopicProviderOracle,
    64  		allTopicsUpdateInterval: cfg.Params.UpdateInterval,
    65  		idProvider:              cfg.IdProvider,
    66  		cache:                   cache,
    67  	}
    68  
    69  	builder := component.NewComponentManagerBuilder()
    70  	p.Component = builder.AddWorker(
    71  		func(ctx irrecoverable.SignalerContext, ready component.ReadyFunc) {
    72  			ready()
    73  			p.logger.Debug().
    74  				Float64("update_interval_seconds", cfg.Params.UpdateInterval.Seconds()).
    75  				Msg("subscription provider started; starting update topics loop")
    76  			p.updateTopicsLoop(ctx)
    77  
    78  			<-ctx.Done()
    79  			p.logger.Debug().Msg("subscription provider stopped; stopping update topics loop")
    80  		}).Build()
    81  
    82  	return p, nil
    83  }
    84  
    85  func (s *SubscriptionProvider) updateTopicsLoop(ctx irrecoverable.SignalerContext) {
    86  	ticker := time.NewTicker(s.allTopicsUpdateInterval)
    87  	defer ticker.Stop()
    88  
    89  	for {
    90  		select {
    91  		case <-ctx.Done():
    92  			return
    93  		case <-ticker.C:
    94  			if err := s.updateTopics(); err != nil {
    95  				ctx.Throw(fmt.Errorf("update loop failed: %w", err))
    96  				return
    97  			}
    98  		}
    99  	}
   100  }
   101  
   102  // updateTopics returns all the topics in the pubsub network that this node (peer) has subscribed to.
   103  // Note that this method always returns the cached version of the subscribed topics while querying the
   104  // pubsub network for the list of topics in a goroutine. Hence, the first call to this method always returns an empty
   105  // list.
   106  // Args:
   107  // - ctx: the context of the caller.
   108  // Returns:
   109  // - error on failure to update the list of topics. The returned error is irrecoverable and indicates an exception.
   110  func (s *SubscriptionProvider) updateTopics() error {
   111  	if updateInProgress := s.allTopicsUpdate.CompareAndSwap(false, true); updateInProgress {
   112  		// another goroutine is already updating the list of topics
   113  		s.logger.Trace().Msg("skipping topic update; another update is already in progress")
   114  		return nil
   115  	}
   116  
   117  	// start of critical section; protected by updateInProgress atomic flag
   118  	allTopics := s.topicProviderOracle().GetTopics()
   119  	s.logger.Trace().Msgf("all topics updated: %v", allTopics)
   120  
   121  	// increments the update cycle of the cache; so that the previous cache entries are invalidated upon a read or write.
   122  	s.cache.MoveToNextUpdateCycle()
   123  	for _, topic := range allTopics {
   124  		peers := s.topicProviderOracle().ListPeers(topic)
   125  
   126  		for _, p := range peers {
   127  			if _, authorized := s.idProvider.ByPeerID(p); !authorized {
   128  				// peer is not authorized (staked); hence it does not have a valid role in the network; and
   129  				// we skip the topic update for this peer (also avoiding sybil attacks on the cache).
   130  				s.logger.Debug().
   131  					Str("remote_peer_id", p2plogging.PeerId(p)).
   132  					Bool(logging.KeyNetworkingSecurity, true).
   133  					Msg("skipping topic update for unauthorized peer")
   134  				continue
   135  			}
   136  
   137  			updatedTopics, err := s.cache.AddWithInitTopicForPeer(p, topic)
   138  			if err != nil {
   139  				// this is an irrecoverable error; hence, we crash the node.
   140  				return fmt.Errorf("failed to update topics for peer %s: %w", p, err)
   141  			}
   142  			s.logger.Debug().
   143  				Str("remote_peer_id", p2plogging.PeerId(p)).
   144  				Strs("updated_topics", updatedTopics).
   145  				Msg("updated topics for peer")
   146  		}
   147  	}
   148  
   149  	// remove the update flag; end of critical section
   150  	s.allTopicsUpdate.Store(false)
   151  	return nil
   152  }
   153  
   154  // GetSubscribedTopics returns all the subscriptions of a peer within the pubsub network.
   155  func (s *SubscriptionProvider) GetSubscribedTopics(pid peer.ID) []string {
   156  	topics, ok := s.cache.GetSubscribedTopics(pid)
   157  	if !ok {
   158  		s.logger.Trace().Str("peer_id", p2plogging.PeerId(pid)).Msg("no topics found for peer")
   159  		return nil
   160  	}
   161  	return topics
   162  }