github.com/muhammedhassanm/blockchain@v0.0.0-20200120143007-697261defd4d/sawtooth-core-master/validator/sawtooth_validator/gossip/gossip.py (about)

     1  # Copyright 2017 Intel Corporation
     2  #
     3  # Licensed under the Apache License, Version 2.0 (the "License");
     4  # you may not use this file except in compliance with the License.
     5  # You may obtain a copy of the License at
     6  #
     7  #     http://www.apache.org/licenses/LICENSE-2.0
     8  #
     9  # Unless required by applicable law or agreed to in writing, software
    10  # distributed under the License is distributed on an "AS IS" BASIS,
    11  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  # See the License for the specific language governing permissions and
    13  # limitations under the License.
    14  # ------------------------------------------------------------------------------
    15  import logging
    16  import copy
    17  import time
    18  import random
    19  import os
    20  import binascii
    21  from threading import Lock
    22  from functools import partial
    23  from collections import namedtuple
    24  from enum import Enum
    25  
    26  from sawtooth_validator.concurrent.thread import InstrumentedThread
    27  from sawtooth_validator.protobuf.network_pb2 import DisconnectMessage
    28  from sawtooth_validator.protobuf.network_pb2 import GossipMessage
    29  from sawtooth_validator.protobuf.network_pb2 import GossipConsensusMessage
    30  from sawtooth_validator.protobuf.network_pb2 import GossipBatchByBatchIdRequest
    31  from sawtooth_validator.protobuf.network_pb2 import \
    32      GossipBatchByTransactionIdRequest
    33  from sawtooth_validator.protobuf.network_pb2 import GossipBlockRequest
    34  from sawtooth_validator.protobuf import validator_pb2
    35  from sawtooth_validator.protobuf.network_pb2 import PeerRegisterRequest
    36  from sawtooth_validator.protobuf.network_pb2 import PeerUnregisterRequest
    37  from sawtooth_validator.protobuf.network_pb2 import GetPeersRequest
    38  from sawtooth_validator.protobuf.network_pb2 import GetPeersResponse
    39  from sawtooth_validator.protobuf.network_pb2 import NetworkAcknowledgement
    40  from sawtooth_validator.exceptions import PeeringException
    41  
    42  LOGGER = logging.getLogger(__name__)
    43  
    44  
    45  class PeerStatus(Enum):
    46      CLOSED = 1
    47      TEMP = 2
    48      PEER = 3
    49  
    50  
    51  class EndpointStatus(Enum):
    52      # Endpoint will be used for peering
    53      PEERING = 1
    54      # Endpoint will be used to request peers
    55      TOPOLOGY = 2
    56  
    57  
    58  EndpointInfo = namedtuple('EndpointInfo',
    59                            ['status', 'time', "retry_threshold"])
    60  
    61  StaticPeerInfo = namedtuple('StaticPeerInfo',
    62                              ['time', 'retry_threshold', 'count'])
    63  
    64  INITIAL_RETRY_FREQUENCY = 10
    65  MAXIMUM_RETRY_FREQUENCY = 300
    66  
    67  MAXIMUM_STATIC_RETRY_FREQUENCY = 3600
    68  MAXIMUM_STATIC_RETRIES = 24
    69  
    70  TIME_TO_LIVE = 3
    71  
    72  # This is the protocol version number.  It should only be incremented when
    73  # there are changes to the network protocols, as well as only once per
    74  # release.
    75  NETWORK_PROTOCOL_VERSION = 1
    76  
    77  
    78  class Gossip(object):
    79      def __init__(self, network,
    80                   settings_cache,
    81                   current_chain_head_func,
    82                   current_root_func,
    83                   endpoint=None,
    84                   peering_mode='static',
    85                   initial_seed_endpoints=None,
    86                   initial_peer_endpoints=None,
    87                   minimum_peer_connectivity=3,
    88                   maximum_peer_connectivity=10,
    89                   topology_check_frequency=1
    90                   ):
    91          """Constructor for the Gossip object. Gossip defines the
    92          overlay network above the lower level networking classes.
    93  
    94          Args:
    95              network (networking.Interconnect): Provides inbound and
    96                  outbound network connections.
    97              settings_cache (state.SettingsCache): A cache for on chain
    98                  settings.
    99              current_chain_head_func (function): returns the current chain head.
   100              current_root_func (function): returns the current state root hash
   101                  for the current chain root.
   102              endpoint (str): The publically accessible zmq-style uri
   103                  endpoint for this validator.
   104              peering_mode (str): The type of peering approach. Either 'static'
   105                  or 'dynamic'. In 'static' mode, no attempted topology
   106                  buildout occurs -- the validator only attempts to initiate
   107                  peering connections with endpoints specified in the
   108                  peer_list. In 'dynamic' mode, the validator will first
   109                  attempt to initiate peering connections with endpoints
   110                  specified in the peer_list and then attempt to do a
   111                  topology buildout starting with peer lists obtained from
   112                  endpoints in the seeds_list. In either mode, the validator
   113                  will accept incoming peer requests up to max_peers.
   114              initial_seed_endpoints ([str]): A list of initial endpoints
   115                  to attempt to connect and gather initial topology buildout
   116                  information from. These are specified as zmq-compatible
   117                  URIs (e.g. tcp://hostname:port).
   118              initial_peer_endpoints ([str]): A list of initial peer endpoints
   119                  to attempt to connect and peer with. These are specified
   120                  as zmq-compatible URIs (e.g. tcp://hostname:port).
   121              minimum_peer_connectivity (int): If the number of connected
   122                  peers is below this threshold, the topology builder will
   123                  continue to attempt to identify new candidate peers to
   124                  connect with.
   125              maximum_peer_connectivity (int): The validator will reject
   126                  new peer requests if the number of connected peers
   127                  reaches this threshold.
   128              topology_check_frequency (int): The time in seconds between
   129                  topology update checks.
   130          """
   131          self._peering_mode = peering_mode
   132          self._lock = Lock()
   133          self._network = network
   134          self._endpoint = endpoint
   135          self._initial_seed_endpoints = initial_seed_endpoints \
   136              if initial_seed_endpoints else []
   137          self._initial_peer_endpoints = initial_peer_endpoints \
   138              if initial_peer_endpoints else []
   139          self._minimum_peer_connectivity = minimum_peer_connectivity
   140          self._maximum_peer_connectivity = maximum_peer_connectivity
   141          self._topology_check_frequency = topology_check_frequency
   142          self._settings_cache = settings_cache
   143  
   144          self._current_chain_head_func = current_chain_head_func
   145          self._current_root_func = current_root_func
   146  
   147          self._topology = None
   148          self._peers = {}
   149  
   150      def send_peers(self, connection_id):
   151          """Sends a message containing our peers to the
   152          connection identified by connection_id.
   153  
   154          Args:
   155              connection_id (str): A unique identifier which identifies an
   156                  connection on the network server socket.
   157          """
   158          with self._lock:
   159              # Needs to actually be the list of advertised endpoints of
   160              # our peers
   161              peer_endpoints = list(self._peers.values())
   162              if self._endpoint:
   163                  peer_endpoints.append(self._endpoint)
   164              peers_response = GetPeersResponse(peer_endpoints=peer_endpoints)
   165              try:
   166                  # Send a one_way message because the connection will be closed
   167                  # if this is a temp connection.
   168                  self._network.send(
   169                      validator_pb2.Message.GOSSIP_GET_PEERS_RESPONSE,
   170                      peers_response.SerializeToString(),
   171                      connection_id,
   172                      one_way=True)
   173              except ValueError:
   174                  LOGGER.debug("Connection disconnected: %s", connection_id)
   175  
   176      def add_candidate_peer_endpoints(self, peer_endpoints):
   177          """Adds candidate endpoints to the list of endpoints to
   178          attempt to peer with.
   179  
   180          Args:
   181              peer_endpoints ([str]): A list of public uri's which the
   182                  validator can attempt to peer with.
   183          """
   184          if self._topology:
   185              self._topology.add_candidate_peer_endpoints(peer_endpoints)
   186          else:
   187              LOGGER.debug("Could not add peer endpoints to topology. "
   188                           "ConnectionManager does not exist.")
   189  
   190      def get_peers(self):
   191          """Returns a copy of the gossip peers.
   192          """
   193          with self._lock:
   194              return copy.copy(self._peers)
   195  
   196      @property
   197      def endpoint(self):
   198          """Returns the validator's public endpoint.
   199          """
   200          return self._endpoint
   201  
   202      def register_peer(self, connection_id, endpoint):
   203          """Registers a connected connection_id.
   204  
   205          Args:
   206              connection_id (str): A unique identifier which identifies an
   207                  connection on the network server socket.
   208              endpoint (str): The publically reachable endpoint of the new
   209                  peer
   210          """
   211          with self._lock:
   212              if len(self._peers) < self._maximum_peer_connectivity:
   213                  self._peers[connection_id] = endpoint
   214                  self._topology.set_connection_status(connection_id,
   215                                                       PeerStatus.PEER)
   216                  LOGGER.debug("Added connection_id %s with endpoint %s, "
   217                               "connected identities are now %s",
   218                               connection_id, endpoint, self._peers)
   219              else:
   220                  raise PeeringException(
   221                      "At maximum configured number of peers: {} "
   222                      "Rejecting peering request from {}.".format(
   223                          self._maximum_peer_connectivity,
   224                          endpoint))
   225  
   226      def unregister_peer(self, connection_id):
   227          """Removes a connection_id from the registry.
   228  
   229          Args:
   230              connection_id (str): A unique identifier which identifies an
   231                  connection on the network server socket.
   232          """
   233          with self._lock:
   234              if connection_id in self._peers:
   235                  del self._peers[connection_id]
   236                  LOGGER.debug("Removed connection_id %s, "
   237                               "connected identities are now %s",
   238                               connection_id, self._peers)
   239                  self._topology.set_connection_status(connection_id,
   240                                                       PeerStatus.TEMP)
   241              else:
   242                  LOGGER.warning("Connection unregister failed as connection "
   243                                 "was not registered: %s",
   244                                 connection_id)
   245  
   246      def get_time_to_live(self):
   247          time_to_live = \
   248              self._settings_cache.get_setting(
   249                  "sawtooth.gossip.time_to_live",
   250                  self._current_root_func(),
   251                  default_value=TIME_TO_LIVE
   252              )
   253          return int(time_to_live)
   254  
   255      def broadcast_block(self, block, exclude=None, time_to_live=None):
   256          if time_to_live is None:
   257              time_to_live = self.get_time_to_live()
   258          gossip_message = GossipMessage(
   259              content_type=GossipMessage.BLOCK,
   260              content=block.SerializeToString(),
   261              time_to_live=time_to_live)
   262  
   263          self.broadcast(
   264              gossip_message, validator_pb2.Message.GOSSIP_MESSAGE, exclude)
   265  
   266      def broadcast_block_request(self, block_id):
   267          time_to_live = self.get_time_to_live()
   268          block_request = GossipBlockRequest(
   269              block_id=block_id,
   270              nonce=binascii.b2a_hex(os.urandom(16)),
   271              time_to_live=time_to_live)
   272          self.broadcast(block_request,
   273                         validator_pb2.Message.GOSSIP_BLOCK_REQUEST)
   274  
   275      def send_block_request(self, block_id, connection_id):
   276          time_to_live = self.get_time_to_live()
   277          block_request = GossipBlockRequest(
   278              block_id=block_id,
   279              nonce=binascii.b2a_hex(os.urandom(16)),
   280              time_to_live=time_to_live)
   281          self.send(validator_pb2.Message.GOSSIP_BLOCK_REQUEST,
   282                    block_request.SerializeToString(),
   283                    connection_id,
   284                    one_way=True)
   285  
   286      def broadcast_batch(self, batch, exclude=None, time_to_live=None):
   287          if time_to_live is None:
   288              time_to_live = self.get_time_to_live()
   289          gossip_message = GossipMessage(
   290              content_type=GossipMessage.BATCH,
   291              content=batch.SerializeToString(),
   292              time_to_live=time_to_live)
   293  
   294          self.broadcast(
   295              gossip_message, validator_pb2.Message.GOSSIP_MESSAGE, exclude)
   296  
   297      def broadcast_batch_by_transaction_id_request(self, transaction_ids):
   298          time_to_live = self.get_time_to_live()
   299          batch_request = GossipBatchByTransactionIdRequest(
   300              ids=transaction_ids,
   301              nonce=binascii.b2a_hex(os.urandom(16)),
   302              time_to_live=time_to_live)
   303          self.broadcast(
   304              batch_request,
   305              validator_pb2.Message.GOSSIP_BATCH_BY_TRANSACTION_ID_REQUEST)
   306  
   307      def broadcast_batch_by_batch_id_request(self, batch_id):
   308          time_to_live = self.get_time_to_live()
   309          batch_request = GossipBatchByBatchIdRequest(
   310              id=batch_id,
   311              nonce=binascii.b2a_hex(os.urandom(16)),
   312              time_to_live=time_to_live)
   313          self.broadcast(
   314              batch_request,
   315              validator_pb2.Message.GOSSIP_BATCH_BY_BATCH_ID_REQUEST)
   316  
   317      def send_consensus_message(self, peer_id, message, public_key):
   318          connection_id = self._network.public_key_to_connection_id(peer_id)
   319  
   320          self.send(
   321              validator_pb2.Message.GOSSIP_CONSENSUS_MESSAGE,
   322              GossipConsensusMessage(
   323                  message=message,
   324                  sender_id=public_key,
   325                  time_to_live=0).SerializeToString(),
   326              connection_id)
   327  
   328      def broadcast_consensus_message(self, message, public_key):
   329          self.broadcast(
   330              GossipConsensusMessage(
   331                  message=message,
   332                  time_to_live=self.get_time_to_live()),
   333              validator_pb2.Message.GOSSIP_CONSENSUS_MESSAGE)
   334  
   335      def send(self, message_type, message, connection_id, one_way=False):
   336          """Sends a message via the network.
   337  
   338          Args:
   339              message_type (str): The type of the message.
   340              message (bytes): The message to be sent.
   341              connection_id (str): The connection to send it to.
   342          """
   343          try:
   344              self._network.send(message_type, message, connection_id,
   345                                 one_way=one_way)
   346          except ValueError:
   347              LOGGER.debug("Connection %s is no longer valid. "
   348                           "Removing from list of peers.",
   349                           connection_id)
   350              if connection_id in self._peers:
   351                  del self._peers[connection_id]
   352  
   353      def broadcast(self, gossip_message, message_type, exclude=None):
   354          """Broadcast gossip messages.
   355  
   356          Broadcast the message to all peers unless they are in the excluded
   357          list.
   358  
   359          Args:
   360              gossip_message: The message to be broadcast.
   361              message_type: Type of the message.
   362              exclude: A list of connection_ids that should be excluded from this
   363                  broadcast.
   364          """
   365          with self._lock:
   366              if exclude is None:
   367                  exclude = []
   368              for connection_id in self._peers.copy():
   369                  if connection_id not in exclude and \
   370                          self._network.is_connection_handshake_complete(
   371                              connection_id):
   372                      self.send(
   373                          message_type,
   374                          gossip_message.SerializeToString(),
   375                          connection_id,
   376                          one_way=True)
   377  
   378      def connect_success(self, connection_id):
   379          """
   380          Notify topology that a connection has been properly authorized
   381  
   382          Args:
   383              connection_id: The connection id for the authorized connection.
   384  
   385          """
   386          if self._topology:
   387              self._topology.connect_success(connection_id)
   388  
   389      def remove_temp_endpoint(self, endpoint):
   390          """
   391          Remove temporary endpoints that never finished authorization.
   392  
   393          Args:
   394              endpoint: The endpoint that is not authorized to connect to the
   395                  network.
   396          """
   397          if self._topology:
   398              self._topology.remove_temp_endpoint(endpoint)
   399  
   400      def start(self):
   401          self._topology = ConnectionManager(
   402              gossip=self,
   403              network=self._network,
   404              endpoint=self._endpoint,
   405              current_chain_head_func=self._current_chain_head_func,
   406              initial_peer_endpoints=self._initial_peer_endpoints,
   407              initial_seed_endpoints=self._initial_seed_endpoints,
   408              peering_mode=self._peering_mode,
   409              min_peers=self._minimum_peer_connectivity,
   410              max_peers=self._maximum_peer_connectivity,
   411              check_frequency=self._topology_check_frequency)
   412  
   413          self._topology.start()
   414  
   415      def stop(self):
   416          for peer in self.get_peers():
   417              request = PeerUnregisterRequest()
   418              try:
   419                  self._network.send(validator_pb2.Message.GOSSIP_UNREGISTER,
   420                                     request.SerializeToString(),
   421                                     peer)
   422              except ValueError:
   423                  pass
   424          if self._topology:
   425              self._topology.stop()
   426  
   427  
   428  class ConnectionManager(InstrumentedThread):
   429      def __init__(self, gossip, network, endpoint,
   430                   current_chain_head_func,
   431                   initial_peer_endpoints, initial_seed_endpoints,
   432                   peering_mode, min_peers=3, max_peers=10,
   433                   check_frequency=1):
   434          """Constructor for the ConnectionManager class.
   435  
   436          Args:
   437              gossip (gossip.Gossip): The gossip overlay network.
   438              network (network.Interconnect): The underlying network.
   439              endpoint (str): A zmq-style endpoint uri representing
   440                  this validator's publically reachable endpoint.
   441              current_chain_head_func (function): Returns the current chain head.
   442              initial_peer_endpoints ([str]): A list of static peers
   443                  to attempt to connect and peer with.
   444              initial_seed_endpoints ([str]): A list of endpoints to
   445                  connect to and get candidate peer lists to attempt
   446                  to reach min_peers threshold.
   447              peering_mode (str): Either 'static' or 'dynamic'. 'static'
   448                  only connects to peers in initial_peer_endpoints.
   449                  'dynamic' connects to peers in initial_peer_endpoints
   450                  and gets candidate peer lists from initial_seed_endpoints.
   451              min_peers (int): The minimum number of peers required to stop
   452                  attempting candidate connections.
   453              max_peers (int): The maximum number of active peer connections
   454                  to allow.
   455              check_frequency (int): How often to attempt dynamic connectivity.
   456          """
   457          super().__init__(name="ConnectionManager")
   458          self._lock = Lock()
   459          self._stopped = False
   460          self._gossip = gossip
   461          self._network = network
   462          self._endpoint = endpoint
   463          self._current_chain_head_func = current_chain_head_func
   464          self._initial_peer_endpoints = initial_peer_endpoints
   465          self._initial_seed_endpoints = initial_seed_endpoints
   466          self._peering_mode = peering_mode
   467          self._min_peers = min_peers
   468          self._max_peers = max_peers
   469          self._check_frequency = check_frequency
   470  
   471          self._candidate_peer_endpoints = []
   472          # Seconds to wait for messages to arrive
   473          self._response_duration = 2
   474          self._connection_statuses = {}
   475          self._temp_endpoints = {}
   476          self._static_peer_status = {}
   477  
   478      def start(self):
   479          # First, attempt to connect to explicit peers
   480          for endpoint in self._initial_peer_endpoints:
   481              self._static_peer_status[endpoint] = \
   482                  StaticPeerInfo(
   483                      time=0,
   484                      retry_threshold=INITIAL_RETRY_FREQUENCY,
   485                      count=0)
   486  
   487          super().start()
   488  
   489      def run(self):
   490          has_chain_head = self._current_chain_head_func() is not None
   491          while not self._stopped:
   492              try:
   493                  if self._peering_mode == 'dynamic':
   494                      self.retry_dynamic_peering()
   495                  elif self._peering_mode == 'static':
   496                      self.retry_static_peering()
   497  
   498                  # This tests for a degenerate case where the node is connected
   499                  # to peers, but at first connection no peer had a valid chain
   500                  # head. Keep querying connected peers until a valid chain head
   501                  # is received.
   502                  has_chain_head = has_chain_head or \
   503                      self._current_chain_head_func() is not None
   504                  if not has_chain_head:
   505                      peered_connections = self._get_peered_connections()
   506                      if peered_connections:
   507                          LOGGER.debug(
   508                              'Have not received a chain head from peers. '
   509                              'Requesting from %s',
   510                              peered_connections)
   511  
   512                          self._request_chain_head(peered_connections)
   513  
   514                  time.sleep(self._check_frequency)
   515              except Exception:  # pylint: disable=broad-except
   516                  LOGGER.exception("Unhandled exception during peer refresh")
   517  
   518      def stop(self):
   519          self._stopped = True
   520          for connection_id in self._connection_statuses:
   521              try:
   522                  if self._connection_statuses[connection_id] == \
   523                          PeerStatus.CLOSED:
   524                      continue
   525  
   526                  msg = DisconnectMessage()
   527                  self._network.send(
   528                      validator_pb2.Message.NETWORK_DISCONNECT,
   529                      msg.SerializeToString(),
   530                      connection_id)
   531                  self._connection_statuses[connection_id] = PeerStatus.CLOSED
   532              except ValueError:
   533                  # Connection has already been disconnected.
   534                  pass
   535  
   536      def _get_peered_connections(self):
   537          peers = self._gossip.get_peers()
   538  
   539          return [conn_id for conn_id in peers
   540                  if self._connection_statuses[conn_id] == PeerStatus.PEER]
   541  
   542      def _request_chain_head(self, peered_connections):
   543          """Request chain head from the given peer ids.
   544  
   545          Args:
   546              peered_connecions (:list:str): a list of peer connection ids where
   547                  the requests will be sent.
   548          """
   549          for conn_id in peered_connections:
   550              self._gossip.send_block_request("HEAD", conn_id)
   551  
   552      def retry_dynamic_peering(self):
   553          self._refresh_peer_list(self._gossip.get_peers())
   554          peers = self._gossip.get_peers()
   555          peer_count = len(peers)
   556          if peer_count < self._min_peers:
   557              LOGGER.debug(
   558                  "Number of peers (%s) below "
   559                  "minimum peer threshold (%s). "
   560                  "Doing topology search.",
   561                  peer_count,
   562                  self._min_peers)
   563  
   564              self._reset_candidate_peer_endpoints()
   565              self._refresh_peer_list(peers)
   566              # Cleans out any old connections that have disconnected
   567              self._refresh_connection_list()
   568              self._check_temp_endpoints()
   569  
   570              peers = self._gossip.get_peers()
   571  
   572              self._get_peers_of_peers(peers)
   573              self._get_peers_of_endpoints(
   574                  peers,
   575                  self._initial_seed_endpoints)
   576  
   577              # Wait for GOSSIP_GET_PEER_RESPONSE messages to arrive
   578              time.sleep(self._response_duration)
   579  
   580              peered_endpoints = list(peers.values())
   581  
   582              with self._lock:
   583                  unpeered_candidates = list(
   584                      set(self._candidate_peer_endpoints)
   585                      - set(peered_endpoints)
   586                      - set([self._endpoint]))
   587  
   588              LOGGER.debug(
   589                  "Peers are: %s. "
   590                  "Unpeered candidates are: %s",
   591                  peered_endpoints,
   592                  unpeered_candidates)
   593  
   594              if unpeered_candidates:
   595                  self._attempt_to_peer_with_endpoint(
   596                      random.choice(unpeered_candidates))
   597  
   598      def retry_static_peering(self):
   599          with self._lock:
   600              # Endpoints that have reached their retry count and should be
   601              # removed
   602              to_remove = []
   603              for endpoint in self._initial_peer_endpoints:
   604                  connection_id = None
   605                  try:
   606                      connection_id = \
   607                          self._network.get_connection_id_by_endpoint(endpoint)
   608                  except KeyError:
   609                      pass
   610  
   611                  static_peer_info = self._static_peer_status[endpoint]
   612                  if connection_id is not None:
   613                      if connection_id in self._connection_statuses:
   614                          # Endpoint is already a Peer
   615                          if self._connection_statuses[connection_id] == \
   616                                  PeerStatus.PEER:
   617                              # reset static peering info
   618                              self._static_peer_status[endpoint] = \
   619                                  StaticPeerInfo(
   620                                      time=0,
   621                                      retry_threshold=INITIAL_RETRY_FREQUENCY,
   622                                      count=0)
   623                              continue
   624  
   625                  if (time.time() - static_peer_info.time) > \
   626                          static_peer_info.retry_threshold:
   627                      LOGGER.debug("Endpoint has not completed authorization in "
   628                                   "%s seconds: %s",
   629                                   static_peer_info.retry_threshold,
   630                                   endpoint)
   631                      if connection_id is not None:
   632                          # If the connection exists remove it before retrying to
   633                          # authorize.
   634                          try:
   635                              self._network.remove_connection(connection_id)
   636                          except KeyError:
   637                              pass
   638  
   639                      if static_peer_info.retry_threshold == \
   640                              MAXIMUM_STATIC_RETRY_FREQUENCY:
   641                          if static_peer_info.count >= MAXIMUM_STATIC_RETRIES:
   642                              # Unable to peer with endpoint
   643                              to_remove.append(endpoint)
   644                              continue
   645                          else:
   646                              # At maximum retry threashold, increment count
   647                              self._static_peer_status[endpoint] = \
   648                                  StaticPeerInfo(
   649                                      time=time.time(),
   650                                      retry_threshold=min(
   651                                          static_peer_info.retry_threshold * 2,
   652                                          MAXIMUM_STATIC_RETRY_FREQUENCY),
   653                                      count=static_peer_info.count + 1)
   654                      else:
   655                          self._static_peer_status[endpoint] = \
   656                              StaticPeerInfo(
   657                                  time=time.time(),
   658                                  retry_threshold=min(
   659                                      static_peer_info.retry_threshold * 2,
   660                                      MAXIMUM_STATIC_RETRY_FREQUENCY),
   661                                  count=0)
   662  
   663                      LOGGER.debug("attempting to peer with %s", endpoint)
   664                      self._network.add_outbound_connection(endpoint)
   665                      self._temp_endpoints[endpoint] = EndpointInfo(
   666                          EndpointStatus.PEERING,
   667                          time.time(),
   668                          INITIAL_RETRY_FREQUENCY)
   669  
   670              for endpoint in to_remove:
   671                  # Endpoints that have reached their retry count and should be
   672                  # removed
   673                  self._initial_peer_endpoints.remove(endpoint)
   674                  del self._static_peer_status[endpoint]
   675  
   676      def add_candidate_peer_endpoints(self, peer_endpoints):
   677          """Adds candidate endpoints to the list of endpoints to
   678          attempt to peer with.
   679  
   680          Args:
   681              peer_endpoints ([str]): A list of public uri's which the
   682                  validator can attempt to peer with.
   683          """
   684          with self._lock:
   685              for endpoint in peer_endpoints:
   686                  if endpoint not in self._candidate_peer_endpoints:
   687                      self._candidate_peer_endpoints.append(endpoint)
   688  
   689      def set_connection_status(self, connection_id, status):
   690          self._connection_statuses[connection_id] = status
   691  
   692      def remove_temp_endpoint(self, endpoint):
   693          with self._lock:
   694              if endpoint in self._temp_endpoints:
   695                  del self._temp_endpoints[endpoint]
   696  
   697      def _check_temp_endpoints(self):
   698          with self._lock:
   699              for endpoint in self._temp_endpoints:
   700                  endpoint_info = self._temp_endpoints[endpoint]
   701                  if (time.time() - endpoint_info.time) > \
   702                          endpoint_info.retry_threshold:
   703                      LOGGER.debug("Endpoint has not completed authorization in "
   704                                   "%s seconds: %s",
   705                                   endpoint_info.retry_threshold,
   706                                   endpoint)
   707                      try:
   708                          # If the connection exists remove it before retrying to
   709                          # authorize. If the connection does not exist, a
   710                          # KeyError will be thrown.
   711                          conn_id = \
   712                              self._network.get_connection_id_by_endpoint(
   713                                  endpoint)
   714                          self._network.remove_connection(conn_id)
   715                      except KeyError:
   716                          pass
   717  
   718                      self._network.add_outbound_connection(endpoint)
   719                      self._temp_endpoints[endpoint] = EndpointInfo(
   720                          endpoint_info.status,
   721                          time.time(),
   722                          min(endpoint_info.retry_threshold * 2,
   723                              MAXIMUM_RETRY_FREQUENCY))
   724  
   725      def _refresh_peer_list(self, peers):
   726          for conn_id in peers:
   727              try:
   728                  self._network.get_connection_id_by_endpoint(
   729                      peers[conn_id])
   730              except KeyError:
   731                  LOGGER.debug("removing peer %s because "
   732                               "connection went away",
   733                               peers[conn_id])
   734  
   735                  self._gossip.unregister_peer(conn_id)
   736                  if conn_id in self._connection_statuses:
   737                      del self._connection_statuses[conn_id]
   738  
   739      def _refresh_connection_list(self):
   740          with self._lock:
   741              closed_connections = []
   742              for connection_id in self._connection_statuses:
   743                  if not self._network.has_connection(connection_id):
   744                      closed_connections.append(connection_id)
   745  
   746              for connection_id in closed_connections:
   747                  del self._connection_statuses[connection_id]
   748  
   749      def _get_peers_of_peers(self, peers):
   750          get_peers_request = GetPeersRequest()
   751  
   752          for conn_id in peers:
   753              try:
   754                  self._network.send(
   755                      validator_pb2.Message.GOSSIP_GET_PEERS_REQUEST,
   756                      get_peers_request.SerializeToString(),
   757                      conn_id)
   758              except ValueError:
   759                  LOGGER.debug("Peer disconnected: %s", conn_id)
   760  
   761      def _get_peers_of_endpoints(self, peers, endpoints):
   762          get_peers_request = GetPeersRequest()
   763  
   764          for endpoint in endpoints:
   765              conn_id = None
   766              try:
   767                  conn_id = self._network.get_connection_id_by_endpoint(
   768                      endpoint)
   769  
   770              except KeyError:
   771                  # If the connection does not exist, send a connection request
   772                  with self._lock:
   773                      if endpoint in self._temp_endpoints:
   774                          del self._temp_endpoints[endpoint]
   775  
   776                      self._temp_endpoints[endpoint] = EndpointInfo(
   777                          EndpointStatus.TOPOLOGY,
   778                          time.time(),
   779                          INITIAL_RETRY_FREQUENCY)
   780  
   781                      self._network.add_outbound_connection(endpoint)
   782  
   783              # If the connection does exist, request peers.
   784              if conn_id is not None:
   785                  if not self._network.is_connection_handshake_complete(conn_id):
   786                      # has not finished the authorization (trust/challenge)
   787                      # process yet.
   788                      continue
   789                  elif conn_id in peers:
   790                      # connected and peered - we've already sent peer request
   791                      continue
   792                  else:
   793                      # connected but not peered
   794                      if endpoint in self._temp_endpoints:
   795                          # Endpoint is not yet authorized, do not request peers
   796                          continue
   797  
   798                      try:
   799                          self._network.send(
   800                              validator_pb2.Message.GOSSIP_GET_PEERS_REQUEST,
   801                              get_peers_request.SerializeToString(),
   802                              conn_id)
   803                      except ValueError:
   804                          LOGGER.debug("Connection disconnected: %s", conn_id)
   805  
   806      def _attempt_to_peer_with_endpoint(self, endpoint):
   807          LOGGER.debug("Attempting to connect/peer with %s", endpoint)
   808  
   809          # check if the connection exists, if it does - send,
   810          # otherwise create it
   811          try:
   812              connection_id = \
   813                  self._network.get_connection_id_by_endpoint(
   814                      endpoint)
   815  
   816              register_request = PeerRegisterRequest(
   817                  endpoint=self._endpoint,
   818                  protocol_version=NETWORK_PROTOCOL_VERSION)
   819  
   820              self._network.send(
   821                  validator_pb2.Message.GOSSIP_REGISTER,
   822                  register_request.SerializeToString(),
   823                  connection_id,
   824                  callback=partial(
   825                      self._peer_callback,
   826                      endpoint=endpoint,
   827                      connection_id=connection_id))
   828          except KeyError:
   829              # if the connection uri wasn't found in the network's
   830              # connections, it raises a KeyError and we need to add
   831              # a new outbound connection
   832              with self._lock:
   833                  self._temp_endpoints[endpoint] = EndpointInfo(
   834                      EndpointStatus.PEERING,
   835                      time.time(),
   836                      INITIAL_RETRY_FREQUENCY)
   837              self._network.add_outbound_connection(endpoint)
   838  
   839      def _reset_candidate_peer_endpoints(self):
   840          with self._lock:
   841              self._candidate_peer_endpoints = []
   842  
   843      def _peer_callback(self, request, result, connection_id, endpoint=None):
   844          with self._lock:
   845              ack = NetworkAcknowledgement()
   846              ack.ParseFromString(result.content)
   847  
   848              if ack.status == ack.ERROR:
   849                  LOGGER.debug("Peering request to %s was NOT successful",
   850                               connection_id)
   851                  self._remove_temporary_connection(connection_id)
   852              elif ack.status == ack.OK:
   853                  LOGGER.debug("Peering request to %s was successful",
   854                               connection_id)
   855                  if endpoint:
   856                      try:
   857                          self._gossip.register_peer(connection_id, endpoint)
   858                          self._connection_statuses[connection_id] = \
   859                              PeerStatus.PEER
   860                          self._gossip.send_block_request("HEAD", connection_id)
   861                      except PeeringException as e:
   862                          # Remove unsuccessful peer
   863                          LOGGER.warning('Unable to successfully peer with '
   864                                         'connection_id: %s, due to %s',
   865                                         connection_id, str(e))
   866  
   867                          self._remove_temporary_connection(connection_id)
   868                  else:
   869                      LOGGER.debug("Cannot register peer with no endpoint for "
   870                                   "connection_id: %s",
   871                                   connection_id)
   872                      self._remove_temporary_connection(connection_id)
   873  
   874      def _remove_temporary_connection(self, connection_id):
   875          status = self._connection_statuses.get(connection_id)
   876          if status == PeerStatus.TEMP:
   877              LOGGER.debug("Closing connection to %s", connection_id)
   878              msg = DisconnectMessage()
   879              try:
   880                  self._network.send(validator_pb2.Message.NETWORK_DISCONNECT,
   881                                     msg.SerializeToString(),
   882                                     connection_id)
   883              except ValueError:
   884                  pass
   885              del self._connection_statuses[connection_id]
   886              self._network.remove_connection(connection_id)
   887          elif status == PeerStatus.PEER:
   888              LOGGER.debug("Connection close request for peer ignored: %s",
   889                           connection_id)
   890          elif status is None:
   891              LOGGER.debug("Connection close request for unknown connection "
   892                           "ignored: %s",
   893                           connection_id)
   894  
   895      def connect_success(self, connection_id):
   896          """
   897          Check to see if the successful connection is meant to be peered with.
   898          If not, it should be used to get the peers from the endpoint.
   899          """
   900          endpoint = self._network.connection_id_to_endpoint(connection_id)
   901          endpoint_info = self._temp_endpoints.get(endpoint)
   902  
   903          LOGGER.debug("Endpoint has completed authorization: %s (id: %s)",
   904                       endpoint,
   905                       connection_id)
   906          if endpoint_info is None:
   907              LOGGER.debug("Received unknown endpoint: %s", endpoint)
   908  
   909          elif endpoint_info.status == EndpointStatus.PEERING:
   910              self._connect_success_peering(connection_id, endpoint)
   911  
   912          elif endpoint_info.status == EndpointStatus.TOPOLOGY:
   913              self._connect_success_topology(connection_id)
   914  
   915          else:
   916              LOGGER.debug("Endpoint has unknown status: %s", endpoint)
   917  
   918          with self._lock:
   919              if endpoint in self._temp_endpoints:
   920                  del self._temp_endpoints[endpoint]
   921  
   922      def _connect_success_peering(self, connection_id, endpoint):
   923          LOGGER.debug("Connection to %s succeeded", connection_id)
   924  
   925          register_request = PeerRegisterRequest(
   926              endpoint=self._endpoint,
   927              protocol_version=NETWORK_PROTOCOL_VERSION)
   928          self._connection_statuses[connection_id] = PeerStatus.TEMP
   929          try:
   930              self._network.send(
   931                  validator_pb2.Message.GOSSIP_REGISTER,
   932                  register_request.SerializeToString(),
   933                  connection_id,
   934                  callback=partial(
   935                      self._peer_callback,
   936                      connection_id=connection_id,
   937                      endpoint=endpoint))
   938          except ValueError:
   939              LOGGER.debug("Connection disconnected: %s", connection_id)
   940  
   941      def _connect_success_topology(self, connection_id):
   942          LOGGER.debug("Connection to %s succeeded for topology request",
   943                       connection_id)
   944          self._connection_statuses[connection_id] = PeerStatus.TEMP
   945          get_peers_request = GetPeersRequest()
   946  
   947          def callback(request, result):
   948              # request, result are ignored, but required by the callback
   949              self._remove_temporary_connection(connection_id)
   950  
   951          try:
   952              self._network.send(
   953                  validator_pb2.Message.GOSSIP_GET_PEERS_REQUEST,
   954                  get_peers_request.SerializeToString(),
   955                  connection_id,
   956                  callback=callback)
   957          except ValueError:
   958              LOGGER.debug("Connection disconnected: %s", connection_id)