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 }