github.com/amazechain/amc@v0.1.3/internal/p2p/pubsub.go (about) 1 package p2p 2 3 import ( 4 "context" 5 "encoding/hex" 6 "github.com/amazechain/amc/api/protocol/msg_proto" 7 "github.com/amazechain/amc/utils" 8 "strings" 9 "time" 10 11 pubsub "github.com/libp2p/go-libp2p-pubsub" 12 pubsubpb "github.com/libp2p/go-libp2p-pubsub/pb" 13 "github.com/libp2p/go-libp2p/core/peer" 14 "github.com/pkg/errors" 15 ) 16 17 const ( 18 // overlay parameters 19 gossipSubD = 8 // topic stable mesh target count 20 gossipSubDlo = 6 // topic stable mesh low watermark 21 gossipSubDhi = 12 // topic stable mesh high watermark 22 23 // gossip parameters 24 gossipSubMcacheLen = 6 // number of windows to retain full messages in cache for `IWANT` responses 25 gossipSubMcacheGossip = 3 // number of windows to gossip about 26 gossipSubSeenTTL = 550 // number of heartbeat intervals to retain message IDs 27 28 // fanout ttl 29 gossipSubFanoutTTL = 60000000000 // TTL for fanout maps for topics we are not subscribed to but have published to, in nano seconds 30 31 // heartbeat interval 32 gossipSubHeartbeatInterval = 700 * time.Millisecond // frequency of heartbeat, milliseconds 33 34 // misc 35 rSubD = 8 // random gossip target 36 37 //todo 38 GossipMaxSize = 10000000 39 ) 40 41 var errInvalidTopic = errors.New("invalid topic format") 42 43 // Specifies the fixed size context length. 44 const digestLength = 4 45 46 // Specifies the prefix for any pubsub topic. 47 const gossipTopicPrefix = "/amc/" 48 49 // JoinTopic will join PubSub topic, if not already joined. 50 func (s *Service) JoinTopic(topic string, opts ...pubsub.TopicOpt) (*pubsub.Topic, error) { 51 s.joinedTopicsLock.Lock() 52 defer s.joinedTopicsLock.Unlock() 53 54 if _, ok := s.joinedTopics[topic]; !ok { 55 topicHandle, err := s.pubsub.Join(topic, opts...) 56 if err != nil { 57 return nil, err 58 } 59 s.joinedTopics[topic] = topicHandle 60 } 61 62 return s.joinedTopics[topic], nil 63 } 64 65 // LeaveTopic closes topic and removes corresponding handler from list of joined topics. 66 // This method will return error if there are outstanding event handlers or subscriptions. 67 func (s *Service) LeaveTopic(topic string) error { 68 s.joinedTopicsLock.Lock() 69 defer s.joinedTopicsLock.Unlock() 70 71 if t, ok := s.joinedTopics[topic]; ok { 72 if err := t.Close(); err != nil { 73 return err 74 } 75 delete(s.joinedTopics, topic) 76 } 77 return nil 78 } 79 80 // PublishToTopic joins (if necessary) and publishes a message to a PubSub topic. 81 func (s *Service) PublishToTopic(ctx context.Context, topic string, data []byte, opts ...pubsub.PubOpt) error { 82 topicHandle, err := s.JoinTopic(topic) 83 if err != nil { 84 return err 85 } 86 87 // Wait for at least 1 peer to be available to receive the published message. 88 for { 89 if len(topicHandle.ListPeers()) > 0 || s.cfg.MinSyncPeers == 0 { 90 return topicHandle.Publish(ctx, data, opts...) 91 } 92 select { 93 case <-ctx.Done(): 94 return errors.Wrapf(ctx.Err(), "unable to find requisite number of peers for topic %s, 0 peers found to publish to", topic) 95 default: 96 time.Sleep(100 * time.Millisecond) 97 } 98 } 99 } 100 101 // SubscribeToTopic joins (if necessary) and subscribes to PubSub topic. 102 func (s *Service) SubscribeToTopic(topic string, opts ...pubsub.SubOpt) (*pubsub.Subscription, error) { 103 104 topicHandle, err := s.JoinTopic(topic) 105 if err != nil { 106 return nil, err 107 } 108 scoringParams, err := s.topicScoreParams(topic) 109 if err != nil { 110 return nil, err 111 } 112 113 if scoringParams != nil { 114 if err := topicHandle.SetScoreParams(scoringParams); err != nil { 115 return nil, err 116 } 117 logGossipParameters(topic, scoringParams) 118 } 119 return topicHandle.Subscribe(opts...) 120 } 121 122 // peerInspector will scrape all the relevant scoring data and add it to our 123 // peer handler. 124 func (s *Service) peerInspector(peerMap map[peer.ID]*pubsub.PeerScoreSnapshot) { 125 // Iterate through all the connected peers and through any of their 126 // relevant topics. 127 for pid, snap := range peerMap { 128 s.peers.Scorers().GossipScorer().SetGossipData(pid, snap.Score, 129 snap.BehaviourPenalty, convertTopicScores(snap.Topics)) 130 } 131 } 132 133 // Creates a list of pubsub options to configure out router with. 134 func (s *Service) pubsubOptions() []pubsub.Option { 135 psOpts := []pubsub.Option{ 136 pubsub.WithMessageSignaturePolicy(pubsub.StrictNoSign), 137 pubsub.WithNoAuthor(), 138 pubsub.WithMessageIdFn(func(pmsg *pubsubpb.Message) string { 139 return MsgID(s.genesisHash, pmsg) 140 }), 141 pubsub.WithSubscriptionFilter(s), 142 pubsub.WithPeerOutboundQueueSize(pubsubQueueSize), 143 pubsub.WithMaxMessageSize(GossipMaxSize), 144 pubsub.WithValidateQueueSize(pubsubQueueSize), 145 pubsub.WithPeerScore(peerScoringParams()), 146 pubsub.WithPeerScoreInspect(s.peerInspector, time.Minute), 147 pubsub.WithGossipSubParams(pubsubGossipParam()), 148 pubsub.WithRawTracer(gossipTracer{host: s.host}), 149 } 150 return psOpts 151 } 152 153 // creates a custom gossipsub parameter set. 154 func pubsubGossipParam() pubsub.GossipSubParams { 155 gParams := pubsub.DefaultGossipSubParams() 156 gParams.Dlo = gossipSubDlo 157 gParams.D = gossipSubD 158 gParams.HeartbeatInterval = gossipSubHeartbeatInterval 159 gParams.HistoryLength = gossipSubMcacheLen 160 gParams.HistoryGossip = gossipSubMcacheGossip 161 return gParams 162 } 163 164 // We have to unfortunately set this globally in order 165 // to configure our message id time-cache rather than instantiating 166 // it with a router instance. 167 func setPubSubParameters() { 168 pubsub.TimeCacheDuration = 550 * gossipSubHeartbeatInterval 169 } 170 171 // convert from libp2p's internal schema to a compatible prysm protobuf format. 172 func convertTopicScores(topicMap map[string]*pubsub.TopicScoreSnapshot) map[string]*msg_proto.TopicScoreSnapshot { 173 newMap := make(map[string]*msg_proto.TopicScoreSnapshot, len(topicMap)) 174 for t, s := range topicMap { 175 newMap[t] = &msg_proto.TopicScoreSnapshot{ 176 TimeInMesh: uint64(s.TimeInMesh.Milliseconds()), 177 FirstMessageDeliveries: float32(s.FirstMessageDeliveries), 178 MeshMessageDeliveries: float32(s.MeshMessageDeliveries), 179 InvalidMessageDeliveries: float32(s.InvalidMessageDeliveries), 180 } 181 } 182 return newMap 183 } 184 185 // ExtractGossipDigest extracts the relevant fork digest from the gossip topic. 186 // Topics are in the form of /eth2/{fork-digest}/{topic} and this method extracts the 187 // fork digest from the topic string to a 4 byte array. 188 func ExtractGossipDigest(topic string) ([4]byte, error) { 189 // Ensure the topic prefix is correct. 190 if len(topic) < len(gossipTopicPrefix)+1 || topic[:len(gossipTopicPrefix)] != gossipTopicPrefix { 191 return [4]byte{}, errInvalidTopic 192 } 193 start := len(gossipTopicPrefix) 194 end := strings.Index(topic[start:], "/") 195 if end == -1 { // Ensure a topic suffix exists. 196 return [4]byte{}, errInvalidTopic 197 } 198 end += start 199 strDigest := topic[start:end] 200 digest, err := hex.DecodeString(strDigest) 201 if err != nil { 202 return [4]byte{}, err 203 } 204 if len(digest) != digestLength { 205 return [4]byte{}, errors.Errorf("invalid digest length wanted %d but got %d", digestLength, len(digest)) 206 } 207 return utils.ToBytes4(digest), nil 208 }