def dataReceived(self, data: bytes) -> None: self._buffer += data total_read = len(self._buffer) if total_read > config.dev.max_bytes_out: logger.warning('Disconnecting peer %s', self.peer) logger.warning('Buffer Size %s', len(self._buffer)) self.loseConnection() return read_bytes = [0] msg = None for msg in self._parse_buffer(read_bytes): self.update_counters() self.in_counter += 1 if self.in_counter > self.rate_limit * IN_FACTOR: logger.warning("Rate Limit hit by %s %s", self.peer.ip, self.peer.port) self.peer_manager.ban_channel(self) return if self._valid_message_count < config.dev.trust_min_msgcount * 2: # Avoid overflows self._valid_message_count += 1 self._observable.notify(msg) if msg is not None and read_bytes[ 0] and msg.func_name != xrdlegacy_pb2.LegacyMessage.P2P_ACK: p2p_ack = xrd_pb2.P2PAcknowledgement(bytes_processed=read_bytes[0]) msg = xrdlegacy_pb2.LegacyMessage( func_name=xrdlegacy_pb2.LegacyMessage.P2P_ACK, p2pAckData=p2p_ack) self.send(msg)
def handle_block_height(self, source, message: xrdlegacy_pb2.LegacyMessage): """ Sends / Receives Blockheight :param source: :param message: :return: """ if message.bhData.block_number == 0: block = source.factory.last_block cumulative_difficulty = source.factory.get_cumulative_difficulty() if block.block_number == 0: return bhdata = xrd_pb2.BlockHeightData( block_number=block.block_number, block_headerhash=block.headerhash, cumulative_difficulty=bytes(cumulative_difficulty)) msg = xrdlegacy_pb2.LegacyMessage( func_name=xrdlegacy_pb2.LegacyMessage.BH, bhData=bhdata) source.send(msg) return try: UInt256ToString(message.bhData.cumulative_difficulty) except ValueError: logger.warning('Invalid Block Height Data') source.loseConnection() return source.factory.update_peer_blockheight( source.peer.full_address, message.bhData.block_number, message.bhData.block_headerhash, message.bhData.cumulative_difficulty)
def test_wrap_message_works(self): veData = xrdlegacy_pb2.VEData(version="version", genesis_prev_hash=b'genesis_hash') msg = xrdlegacy_pb2.LegacyMessage( func_name=xrdlegacy_pb2.LegacyMessage.VE, veData=veData) self.assertEqual( '000000191a170a0776657273696f6e120c67656e657369735f68617368', bin2hstr(P2PProtocol._wrap_message(msg)))
def get(self, msg_type, msg_hash: bytes) -> Optional[xrdlegacy_pb2.LegacyMessage]: if not self.contains(msg_hash, msg_type): return None msg = self._hash_msg[msg_hash].msg data = xrdlegacy_pb2.LegacyMessage(**{'func_name': msg_type, self.services_arg[msg_type]: msg}) return data
def send_sync(self, synced=False): state_str = '' if synced: state_str = 'Synced' msg = xrdlegacy_pb2.LegacyMessage( func_name=xrdlegacy_pb2.LegacyMessage.SYNC, syncData=xrdlegacy_pb2.SYNCData(state=state_str)) self.send(msg)
def test_handle_sync_unsynced(self): """ If the message says anything else: the peer is unsynced. """ sync_message = xrdlegacy_pb2.LegacyMessage(func_name=xrdlegacy_pb2.LegacyMessage.SYNC, syncData=xrdlegacy_pb2.SYNCData(state='Unsynced')) channel = make_channel() self.peer_manager.handle_sync(channel, sync_message)
def test_handle_sync(self): """ If a message comes in saying 'Synced': the peer is synced. """ sync_message = xrdlegacy_pb2.LegacyMessage(func_name=xrdlegacy_pb2.LegacyMessage.SYNC, syncData=xrdlegacy_pb2.SYNCData(state='Synced')) channel = make_channel() self.peer_manager.handle_sync(channel, sync_message)
def send_get_headerhash_list(self, current_block_height): start_blocknumber = max(0, current_block_height - config.dev.reorg_limit) node_header_hash = xrd_pb2.NodeHeaderHash( block_number=start_blocknumber, headerhashes=[]) msg = xrdlegacy_pb2.LegacyMessage( func_name=xrdlegacy_pb2.LegacyMessage.HEADERHASHES, nodeHeaderHash=node_header_hash) self.send(msg)
def _parse_buffer(self, total_read): # FIXME: This parsing/wire protocol needs to be replaced """ >>> from pyxrdlib.pyxrdlib import hstr2bin >>> p=P2PProtocol() >>> p._buffer = bytes(hstr2bin('000000191a170a0776657273696f6e120c67656e657369735f68617368'+ \ '000000191a170a0776657273696f6e120c67656e657369735f68617368')) >>> messages = p._parse_buffer([0]) >>> len(list(messages)) 2 """ chunk_size = 0 while self._buffer: if len(self._buffer) < 5: # Buffer is still incomplete as it doesn't have message size return ignore_skip = False try: chunk_size_raw = self._buffer[:4] chunk_size = struct.unpack( '>L', chunk_size_raw)[0] # is m length encoded correctly? if chunk_size <= 0: logger.debug("<X< %s", bin2hstr(self._buffer)) raise Exception("Invalid chunk size <= 0") if chunk_size > config.dev.message_buffer_size: raise Exception("Invalid chunk size > message_buffer_size") if len( self._buffer ) - 4 < chunk_size: # As 4 bytes includes chunk_size_raw ignore_skip = True # Buffer is still incomplete as it doesn't have message so skip moving buffer return message_raw = self._buffer[4:4 + chunk_size] message = xrdlegacy_pb2.LegacyMessage() message.ParseFromString(message_raw) yield message except Exception as e: # no qa logger.warning( "Problem parsing message. Banning+Dropping connection") logger.exception(e) self.peer_manager.ban_channel(self) finally: if not ignore_skip: skip = 4 + chunk_size self._buffer = self._buffer[skip:] total_read[0] += skip
def test_handle_p2p_acknowledgement_negative_bytes_processed(self): """ If we sent 10 bytes to the peer, but the peer says it processed 20 bytes, disconnect the peer. """ ack = xrdlegacy_pb2.LegacyMessage(func_name=xrdlegacy_pb2.LegacyMessage.P2P_ACK, p2pAckData=xrd_pb2.P2PAcknowledgement(bytes_processed=20)) channel = make_channel() channel.bytes_sent = 10 self.peer_manager.handle_p2p_acknowledgement(channel, ack) channel.loseConnection.assert_called_once_with()
def test_handle_p2p_acknowledgement(self): """ Once an acknowledgement is received from a peer, we can update rate counters and send the next message. """ ack = xrdlegacy_pb2.LegacyMessage(func_name=xrdlegacy_pb2.LegacyMessage.P2P_ACK, p2pAckData=xrd_pb2.P2PAcknowledgement(bytes_processed=15)) channel = make_channel() channel.bytes_sent = 20 self.peer_manager.handle_p2p_acknowledgement(channel, ack) channel.send_next.assert_called_once_with()
def send_fetch_block(self, block_idx): """ Fetch Block n Sends request for the block number n. :return: """ logger.info('<<<Fetching block: %s from %s', block_idx, self.peer) msg = xrdlegacy_pb2.LegacyMessage( func_name=xrdlegacy_pb2.LegacyMessage.FB, fbData=xrdlegacy_pb2.FBData(index=block_idx)) self.send(msg)
def test_handle_version_empty_version_message(self): """ If the incoming version message has an empty version field, then send another version request. That message should have the node's version in it. """ channel = make_channel() message = xrdlegacy_pb2.LegacyMessage(func_name=xrdlegacy_pb2.LegacyMessage.VE, veData=xrdlegacy_pb2.VEData(version='', genesis_prev_hash=config.user.genesis_prev_headerhash, rate_limit=config.user.peer_rate_limit)) self.peer_manager.handle_version(channel, message) self.assertEqual(channel.send.call_args[0][0].veData.version, config.dev.version)
def test_handle_version_wrong_genesis_prev_headerhash(self): """ If the genesis_prev_headerhash is different, the nodes should disconnect from each other. """ with set_xrd_dir('no_data'): channel = make_channel() message = xrdlegacy_pb2.LegacyMessage(func_name=xrdlegacy_pb2.LegacyMessage.VE, veData=xrdlegacy_pb2.VEData(version=config.dev.version, genesis_prev_hash=b'TEST123', rate_limit=config.user.peer_rate_limit)) self.peer_manager.handle_version(channel, message) channel.loseConnection.assert_any_call()
def test_handle_peer_list_works(self, logger): """ Heavy error testing should be done in combine_peer_lists() and extend_known_peers(), which this fx uses. """ peer_list_message = xrdlegacy_pb2.LegacyMessage(func_name=xrdlegacy_pb2.LegacyMessage.PL, plData=xrdlegacy_pb2.PLData( peer_ips={'127.0.0.3:5000', '127.0.0.4:5001'}, public_port=9000)) channel = make_channel() channel.host = IPMetadata('187.0.0.1', 9000) channel.peer = IPMetadata('187.0.0.2', 9000) channel.ip_public_port = '187.0.0.1:9000' # handle_peer_list() will call extend_known_peers(), so we gotta mock it out. It's tested elsewhere anyway. self.peer_manager.handle_peer_list(channel, peer_list_message)
def test_handle_version(self): """ When a version message arrives from a peer, and all else is normal: A version request message was not sent to the peer. The peer is not banned. P2PProtocol.loseConnection() is not called. """ channel = make_channel() message = xrdlegacy_pb2.LegacyMessage(func_name=xrdlegacy_pb2.LegacyMessage.VE, veData=xrdlegacy_pb2.VEData(version=config.dev.version, genesis_prev_hash=config.user.genesis_prev_headerhash, rate_limit=config.user.peer_rate_limit)) self.peer_manager.handle_version(channel, message) channel.peer_manager.ban_channel.assert_not_called() channel.loseConnection.assert_not_called()
def request_full_message(self, mr_data: xrdlegacy_pb2.MRData): """ Request Full Message This function request for the full message against, the Message Receipt received. :return: """ # FIXME: Again, breaking encasulation # FIXME: Huge amount of lookups in dictionaries msg_hash = mr_data.hash if msg_hash in self.master_mr._hash_msg: if msg_hash in self.master_mr.requested_hash: del self.master_mr.requested_hash[msg_hash] return if msg_hash not in self.master_mr.requested_hash: return peers_list = self.master_mr.requested_hash[ msg_hash].peers_connection_list message_request = self.master_mr.requested_hash[msg_hash] for peer in peers_list: if peer in message_request.already_requested_peers: continue message_request.already_requested_peers.append(peer) msg = xrdlegacy_pb2.LegacyMessage( func_name=xrdlegacy_pb2.LegacyMessage.SFM, mrData=xrdlegacy_pb2.MRData(hash=mr_data.hash, type=mr_data.type)) peer.send(msg) call_later_obj = reactor.callLater( config.dev.message_receipt_timeout, self.request_full_message, mr_data) message_request.callLater = call_later_obj return # If execution reach to this line, then it means no peer was able to provide # Full message for this hash thus the hash has to be deleted. # Moreover, negative points could be added to the peers, for this behavior if msg_hash in self.master_mr.requested_hash: del self.master_mr.requested_hash[msg_hash]
def test_handle_peer_list_empty_peer_list_message(self, logger): """ An empty plData should result in no further processing being done. """ peer_list_message = xrdlegacy_pb2.LegacyMessage(func_name=xrdlegacy_pb2.LegacyMessage.PL, plData=xrdlegacy_pb2.PLData(peer_ips={}, public_port=9000)) channel = make_channel() channel.host_ip = '127.0.0.1' channel.peer_ip = '127.0.0.2' self.peer_manager.extend_known_peers = Mock(autospec=P2PPeerManager.extend_known_peers) self.peer_manager.combine_peer_lists = Mock(autospec=P2PPeerManager.combine_peer_lists) self.peer_manager.handle_peer_list(channel, peer_list_message) self.peer_manager.combine_peer_lists.assert_not_called() self.peer_manager.extend_known_peers.assert_not_called()
def test_handle_sync_blank(self): """ If the message says '': the peer doesn't know, and isn't synced. We tell it that we are synced. """ sync_message = xrdlegacy_pb2.LegacyMessage(func_name=xrdlegacy_pb2.LegacyMessage.SYNC, syncData=xrdlegacy_pb2.SYNCData(state='')) channel = make_channel() # But if we ourselves aren't synced, then we cannot say if anybody else is synced. channel.factory.synced = False self.peer_manager.handle_sync(channel, sync_message) channel.send_sync.assert_not_called() # If we are synced, then we can tell other nodes we are synced. channel.factory.synced = True self.peer_manager.handle_sync(channel, sync_message) channel.send_sync.assert_called_once_with(synced=True)
def send_peer_list(self): """ Get Peers Sends the peers list. :return: """ trusted_peers = self.peer_manager.trusted_addresses logger.debug('<<< Sending connected peers to %s [%s]', self.peer, trusted_peers) msg = xrdlegacy_pb2.LegacyMessage( func_name=xrdlegacy_pb2.LegacyMessage.PL, plData=xrdlegacy_pb2.PLData( peer_ips=trusted_peers, public_port=config.user.p2p_public_port)) self.send(msg)
def handle_node_headerhash(self, source, message: xrdlegacy_pb2.LegacyMessage): """ Sends/Receives NodeHeaderHashes :param source: :param message: :return: """ if len(message.nodeHeaderHash.headerhashes) == 0: node_headerhash = source.factory.get_headerhashes( message.nodeHeaderHash.block_number) msg = xrdlegacy_pb2.LegacyMessage( func_name=xrdlegacy_pb2.LegacyMessage.HEADERHASHES, nodeHeaderHash=node_headerhash) source.send(msg) else: source.factory.compare_and_sync(source, message.nodeHeaderHash)
def handle_fetch_block( self, source, message: xrdlegacy_pb2.LegacyMessage): # Fetch Request for block """ Fetch Block Sends the request for the block. :return: """ P2PBaseObserver._validate_message(message, xrdlegacy_pb2.LegacyMessage.FB) block_number = message.fbData.index logger.info(' Request for %s by %s', block_number, source.peer) if 0 < block_number <= source.factory.chain_height: block = source.factory.get_block_by_number(block_number) msg = xrdlegacy_pb2.LegacyMessage( func_name=xrdlegacy_pb2.LegacyMessage.PB, pbData=xrdlegacy_pb2.PBData(block=block.pbdata)) source.send(msg)
def test_handle_peer_list_peer_discovery_disabled(self, config, logger): """ If enable_peer_discovery is False, no further processing should be done. """ config.user.enable_peer_discovery = False peer_list_message = xrdlegacy_pb2.LegacyMessage(func_name=xrdlegacy_pb2.LegacyMessage.PL, plData=xrdlegacy_pb2.PLData( peer_ips={'127.0.0.3:5000', '127.0.0.4:5001'}, public_port=9000)) channel = make_channel() channel.host_ip = '127.0.0.1' channel.peer_ip = '127.0.0.2' self.peer_manager.extend_known_peers = Mock(autospec=P2PPeerManager.extend_known_peers) self.peer_manager.combine_peer_lists = Mock(autospec=P2PPeerManager.combine_peer_lists) self.peer_manager.handle_peer_list(channel, peer_list_message) self.peer_manager.combine_peer_lists.assert_not_called() self.peer_manager.extend_known_peers.assert_not_called()
def test_handle_chain_state_works(self): """ When a peer reports its chain state, we update the timestamp to reflect the time we received the message. Then we update our _peer_node_status. """ chain_state_data = make_node_chain_state() chain_state_message = xrdlegacy_pb2.LegacyMessage(func_name=xrdlegacy_pb2.LegacyMessage.CHAINSTATE, chainStateData=chain_state_data) channel = make_channel() # The P2PPeerManager should update its hash table with the info from chain_state_data. # Unfortunately it also updates the timestamp so we cannot simply compare the objects, # we have to compare the fields in the object excluding the timestamp. self.peer_manager.handle_chain_state(channel, chain_state_message) self.assertEqual(self.peer_manager._peer_node_status[channel].block_number, chain_state_data.block_number) self.assertEqual(self.peer_manager._peer_node_status[channel].header_hash, chain_state_data.header_hash) self.assertEqual(self.peer_manager._peer_node_status[channel].cumulative_difficulty, chain_state_data.cumulative_difficulty)
def broadcast(self, msg_type, msg_hash: bytes, mr_data=None): """ Broadcast This function sends the Message Receipt to all connected peers. :return: """ ignore_peers = [] if msg_hash in self.master_mr.requested_hash: ignore_peers = self.master_mr.requested_hash[ msg_hash].peers_connection_list if not mr_data: mr_data = xrdlegacy_pb2.MRData() mr_data.hash = msg_hash mr_data.type = msg_type data = xrdlegacy_pb2.LegacyMessage( func_name=xrdlegacy_pb2.LegacyMessage.MR, mrData=mr_data) for peer in self._peer_connections: if peer not in ignore_peers: peer.send(data)
def request_peer_blockheight(self): for peer in self._peer_connections: msg = xrdlegacy_pb2.LegacyMessage( func_name=xrdlegacy_pb2.LegacyMessage.BH, bhData=xrd_pb2.BlockHeightData(block_number=0)) peer.send(msg)
def make_message(**kwargs): return xrdlegacy_pb2.LegacyMessage(**kwargs)
def send_version_request(self): msg = xrdlegacy_pb2.LegacyMessage( func_name=xrdlegacy_pb2.LegacyMessage.VE, veData=xrdlegacy_pb2.VEData()) self.send(msg)