github.com/prysmaticlabs/prysm@v1.4.4/beacon-chain/sync/subscriber.go (about) 1 package sync 2 3 import ( 4 "context" 5 "fmt" 6 "reflect" 7 "runtime/debug" 8 "strings" 9 "time" 10 11 "github.com/libp2p/go-libp2p-core/peer" 12 pubsub "github.com/libp2p/go-libp2p-pubsub" 13 types "github.com/prysmaticlabs/eth2-types" 14 "github.com/prysmaticlabs/prysm/beacon-chain/p2p" 15 "github.com/prysmaticlabs/prysm/cmd/beacon-chain/flags" 16 pb "github.com/prysmaticlabs/prysm/proto/eth/v1alpha1" 17 "github.com/prysmaticlabs/prysm/shared/messagehandler" 18 "github.com/prysmaticlabs/prysm/shared/p2putils" 19 "github.com/prysmaticlabs/prysm/shared/params" 20 "github.com/prysmaticlabs/prysm/shared/sliceutil" 21 "github.com/prysmaticlabs/prysm/shared/slotutil" 22 "github.com/prysmaticlabs/prysm/shared/traceutil" 23 "go.opencensus.io/trace" 24 "google.golang.org/protobuf/proto" 25 ) 26 27 const pubsubMessageTimeout = 30 * time.Second 28 29 // subHandler represents handler for a given subscription. 30 type subHandler func(context.Context, proto.Message) error 31 32 // noopValidator is a no-op that only decodes the message, but does not check its contents. 33 func (s *Service) noopValidator(_ context.Context, _ peer.ID, msg *pubsub.Message) pubsub.ValidationResult { 34 m, err := s.decodePubsubMessage(msg) 35 if err != nil { 36 log.WithError(err).Debug("Could not decode message") 37 return pubsub.ValidationReject 38 } 39 msg.ValidatorData = m 40 return pubsub.ValidationAccept 41 } 42 43 // Register PubSub subscribers 44 func (s *Service) registerSubscribers() { 45 s.subscribe( 46 p2p.BlockSubnetTopicFormat, 47 s.validateBeaconBlockPubSub, 48 s.beaconBlockSubscriber, 49 ) 50 s.subscribe( 51 p2p.AggregateAndProofSubnetTopicFormat, 52 s.validateAggregateAndProof, 53 s.beaconAggregateProofSubscriber, 54 ) 55 s.subscribe( 56 p2p.ExitSubnetTopicFormat, 57 s.validateVoluntaryExit, 58 s.voluntaryExitSubscriber, 59 ) 60 s.subscribe( 61 p2p.ProposerSlashingSubnetTopicFormat, 62 s.validateProposerSlashing, 63 s.proposerSlashingSubscriber, 64 ) 65 s.subscribe( 66 p2p.AttesterSlashingSubnetTopicFormat, 67 s.validateAttesterSlashing, 68 s.attesterSlashingSubscriber, 69 ) 70 if flags.Get().SubscribeToAllSubnets { 71 s.subscribeStaticWithSubnets( 72 "/eth2/%x/beacon_attestation_%d", 73 s.validateCommitteeIndexBeaconAttestation, /* validator */ 74 s.committeeIndexBeaconAttestationSubscriber, /* message handler */ 75 ) 76 } else { 77 s.subscribeDynamicWithSubnets( 78 "/eth2/%x/beacon_attestation_%d", 79 s.validateCommitteeIndexBeaconAttestation, /* validator */ 80 s.committeeIndexBeaconAttestationSubscriber, /* message handler */ 81 ) 82 } 83 } 84 85 // subscribe to a given topic with a given validator and subscription handler. 86 // The base protobuf message is used to initialize new messages for decoding. 87 func (s *Service) subscribe(topic string, validator pubsub.ValidatorEx, handle subHandler) *pubsub.Subscription { 88 base := p2p.GossipTopicMappings[topic] 89 if base == nil { 90 panic(fmt.Sprintf("%s is not mapped to any message in GossipTopicMappings", topic)) 91 } 92 return s.subscribeWithBase(s.addDigestToTopic(topic), validator, handle) 93 } 94 95 func (s *Service) subscribeWithBase(topic string, validator pubsub.ValidatorEx, handle subHandler) *pubsub.Subscription { 96 topic += s.cfg.P2P.Encoding().ProtocolSuffix() 97 log := log.WithField("topic", topic) 98 99 if err := s.cfg.P2P.PubSub().RegisterTopicValidator(s.wrapAndReportValidation(topic, validator)); err != nil { 100 log.WithError(err).Error("Could not register validator for topic") 101 return nil 102 } 103 104 sub, err := s.cfg.P2P.SubscribeToTopic(topic) 105 if err != nil { 106 // Any error subscribing to a PubSub topic would be the result of a misconfiguration of 107 // libp2p PubSub library or a subscription request to a topic that fails to match the topic 108 // subscription filter. 109 log.WithError(err).Error("Could not subscribe topic") 110 return nil 111 } 112 113 // Pipeline decodes the incoming subscription data, runs the validation, and handles the 114 // message. 115 pipeline := func(msg *pubsub.Message) { 116 ctx, cancel := context.WithTimeout(s.ctx, pubsubMessageTimeout) 117 defer cancel() 118 ctx, span := trace.StartSpan(ctx, "sync.pubsub") 119 defer span.End() 120 121 defer func() { 122 if r := recover(); r != nil { 123 traceutil.AnnotateError(span, fmt.Errorf("panic occurred: %v", r)) 124 log.WithField("error", r).Error("Panic occurred") 125 debug.PrintStack() 126 } 127 }() 128 129 span.AddAttributes(trace.StringAttribute("topic", topic)) 130 131 if msg.ValidatorData == nil { 132 log.Debug("Received nil message on pubsub") 133 messageFailedProcessingCounter.WithLabelValues(topic).Inc() 134 return 135 } 136 137 if err := handle(ctx, msg.ValidatorData.(proto.Message)); err != nil { 138 traceutil.AnnotateError(span, err) 139 log.WithError(err).Debug("Could not handle p2p pubsub") 140 messageFailedProcessingCounter.WithLabelValues(topic).Inc() 141 return 142 } 143 } 144 145 // The main message loop for receiving incoming messages from this subscription. 146 messageLoop := func() { 147 for { 148 msg, err := sub.Next(s.ctx) 149 if err != nil { 150 // This should only happen when the context is cancelled or subscription is cancelled. 151 if err != pubsub.ErrSubscriptionCancelled { // Only log a warning on unexpected errors. 152 log.WithError(err).Warn("Subscription next failed") 153 } 154 // Cancel subscription in the event of an error, as we are 155 // now exiting topic event loop. 156 sub.Cancel() 157 return 158 } 159 160 if msg.ReceivedFrom == s.cfg.P2P.PeerID() { 161 continue 162 } 163 164 go pipeline(msg) 165 } 166 } 167 168 go messageLoop() 169 return sub 170 } 171 172 // Wrap the pubsub validator with a metric monitoring function. This function increments the 173 // appropriate counter if the particular message fails to validate. 174 func (s *Service) wrapAndReportValidation(topic string, v pubsub.ValidatorEx) (string, pubsub.ValidatorEx) { 175 return topic, func(ctx context.Context, pid peer.ID, msg *pubsub.Message) (res pubsub.ValidationResult) { 176 defer messagehandler.HandlePanic(ctx, msg) 177 res = pubsub.ValidationIgnore // Default: ignore any message that panics. 178 ctx, cancel := context.WithTimeout(ctx, pubsubMessageTimeout) 179 defer cancel() 180 messageReceivedCounter.WithLabelValues(topic).Inc() 181 if msg.Topic == nil { 182 messageFailedValidationCounter.WithLabelValues(topic).Inc() 183 return pubsub.ValidationReject 184 } 185 // Ignore any messages received before chainstart. 186 if s.chainStarted.IsNotSet() { 187 messageFailedValidationCounter.WithLabelValues(topic).Inc() 188 return pubsub.ValidationIgnore 189 } 190 b := v(ctx, pid, msg) 191 if b == pubsub.ValidationReject { 192 messageFailedValidationCounter.WithLabelValues(topic).Inc() 193 } 194 return b 195 } 196 } 197 198 // subscribe to a static subnet with the given topic and index.A given validator and subscription handler is 199 // used to handle messages from the subnet. The base protobuf message is used to initialize new messages for decoding. 200 func (s *Service) subscribeStaticWithSubnets(topic string, validator pubsub.ValidatorEx, handle subHandler) { 201 base := p2p.GossipTopicMappings[topic] 202 if base == nil { 203 panic(fmt.Sprintf("%s is not mapped to any message in GossipTopicMappings", topic)) 204 } 205 for i := uint64(0); i < params.BeaconNetworkConfig().AttestationSubnetCount; i++ { 206 s.subscribeWithBase(s.addDigestAndIndexToTopic(topic, i), validator, handle) 207 } 208 genesis := s.cfg.Chain.GenesisTime() 209 ticker := slotutil.NewSlotTicker(genesis, params.BeaconConfig().SecondsPerSlot) 210 211 go func() { 212 for { 213 select { 214 case <-s.ctx.Done(): 215 ticker.Done() 216 return 217 case <-ticker.C(): 218 if s.chainStarted.IsSet() && s.cfg.InitialSync.Syncing() { 219 continue 220 } 221 // Check every slot that there are enough peers 222 for i := uint64(0); i < params.BeaconNetworkConfig().AttestationSubnetCount; i++ { 223 if !s.validPeersExist(s.addDigestAndIndexToTopic(topic, i)) { 224 log.Debugf("No peers found subscribed to attestation gossip subnet with "+ 225 "committee index %d. Searching network for peers subscribed to the subnet.", i) 226 _, err := s.cfg.P2P.FindPeersWithSubnet( 227 s.ctx, 228 s.addDigestAndIndexToTopic(topic, i), 229 i, 230 params.BeaconNetworkConfig().MinimumPeersInSubnet, 231 ) 232 if err != nil { 233 log.WithError(err).Debug("Could not search for peers") 234 return 235 } 236 } 237 } 238 } 239 } 240 }() 241 } 242 243 // subscribe to a dynamically changing list of subnets. This method expects a fmt compatible 244 // string for the topic name and the list of subnets for subscribed topics that should be 245 // maintained. 246 func (s *Service) subscribeDynamicWithSubnets( 247 topicFormat string, 248 validate pubsub.ValidatorEx, 249 handle subHandler, 250 ) { 251 base := p2p.GossipTopicMappings[topicFormat] 252 if base == nil { 253 log.Fatalf("%s is not mapped to any message in GossipTopicMappings", topicFormat) 254 } 255 digest, err := s.forkDigest() 256 if err != nil { 257 log.WithError(err).Fatal("Could not compute fork digest") 258 } 259 subscriptions := make(map[uint64]*pubsub.Subscription, params.BeaconConfig().MaxCommitteesPerSlot) 260 genesis := s.cfg.Chain.GenesisTime() 261 ticker := slotutil.NewSlotTicker(genesis, params.BeaconConfig().SecondsPerSlot) 262 263 go func() { 264 for { 265 select { 266 case <-s.ctx.Done(): 267 ticker.Done() 268 return 269 case currentSlot := <-ticker.C(): 270 if s.chainStarted.IsSet() && s.cfg.InitialSync.Syncing() { 271 continue 272 } 273 wantedSubs := s.retrievePersistentSubs(currentSlot) 274 // Resize as appropriate. 275 s.reValidateSubscriptions(subscriptions, wantedSubs, topicFormat, digest) 276 277 // subscribe desired aggregator subnets. 278 for _, idx := range wantedSubs { 279 s.subscribeAggregatorSubnet(subscriptions, idx, digest, validate, handle) 280 } 281 // find desired subs for attesters 282 attesterSubs := s.attesterSubnetIndices(currentSlot) 283 for _, idx := range attesterSubs { 284 s.lookupAttesterSubnets(digest, idx) 285 } 286 } 287 } 288 }() 289 } 290 291 // revalidate that our currently connected subnets are valid. 292 func (s *Service) reValidateSubscriptions(subscriptions map[uint64]*pubsub.Subscription, 293 wantedSubs []uint64, topicFormat string, digest [4]byte) { 294 for k, v := range subscriptions { 295 var wanted bool 296 for _, idx := range wantedSubs { 297 if k == idx { 298 wanted = true 299 break 300 } 301 } 302 if !wanted && v != nil { 303 v.Cancel() 304 fullTopic := fmt.Sprintf(topicFormat, digest, k) + s.cfg.P2P.Encoding().ProtocolSuffix() 305 if err := s.cfg.P2P.PubSub().UnregisterTopicValidator(fullTopic); err != nil { 306 log.WithError(err).Error("Could not unregister topic validator") 307 } 308 delete(subscriptions, k) 309 } 310 } 311 } 312 313 // subscribe missing subnets for our aggregators. 314 func (s *Service) subscribeAggregatorSubnet( 315 subscriptions map[uint64]*pubsub.Subscription, 316 idx uint64, 317 digest [4]byte, 318 validate pubsub.ValidatorEx, 319 handle subHandler, 320 ) { 321 // do not subscribe if we have no peers in the same 322 // subnet 323 topic := p2p.GossipTypeMapping[reflect.TypeOf(&pb.Attestation{})] 324 subnetTopic := fmt.Sprintf(topic, digest, idx) 325 // check if subscription exists and if not subscribe the relevant subnet. 326 if _, exists := subscriptions[idx]; !exists { 327 subscriptions[idx] = s.subscribeWithBase(subnetTopic, validate, handle) 328 } 329 if !s.validPeersExist(subnetTopic) { 330 log.Debugf("No peers found subscribed to attestation gossip subnet with "+ 331 "committee index %d. Searching network for peers subscribed to the subnet.", idx) 332 _, err := s.cfg.P2P.FindPeersWithSubnet(s.ctx, subnetTopic, idx, params.BeaconNetworkConfig().MinimumPeersInSubnet) 333 if err != nil { 334 log.WithError(err).Debug("Could not search for peers") 335 } 336 } 337 } 338 339 // lookup peers for attester specific subnets. 340 func (s *Service) lookupAttesterSubnets(digest [4]byte, idx uint64) { 341 topic := p2p.GossipTypeMapping[reflect.TypeOf(&pb.Attestation{})] 342 subnetTopic := fmt.Sprintf(topic, digest, idx) 343 if !s.validPeersExist(subnetTopic) { 344 log.Debugf("No peers found subscribed to attestation gossip subnet with "+ 345 "committee index %d. Searching network for peers subscribed to the subnet.", idx) 346 // perform a search for peers with the desired committee index. 347 _, err := s.cfg.P2P.FindPeersWithSubnet(s.ctx, subnetTopic, idx, params.BeaconNetworkConfig().MinimumPeersInSubnet) 348 if err != nil { 349 log.WithError(err).Debug("Could not search for peers") 350 } 351 } 352 } 353 354 // find if we have peers who are subscribed to the same subnet 355 func (s *Service) validPeersExist(subnetTopic string) bool { 356 numOfPeers := s.cfg.P2P.PubSub().ListPeers(subnetTopic + s.cfg.P2P.Encoding().ProtocolSuffix()) 357 return uint64(len(numOfPeers)) >= params.BeaconNetworkConfig().MinimumPeersInSubnet 358 } 359 360 func (s *Service) retrievePersistentSubs(currSlot types.Slot) []uint64 { 361 // Persistent subscriptions from validators 362 persistentSubs := s.persistentSubnetIndices() 363 // Update desired topic indices for aggregator 364 wantedSubs := s.aggregatorSubnetIndices(currSlot) 365 366 // Combine subscriptions to get all requested subscriptions 367 return sliceutil.SetUint64(append(persistentSubs, wantedSubs...)) 368 } 369 370 // filters out required peers for the node to function, not 371 // pruning peers who are in our attestation subnets. 372 func (s *Service) filterNeededPeers(pids []peer.ID) []peer.ID { 373 // Exit early if nothing to filter. 374 if len(pids) == 0 { 375 return pids 376 } 377 digest, err := s.forkDigest() 378 if err != nil { 379 log.WithError(err).Error("Could not compute fork digest") 380 return pids 381 } 382 currSlot := s.cfg.Chain.CurrentSlot() 383 wantedSubs := s.retrievePersistentSubs(currSlot) 384 wantedSubs = sliceutil.SetUint64(append(wantedSubs, s.attesterSubnetIndices(currSlot)...)) 385 topic := p2p.GossipTypeMapping[reflect.TypeOf(&pb.Attestation{})] 386 387 // Map of peers in subnets 388 peerMap := make(map[peer.ID]bool) 389 390 for _, sub := range wantedSubs { 391 subnetTopic := fmt.Sprintf(topic, digest, sub) + s.cfg.P2P.Encoding().ProtocolSuffix() 392 peers := s.cfg.P2P.PubSub().ListPeers(subnetTopic) 393 if len(peers) > int(params.BeaconNetworkConfig().MinimumPeersInSubnet) { 394 // In the event we have more than the minimum, we can 395 // mark the remaining as viable for pruning. 396 peers = peers[:params.BeaconNetworkConfig().MinimumPeersInSubnet] 397 } 398 // Add peer to peer map. 399 for _, p := range peers { 400 // Even if the peer id has 401 // already been seen we still set 402 // it, as the outcome is the same. 403 peerMap[p] = true 404 } 405 } 406 407 // Clear out necessary peers from the peers to prune. 408 newPeers := make([]peer.ID, 0, len(pids)) 409 410 for _, pid := range pids { 411 if peerMap[pid] { 412 continue 413 } 414 newPeers = append(newPeers, pid) 415 } 416 return newPeers 417 } 418 419 // Add fork digest to topic. 420 func (s *Service) addDigestToTopic(topic string) string { 421 if !strings.Contains(topic, "%x") { 422 log.Fatal("Topic does not have appropriate formatter for digest") 423 } 424 digest, err := s.forkDigest() 425 if err != nil { 426 log.WithError(err).Fatal("Could not compute fork digest") 427 } 428 return fmt.Sprintf(topic, digest) 429 } 430 431 // Add the digest and index to subnet topic. 432 func (s *Service) addDigestAndIndexToTopic(topic string, idx uint64) string { 433 if !strings.Contains(topic, "%x") { 434 log.Fatal("Topic does not have appropriate formatter for digest") 435 } 436 digest, err := s.forkDigest() 437 if err != nil { 438 log.WithError(err).Fatal("Could not compute fork digest") 439 } 440 return fmt.Sprintf(topic, digest, idx) 441 } 442 443 func (s *Service) forkDigest() ([4]byte, error) { 444 genRoot := s.cfg.Chain.GenesisValidatorRoot() 445 return p2putils.CreateForkDigest(s.cfg.Chain.GenesisTime(), genRoot[:]) 446 }