From bb440205f83b85e4a70b640de52691f817f3ea79 Mon Sep 17 00:00:00 2001 From: Tim Olson <> Date: Wed, 10 Jan 2024 16:50:49 -0400 Subject: [PATCH] runner workarounds for hardhat --- src/dexorder/database/model/block.py | 3 +- src/dexorder/runner.py | 60 ++++++++++++++++++---------- 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/src/dexorder/database/model/block.py b/src/dexorder/database/model/block.py index caecca3..c280053 100644 --- a/src/dexorder/database/model/block.py +++ b/src/dexorder/database/model/block.py @@ -16,8 +16,9 @@ class Block(Base): @property def timestamp(self) -> int: + raw = self.data['timestamp'] # noinspection PyTypeChecker - return hexint(self.data['timestamp']) + return raw if type(raw) is int else hexint(raw) def __str__(self): return f'{self.height}_{self.hash.hex()[:5]}' diff --git a/src/dexorder/runner.py b/src/dexorder/runner.py index cf12812..41da3cb 100644 --- a/src/dexorder/runner.py +++ b/src/dexorder/runner.py @@ -150,15 +150,20 @@ class BlockStateRunner: while self.running: try: - new_blocks = await w3.eth.filter("latest") - for head in await new_blocks.get_new_entries(): - if head != prev_blockhash: - prev_blockhash = head - await self.queue.put(head) - log.debug(f'polled new block {hexstr(head)}') - if not self.running: - break - await asyncio.sleep(config.polling) + # polling mode is used primarily because Hardhat fails to deliver newHeads events after about an hour + # unfortunately, hardhat also stops responding to eth_getBlockByHash. so instead, we use the standard (stupid) + # 'latest' polling for blocks, and we push the entire block to the queue since apparently this is the only + # rpc call Hardhat seems to consistently support. The worker must then detect the type of object pushed to the + # work queue and either use the block directly or query for the block if the queue object is a hashcode. + block = await w3.eth.get_block('latest') + head = block['hash'] + if head != prev_blockhash: + prev_blockhash = head + await self.queue.put(block) + log.debug(f'polled new block {hexstr(head)}') + if not self.running: + break + await asyncio.sleep(config.polling) except ConnectionClosedError: pass finally: @@ -187,7 +192,7 @@ class BlockStateRunner: except TimeoutError: # 1 second has passed without a new head. Run the postprocess callbacks to check for activated time-based triggers if prev_head is not None: - await self.handle_time_tick(head) + await self.handle_time_tick(prev_head) else: try: await self.handle_head(chain, head, w3) @@ -202,21 +207,26 @@ class BlockStateRunner: async def handle_head(self, chain, blockhash, w3): - log.debug(f'processing block {hexstr(blockhash)}') + # check blockhash type and convert + try: + block_data = blockhash + blockhash = block_data['hash'] + parent = block_data['parentHash'] + height = block_data['number'] + except TypeError: + response = await w3.provider.make_request('eth_getBlockByHash', [blockhash, False]) + block_data:dict = response['result'] + parent = bytes.fromhex(block_data['parentHash'][2:]) + height = int(block_data['number'], 0) + log.debug(f'processing block {blockhash}') chain_id = chain.chain_id session = None try: - blockhash = hexstr(blockhash) if self.state is not None and blockhash in self.state.by_hash: + log.debug(f'block {blockhash} was already processed') return - # block_data = await w3.eth.get_block(head['hash'], True) - response = await w3.provider.make_request('eth_getBlockByHash', [blockhash, False]) - block_data = response['result'] - if block_data is None: - log.warning(f'block data for {blockhash} was None') - return # todo get block when hardhat stops responding to getBlockByHash - block = Block(chain=chain_id, height=int(block_data['number'], 0), - hash=bytes.fromhex(block_data['hash'][2:]), parent=bytes.fromhex(block_data['parentHash'][2:]), data=block_data) + assert block_data is not None + block = Block(chain=chain_id, height=height, hash=blockhash, parent=parent, data=block_data) latest_block.set(block) current_clock.get().set(block.timestamp) if self.state is None: @@ -329,8 +339,16 @@ class BlockStateRunner: async def handle_time_tick(self, blockhash): if current_blockstate.get() is None: return + try: + blockhash = blockhash['hash'] + except TypeError: + pass # similar to handle_head, but we only call the postprocess events, since there was only a time tick and no new block data - block = self.state.by_hash[blockhash] + try: + block = self.state.by_hash[blockhash] + except KeyError: + log.warning(f'No block data for {blockhash}. Aborting time tick.') + return fork = self.state.fork(block) current_block.set(block) current_fork.set(fork)