def __init__(self, opts, node_ssl_service: NodeSSLService) -> None: super(EthGatewayNode, self).__init__( opts, node_ssl_service, eth_common_constants.TRACKED_BLOCK_CLEANUP_INTERVAL_S) self._node_public_key = None self._remote_public_key = None if opts.node_public_key is not None: self._node_public_key = convert.hex_to_bytes(opts.node_public_key) elif opts.blockchain_peers is None: raise RuntimeError( "128 digit public key must be included with command-line specified blockchain peer." ) if opts.remote_blockchain_peer is not None: if opts.remote_public_key is None: raise RuntimeError( "128 digit public key must be included with command-line specified remote blockchain peer." ) else: self._remote_public_key = convert.hex_to_bytes( opts.remote_public_key) self.block_processing_service: EthBlockProcessingService = EthBlockProcessingService( self) self.block_queuing_service: EthBlockQueuingService = EthBlockQueuingService( self) # List of know total difficulties, tuples of values (block hash, total difficulty) self._last_known_difficulties = deque( maxlen=eth_common_constants.LAST_KNOWN_TOTAL_DIFFICULTIES_MAX_COUNT ) # queue of the block hashes requested from remote blockchain node during sync self._requested_remote_blocks_queue = deque() # number of remote block requests to skip in case if requests and responses got out of sync self._skip_remote_block_requests_stats_count = 0 self.init_eth_gateway_stat_logging() self.init_eth_on_block_feed_stat_logging() self.message_converter = converter_factory.create_eth_message_converter( self.opts) self.eth_ws_proxy_publisher = EthWsProxyPublisher( opts.eth_ws_uri, self.feed_manager, self._tx_service, self) if self.opts.ws and not self.opts.eth_ws_uri: logger.warning(log_messages.ETH_WS_SUBSCRIBER_NOT_STARTED) self.average_block_gas_price = RunningAverage( gateway_constants.ETH_GAS_RUNNING_AVERAGE_SIZE) self.min_tx_from_node_gas_price = IntervalMinimum( gateway_constants.ETH_MIN_GAS_INTERVAL_S, self.alarm_queue) logger.info("Gateway enode url: {}", self.get_enode())
async def setUp(self) -> None: crypto_utils.recover_public_key = MagicMock(return_value=bytes(32)) account_model = BdnAccountModelBase( "account_id", "account_name", "fake_certificate", new_transaction_streaming=BdnServiceModelConfigBase( expire_date=date(2999, 1, 1).isoformat())) self.eth_ws_port = helpers.get_free_port() self.eth_ws_uri = f"ws://127.0.0.1:{self.eth_ws_port}" self.eth_ws_server_message_queue = asyncio.Queue() await self.start_server() gateway_opts = gateway_helpers.get_gateway_opts( 8000, eth_ws_uri=self.eth_ws_uri, ws=True) gateway_opts.set_account_options(account_model) self.gateway_node = MockGatewayNode(gateway_opts) self.gateway_node.transaction_streamer_peer = OutboundPeerModel( "127.0.0.1", 8006, node_type=NodeType.INTERNAL_GATEWAY) self.gateway_node.feed_manager.register_feed( EthPendingTransactionFeed(self.gateway_node.alarm_queue)) self.eth_ws_proxy_publisher = EthWsProxyPublisher( self.eth_ws_uri, self.gateway_node.feed_manager, self.gateway_node.get_tx_service(), self.gateway_node) self.subscriber: Subscriber[ RawTransactionFeedEntry] = self.gateway_node.feed_manager.subscribe_to_feed( EthPendingTransactionFeed.NAME, {}) self.assertIsNotNone(self.subscriber) await self.eth_ws_proxy_publisher.start() await asyncio.sleep(0.01) self.assertEqual(len(self.eth_ws_proxy_publisher.receiving_tasks), 2) self.assertEqual(0, self.subscriber.messages.qsize()) self.sample_transactions = { i: mock_eth_messages.get_dummy_transaction(i) for i in range(10) }
class EthGatewayNode(AbstractGatewayNode): DISCONNECT_REASON_TO_DESCRIPTION = { 0: "Disconnect requested", 1: "TCP sub-system error", 2: "Breach of protocol, e.g. a malformed message or bad RLP", 3: "Useless peer", 4: "Too many peers", 5: "Already connected", 6: "Incompatible P2P protocol version", 7: "Null node identity received - this is automatically invalid", 8: "Client quitting", 9: "Unexpected identity in handshake", 10: "Identity is the same as this node", 11: "Ping timeout", 16: "Some other reason specific to a subprotocol" } DISCONNECT_REASON_TO_INSTRUCTION = { 4: "Remove some peers or add gateway as trusted peer.", 5: "Verify that another instance of the gateway is not already running.", 16: "Verify that '--blockchain-network' and other blockchain network parameters are correct." } def __init__(self, opts, node_ssl_service: NodeSSLService) -> None: super(EthGatewayNode, self).__init__( opts, node_ssl_service, eth_common_constants.TRACKED_BLOCK_CLEANUP_INTERVAL_S) self._node_public_key = None self._remote_public_key = None if opts.node_public_key is not None: self._node_public_key = convert.hex_to_bytes(opts.node_public_key) elif opts.blockchain_peers is None: raise RuntimeError( "128 digit public key must be included with command-line specified blockchain peer." ) if opts.remote_blockchain_peer is not None: if opts.remote_public_key is None: raise RuntimeError( "128 digit public key must be included with command-line specified remote blockchain peer." ) else: self._remote_public_key = convert.hex_to_bytes( opts.remote_public_key) self.block_processing_service: EthBlockProcessingService = EthBlockProcessingService( self) self.block_queuing_service: EthBlockQueuingService = EthBlockQueuingService( self) # List of know total difficulties, tuples of values (block hash, total difficulty) self._last_known_difficulties = deque( maxlen=eth_common_constants.LAST_KNOWN_TOTAL_DIFFICULTIES_MAX_COUNT ) # queue of the block hashes requested from remote blockchain node during sync self._requested_remote_blocks_queue = deque() # number of remote block requests to skip in case if requests and responses got out of sync self._skip_remote_block_requests_stats_count = 0 self.init_eth_gateway_stat_logging() self.init_eth_on_block_feed_stat_logging() self.message_converter = converter_factory.create_eth_message_converter( self.opts) self.eth_ws_proxy_publisher = EthWsProxyPublisher( opts.eth_ws_uri, self.feed_manager, self._tx_service, self) if self.opts.ws and not self.opts.eth_ws_uri: logger.warning(log_messages.ETH_WS_SUBSCRIBER_NOT_STARTED) self.average_gas_price = RunningAverage( gateway_constants.ETH_GAS_RUNNING_AVERAGE_SIZE) logger.info("Gateway enode url: {}", self.get_enode()) def build_blockchain_connection( self, socket_connection: AbstractSocketConnectionProtocol ) -> AbstractGatewayBlockchainConnection: if self._is_in_local_discovery(): return EthNodeDiscoveryConnection(socket_connection, self) else: return EthNodeConnection(socket_connection, self) def build_relay_connection( self, socket_connection: AbstractSocketConnectionProtocol ) -> AbstractRelayConnection: if TestModes.DROPPING_TXS in self.opts.test_mode: cls = EthLossyRelayConnection else: cls = EthRelayConnection relay_connection = cls(socket_connection, self) return relay_connection def build_remote_blockchain_connection( self, socket_connection: AbstractSocketConnectionProtocol ) -> AbstractGatewayBlockchainConnection: return EthRemoteConnection(socket_connection, self) def build_block_queuing_service(self) -> EthBlockQueuingService: return EthBlockQueuingService(self) def build_block_cleanup_service(self) -> AbstractBlockCleanupService: if self.opts.use_extensions: from bxgateway.services.eth.eth_extension_block_cleanup_service import EthExtensionBlockCleanupService return EthExtensionBlockCleanupService(self, self.network_num) else: return EthNormalBlockCleanupService(self, self.network_num) def on_updated_remote_blockchain_peer(self, outbound_peer) -> int: if "node_public_key" not in outbound_peer.attributes: logger.warning(log_messages.BLOCKCHAIN_PEER_LACKS_PUBLIC_KEY) return constants.SDN_CONTACT_RETRY_SECONDS else: super(EthGatewayNode, self).on_updated_remote_blockchain_peer(outbound_peer) self._remote_public_key = convert.hex_to_bytes( outbound_peer.attributes["node_public_key"]) return constants.CANCEL_ALARMS def get_outbound_peer_info(self) -> List[ConnectionPeerInfo]: peers = [] for peer in self.opts.outbound_peers: peers.append( ConnectionPeerInfo( IpEndpoint(peer.ip, peer.port), convert.peer_node_to_connection_type( self.NODE_TYPE, peer.node_type))) local_protocol = TransportLayerProtocol.UDP if self._is_in_local_discovery( ) else TransportLayerProtocol.TCP for blockchain_peer in self.blockchain_peers: peers.append( ConnectionPeerInfo( IpEndpoint(blockchain_peer.ip, blockchain_peer.port), ConnectionType.BLOCKCHAIN_NODE, local_protocol)) if self.remote_blockchain_ip is not None and self.remote_blockchain_port is not None: remote_protocol = TransportLayerProtocol.UDP if self._is_in_remote_discovery() else \ TransportLayerProtocol.TCP peers.append( ConnectionPeerInfo( # pyre-fixme[6]: Expected `str` for 1st param but got `Optional[str]`. IpEndpoint(self.remote_blockchain_ip, self.remote_blockchain_port), ConnectionType.REMOTE_BLOCKCHAIN_NODE, remote_protocol)) return peers def get_private_key(self) -> bytes: return convert.hex_to_bytes(self.opts.private_key) def get_public_key(self) -> bytes: return crypto_utils.private_to_public_key(self.get_private_key()) def set_node_public_key(self, discovery_connection, node_public_key) -> None: if not isinstance(discovery_connection, EthNodeDiscoveryConnection): raise TypeError( "Argument discovery_connection is expected to be of type EthNodeDiscoveryConnection, was {}" .format(type(discovery_connection))) if not node_public_key: raise ValueError("node_public_key argument is required") self._node_public_key = node_public_key # close UDP connection discovery_connection.mark_for_close(False) # establish TCP connection self.enqueue_connection(self.opts.blockchain_ip, self.opts.blockchain_port, ConnectionType.BLOCKCHAIN_NODE) def set_remote_public_key(self, discovery_connection, remote_public_key) -> None: if not isinstance(discovery_connection, EthNodeDiscoveryConnection): raise TypeError( "Argument discovery_connection is expected to be of type EthNodeDiscoveryConnection, was {}" .format(type(discovery_connection))) if not remote_public_key: raise ValueError("remote_public_key argument is required") self._remote_public_key = remote_public_key # close UDP connection discovery_connection.mark_for_close(False) remote_blockchain_ip = self.remote_blockchain_ip remote_blockchain_port = self.remote_blockchain_port assert remote_blockchain_ip is not None and remote_blockchain_port is not None # establish TCP connection self.enqueue_connection(remote_blockchain_ip, remote_blockchain_port, ConnectionType.REMOTE_BLOCKCHAIN_NODE) def get_node_public_key(self, ip: str, port: int) -> bytes: node_public_key = None for blockchain_peer in self.blockchain_peers: if blockchain_peer.ip == ip and blockchain_peer.port == port: node_public_key = blockchain_peer.node_public_key break if not node_public_key: raise RuntimeError( f"128 digit public key must be included with for blockchain peer ip {ip} and port {port}." ) return convert.hex_to_bytes(node_public_key) def get_remote_public_key(self) -> bytes: return self._remote_public_key def set_known_total_difficulty(self, block_hash: Sha256Hash, total_difficulty: int) -> None: self._last_known_difficulties.append((block_hash, total_difficulty)) def try_calculate_total_difficulty( self, block_hash: Sha256Hash, new_block_parts: NewBlockParts) -> Optional[int]: previous_block_hash = new_block_parts.get_previous_block_hash() previous_block_total_difficulty = None for known_block_hash, known_total_difficulty in self._last_known_difficulties: if previous_block_hash == known_block_hash: previous_block_total_difficulty = known_total_difficulty break if previous_block_total_difficulty is None: logger.debug( "Unable to calculate total difficulty after block {}.", convert.bytes_to_hex(block_hash.binary)) return None block_total_difficulty = previous_block_total_difficulty + new_block_parts.get_block_difficulty( ) self._last_known_difficulties.append( (block_hash, block_total_difficulty)) logger.debug("Calculated total difficulty after block {} = {}.", convert.bytes_to_hex(block_hash.binary), block_total_difficulty) return block_total_difficulty def log_requested_remote_blocks(self, block_hashes: List[Sha256Hash]) -> None: if self._skip_remote_block_requests_stats_count > 0: self._skip_remote_block_requests_stats_count -= 1 else: for block_hash in block_hashes: block_stats.add_block_event_by_block_hash( block_hash, BlockStatEventType.REMOTE_BLOCK_REQUESTED_BY_GATEWAY, network_num=self.network_num, more_info=f"Protocol: {self.opts.blockchain_protocol}, " f"Network: {self.opts.blockchain_network}") self._requested_remote_blocks_queue.append(block_hashes) def log_received_remote_blocks(self, blocks_count: int) -> None: if len(self._requested_remote_blocks_queue) > 0: expected_blocks = self._requested_remote_blocks_queue.pop() if len(expected_blocks) != blocks_count: logger.warning(log_messages.BLOCK_COUNT_MISMATCH, blocks_count, len(expected_blocks)) self._skip_remote_block_requests_stats_count = len( self._requested_remote_blocks_queue) * 2 self._requested_remote_blocks_queue.clear() return for block_hash in expected_blocks: block_stats.add_block_event_by_block_hash( block_hash, BlockStatEventType.REMOTE_BLOCK_RECEIVED_BY_GATEWAY, network_num=self.network_num, more_info="Protocol: {}, Network: {}".format( self.opts.blockchain_protocol, self.opts.blockchain_network)) else: logger.warning(log_messages.UNEXPECTED_BLOCKS) def log_closed_connection(self, connection: AbstractConnection) -> None: if isinstance(connection, EthNodeConnection): # pyre-fixme[22]: The cast is redundant. eth_node_connection = cast(EthNodeConnection, connection) connection_status = connection.connection_protocol.connection_status if ConnectionState.INITIALIZED not in eth_node_connection.state: logger.info( "Failed to connect to Ethereum node. Verify that provided ip address ({}) and port ({}) " "are correct. Verify that firewall port is open. Connection details: {}.", eth_node_connection.peer_ip, eth_node_connection.peer_port, eth_node_connection) elif connection_status.disconnect_message_received: logger.info( "Connection to Ethereum node failed. Disconnect reason: '{}'. {} Connection details: {}.", # pyre-fixme[6]: Expected `int` for 1st param but got # `Optional[int]`. self._get_disconnect_reason_description( connection_status.disconnect_reason), # pyre-fixme[6]: Expected `int` for 1st param but got # `Optional[int]`. self._get_disconnect_reason_instruction( connection_status.disconnect_reason), connection) elif not connection_status.auth_message_received and not connection_status.auth_ack_message_received: logger.info( "Failed to connect to Ethereum node. Verify that '--node-public-key' argument is provided " "and value matches enode of the Ethereum node. Connection details: {}.", eth_node_connection) elif connection_status.hello_message_received and connection_status.status_message_sent and \ not connection_status.status_message_received: logger.info( "Failed to connect to Ethereum node. Verify that '--blockchain-network' and other " "blockchain network arguments are correct. Connection details: {}.", eth_node_connection) else: super(EthGatewayNode, self).log_closed_connection(connection) elif isinstance(connection, GatewayConnection): if ConnectionState.ESTABLISHED not in connection.state: logger.debug("Failed to connect to: {}.", connection) else: logger.debug("Closed connection: {}", connection) else: super(EthGatewayNode, self).log_closed_connection(connection) def init_eth_gateway_stat_logging(self) -> None: eth_gateway_stats_service.set_node(self) self.alarm_queue.register_alarm(eth_gateway_stats_service.interval, eth_gateway_stats_service.flush_info) def init_eth_on_block_feed_stat_logging(self) -> None: eth_on_block_feed_stats_service.set_node(self) self.alarm_queue.register_alarm( eth_on_block_feed_stats_service.interval, eth_on_block_feed_stats_service.flush_info) def init_live_feeds(self) -> None: self.feed_manager.register_feed(EthNewTransactionFeed()) self.feed_manager.register_feed( EthPendingTransactionFeed(self.alarm_queue)) self.feed_manager.register_feed(EthOnBlockFeed(self)) self.feed_manager.register_feed(EthNewBlockFeed(self)) def on_new_subscriber_request(self) -> None: if self.opts.eth_ws_uri and not self.eth_ws_proxy_publisher.running: asyncio.create_task(self.eth_ws_proxy_publisher.revive()) def on_transactions_in_block(self, transactions: List[Transaction]) -> None: for transaction in transactions: self.average_gas_price.add_value(transaction.gas_price) def broadcast_transactions_to_node( self, msg: AbstractMessage, broadcasting_conn: Optional[AbstractConnection]) -> bool: msg = cast(TransactionsEthProtocolMessage, msg) if self.opts.filter_txs_factor > 0: assert len(msg.get_transactions()) == 1 transaction = msg.get_transactions()[0] if (float(transaction.gas_price) < self.average_gas_price.average * self.opts.filter_txs_factor): logger.trace( "Skipping sending transaction {} with gas price: {}. Average was {}", transaction.hash(), float(transaction.gas_price), self.average_gas_price.average) return False return super().broadcast_transactions_to_node(msg, broadcasting_conn) def get_enode(self) -> str: return \ f"enode://{convert.bytes_to_hex(self.get_public_key())}@{self.opts.external_ip}:{self.opts.non_ssl_port}" def on_block_received_from_bdn( self, block_hash: Sha256Hash, block_message: AbstractBlockMessage) -> None: super().on_block_received_from_bdn(block_hash, block_message) node_conn = cast(EthNodeConnection, self.get_any_active_blockchain_connection()) if node_conn is not None: eth_connection_protocol = node_conn.connection_protocol eth_connection_protocol.pending_new_block_parts.remove_item( block_hash) # pyre-fixme[14]: Inconsistent override: # Parameter of type InternalEthBlockInfo is not a supertype of AbstractBlockMessage def publish_block(self, block_number: Optional[int], block_hash: Sha256Hash, block_message: Optional[InternalEthBlockInfo], source: FeedSource) -> None: if block_number is None and block_message is not None: block_number = block_message.block_number() logger.debug( "Handle block notification for feed. Number: {}, Hash: {} Msg: {} From: {}", block_number, block_hash, block_message, source) if block_number and self.opts.ws: raw_block = EthRawBlock( block_number, block_hash, source, self._get_block_message_lazy(block_message, block_hash)) self._publish_block_to_on_block_feed(raw_block) self._publish_block_to_new_block_feed(raw_block) async def init(self) -> None: await super().init() try: await asyncio.wait_for(self.eth_ws_proxy_publisher.start(), rpc_constants.RPC_SERVER_INIT_TIMEOUT_S) except Exception as e: logger.error(log_messages.ETH_WS_INITIALIZATION_FAIL, e) self.should_force_exit = True async def close(self) -> None: try: await asyncio.wait_for(self.eth_ws_proxy_publisher.stop(), rpc_constants.RPC_SERVER_STOP_TIMEOUT_S) except Exception as e: logger.error(log_messages.ETH_WS_CLOSE_FAIL, e, exc_info=True) await super().close() def _is_in_local_discovery(self) -> bool: return not self.opts.no_discovery and self._node_public_key is None def _is_in_remote_discovery(self) -> bool: return not self.opts.no_discovery and self._remote_public_key is None def _get_disconnect_reason_description(self, reason: int) -> str: if reason not in self.DISCONNECT_REASON_TO_DESCRIPTION: return f"Disconnect reason ({reason}) is unknown." return self.DISCONNECT_REASON_TO_DESCRIPTION[reason] def _get_disconnect_reason_instruction(self, reason: int) -> str: if reason not in self.DISCONNECT_REASON_TO_INSTRUCTION: return "" return self.DISCONNECT_REASON_TO_INSTRUCTION[reason] def _get_block_message_lazy(self, block_message: Optional[InternalEthBlockInfo], block_hash) -> Iterator[InternalEthBlockInfo]: if block_message: yield block_message else: block_parts = self.block_queuing_service.get_block_parts( block_hash) if block_parts: block_message = InternalEthBlockInfo.from_new_block_parts( block_parts) assert block_message is not None yield block_message def _publish_block_to_new_block_feed( self, raw_block: EthRawBlock, ) -> None: self.feed_manager.publish_to_feed(EthNewBlockFeed.NAME, raw_block) def _publish_block_to_on_block_feed( self, raw_block: EthRawBlock, ) -> None: if raw_block.source in {FeedSource.BLOCKCHAIN_RPC}: self.feed_manager.publish_to_feed( EthOnBlockFeed.NAME, EventNotification(raw_block.block_number)) def is_gas_price_above_min_network_fee( self, transaction_contents: Union[memoryview, bytearray]) -> bool: gas_price = eth_common_utils.raw_tx_gas_price( memoryview(transaction_contents), 0) if gas_price >= self.get_blockchain_network().min_tx_network_fee: return True return False