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  }