From 9327097ea71bdb51f91abd60f37c33dd429d029d Mon Sep 17 00:00:00 2001 From: cyc60 Date: Fri, 16 Aug 2024 15:30:37 +0300 Subject: [PATCH 1/5] Specify finalized head state --- pyproject.toml | 2 +- sw_utils/consensus.py | 9 ++++++--- sw_utils/typings.py | 1 + 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b4218df..de097da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "sw-utils" -version = "0.6.20" +version = "0.6.21" description = "StakeWise Python utils" authors = ["StakeWise Labs "] license = "GPL-3.0-or-later" diff --git a/sw_utils/consensus.py b/sw_utils/consensus.py index f42ad37..e85ddba 100644 --- a/sw_utils/consensus.py +++ b/sw_utils/consensus.py @@ -14,7 +14,7 @@ from sw_utils.common import urljoin from sw_utils.decorators import can_be_retried_aiohttp_error, retry_aiohttp_errors -from sw_utils.typings import ChainHead, ConsensusFork, Finality +from sw_utils.typings import ChainHead, ConsensusFork, Finality, State logger = logging.getLogger(__name__) @@ -206,10 +206,13 @@ def get_consensus_client( async def get_chain_finalized_head( - consensus_client: ExtendedAsyncBeacon, slots_per_epoch: int, finality: Finality = 'finalized' + consensus_client: ExtendedAsyncBeacon, + slots_per_epoch: int, + finality: Finality = 'finalized', + state: State = 'finalized', ) -> ChainHead: """Fetches the fork safe chain head.""" - checkpoints = await consensus_client.get_finality_checkpoint() + checkpoints = await consensus_client.get_finality_checkpoint(state) epoch: int = int(checkpoints['data'][finality]['epoch']) last_slot_id: int = (epoch * slots_per_epoch) + slots_per_epoch - 1 for i in range(slots_per_epoch): diff --git a/sw_utils/typings.py b/sw_utils/typings.py index fface16..501540a 100644 --- a/sw_utils/typings.py +++ b/sw_utils/typings.py @@ -9,6 +9,7 @@ Bytes32 = NewType('Bytes32', bytes) Finality: TypeAlias = Literal['finalized', 'current_justified', 'previous_justified'] +State: TypeAlias = Literal['genesis', 'finalized', 'justified'] | str @dataclass From 678558a08217559cf59198a0bd4ebb1e165897a9 Mon Sep 17 00:00:00 2001 From: cyc60 Date: Wed, 21 Aug 2024 15:54:46 +0300 Subject: [PATCH 2/5] Use finalized checkpoints --- sw_utils/consensus.py | 21 ++++++++++----------- sw_utils/typings.py | 13 ++++++++----- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/sw_utils/consensus.py b/sw_utils/consensus.py index e85ddba..8345ccc 100644 --- a/sw_utils/consensus.py +++ b/sw_utils/consensus.py @@ -14,7 +14,7 @@ from sw_utils.common import urljoin from sw_utils.decorators import can_be_retried_aiohttp_error, retry_aiohttp_errors -from sw_utils.typings import ChainHead, ConsensusFork, Finality, State +from sw_utils.typings import ChainHead, ConsensusFork, Finality logger = logging.getLogger(__name__) @@ -209,13 +209,12 @@ async def get_chain_finalized_head( consensus_client: ExtendedAsyncBeacon, slots_per_epoch: int, finality: Finality = 'finalized', - state: State = 'finalized', ) -> ChainHead: """Fetches the fork safe chain head.""" - checkpoints = await consensus_client.get_finality_checkpoint(state) + checkpoints = await consensus_client.get_finality_checkpoint() epoch: int = int(checkpoints['data'][finality]['epoch']) - last_slot_id: int = (epoch * slots_per_epoch) + slots_per_epoch - 1 - for i in range(slots_per_epoch): + last_slot_id: int = epoch * slots_per_epoch + for i in range(slots_per_epoch + 1): try: slot = await consensus_client.get_block(str(last_slot_id - i)) except ClientResponseError as e: @@ -227,8 +226,8 @@ async def get_chain_finalized_head( execution_payload = slot['data']['message']['body']['execution_payload'] return ChainHead( epoch=epoch, - consensus_block=last_slot_id - i, - execution_block=BlockNumber(int(execution_payload['block_number'])), + slot=last_slot_id - i, + block_number=BlockNumber(int(execution_payload['block_number'])), execution_ts=Timestamp(int(execution_payload['timestamp'])), ) @@ -255,8 +254,8 @@ async def get_chain_epoch_head( execution_payload = slot['data']['message']['body']['execution_payload'] return ChainHead( epoch=epoch, - consensus_block=slot_id - i, - execution_block=BlockNumber(int(execution_payload['block_number'])), + slot=slot_id - i, + block_number=BlockNumber(int(execution_payload['block_number'])), execution_ts=Timestamp(int(execution_payload['timestamp'])), ) except KeyError: # pre shapella slot @@ -268,8 +267,8 @@ async def get_chain_epoch_head( return ChainHead( epoch=epoch, - consensus_block=slot_id - i, - execution_block=BlockNumber(int(block['number'])), + slot=slot_id - i, + block_number=BlockNumber(int(block['number'])), execution_ts=Timestamp(int(block['timestamp'])), ) diff --git a/sw_utils/typings.py b/sw_utils/typings.py index 501540a..fdd6040 100644 --- a/sw_utils/typings.py +++ b/sw_utils/typings.py @@ -9,7 +9,6 @@ Bytes32 = NewType('Bytes32', bytes) Finality: TypeAlias = Literal['finalized', 'current_justified', 'previous_justified'] -State: TypeAlias = Literal['genesis', 'finalized', 'justified'] | str @dataclass @@ -21,13 +20,17 @@ class ConsensusFork: @dataclass class ChainHead: epoch: int - consensus_block: int - execution_block: BlockNumber + slot: int + block_number: BlockNumber execution_ts: Timestamp @property - def slot(self) -> int: - return self.consensus_block + def consensus_block(self) -> int: + return self.slot + + @property + def execution_block(self) -> BlockNumber: + return self.block_number @dataclass From fc132582d7feb6277cc65186d0565348e15de270 Mon Sep 17 00:00:00 2001 From: cyc60 Date: Wed, 21 Aug 2024 16:06:11 +0300 Subject: [PATCH 3/5] Version bump --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5cf97a2..c0a43a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "sw-utils" -version = "v0.6.20" +version = "v0.6.21" description = "StakeWise Python utils" authors = ["StakeWise Labs "] license = "GPL-3.0-or-later" From d45d91d092fbd58191b7f6ce04be62120882506d Mon Sep 17 00:00:00 2001 From: cyc60 Date: Thu, 22 Aug 2024 16:56:22 +0300 Subject: [PATCH 4/5] Use consensus_client get_block for chain head --- sw_utils/consensus.py | 89 +++++++++++++++++-------------------------- sw_utils/typings.py | 1 + 2 files changed, 35 insertions(+), 55 deletions(-) diff --git a/sw_utils/consensus.py b/sw_utils/consensus.py index 8345ccc..29af785 100644 --- a/sw_utils/consensus.py +++ b/sw_utils/consensus.py @@ -3,18 +3,16 @@ from typing import TYPE_CHECKING, Any, Sequence import aiohttp -from aiohttp import ClientResponseError from eth_typing import URI, BlockNumber, HexStr from web3 import AsyncWeb3, Web3 from web3._utils.request import async_json_make_get_request from web3.beacon import AsyncBeacon from web3.beacon.api_endpoints import GET_VOLUNTARY_EXITS -from web3.exceptions import BlockNotFound from web3.types import Timestamp from sw_utils.common import urljoin from sw_utils.decorators import can_be_retried_aiohttp_error, retry_aiohttp_errors -from sw_utils.typings import ChainHead, ConsensusFork, Finality +from sw_utils.typings import ChainHead, ConsensusFork, State logger = logging.getLogger(__name__) @@ -208,30 +206,22 @@ def get_consensus_client( async def get_chain_finalized_head( consensus_client: ExtendedAsyncBeacon, slots_per_epoch: int, - finality: Finality = 'finalized', + state: State = 'finalized', ) -> ChainHead: """Fetches the fork safe chain head.""" - checkpoints = await consensus_client.get_finality_checkpoint() - epoch: int = int(checkpoints['data'][finality]['epoch']) - last_slot_id: int = epoch * slots_per_epoch - for i in range(slots_per_epoch + 1): - try: - slot = await consensus_client.get_block(str(last_slot_id - i)) - except ClientResponseError as e: - if hasattr(e, 'status') and e.status == 404: - # slot was not proposed, try the previous one - continue - raise e - - execution_payload = slot['data']['message']['body']['execution_payload'] - return ChainHead( - epoch=epoch, - slot=last_slot_id - i, - block_number=BlockNumber(int(execution_payload['block_number'])), - execution_ts=Timestamp(int(execution_payload['timestamp'])), - ) - - raise RuntimeError(f'Failed to fetch slot for epoch {epoch}') + block_data = await consensus_client.get_block(state) + slot = int(block_data['data']['message']['slot']) + + return ChainHead( + epoch=slot // slots_per_epoch, + slot=slot, + block_number=BlockNumber( + int(block_data['data']['message']['body']['execution_payload']['block_number']) + ), + execution_ts=Timestamp( + int(block_data['data']['message']['body']['execution_payload']['timestamp']) + ), + ) async def get_chain_epoch_head( @@ -242,34 +232,23 @@ async def get_chain_epoch_head( ) -> ChainHead: """Fetches the epoch chain head.""" slot_id: int = (epoch * slots_per_epoch) + slots_per_epoch - 1 - for i in range(slots_per_epoch): - try: - slot = await consensus_client.get_block(str(slot_id - i)) - except ClientResponseError as e: - if hasattr(e, 'status') and e.status == 404: - # slot was not proposed, try the previous one - continue - raise e - try: - execution_payload = slot['data']['message']['body']['execution_payload'] - return ChainHead( - epoch=epoch, - slot=slot_id - i, - block_number=BlockNumber(int(execution_payload['block_number'])), - execution_ts=Timestamp(int(execution_payload['timestamp'])), - ) - except KeyError: # pre shapella slot - block_hash = slot['data']['message']['body']['eth1_data']['block_hash'] - try: - block = await execution_client.eth.get_block(block_hash) - except BlockNotFound: - continue - - return ChainHead( - epoch=epoch, - slot=slot_id - i, - block_number=BlockNumber(int(block['number'])), - execution_ts=Timestamp(int(block['timestamp'])), - ) + block_data = await consensus_client.get_block(str(slot_id)) + slot = int(block_data['data']['message']['slot']) - raise RuntimeError(f'Failed to fetch slot for epoch {epoch}') + try: + execution_payload = block_data['data']['message']['body']['execution_payload'] + return ChainHead( + epoch=epoch, + slot=slot, + block_number=BlockNumber(int(execution_payload['block_number'])), + execution_ts=Timestamp(int(execution_payload['timestamp'])), + ) + except KeyError: # pre shapella slot + block_hash = block_data['data']['message']['body']['eth1_data']['block_hash'] + block = await execution_client.eth.get_block(block_hash) + return ChainHead( + epoch=epoch, + slot=slot_id, + block_number=BlockNumber(int(block['number'])), + execution_ts=Timestamp(int(block['timestamp'])), + ) diff --git a/sw_utils/typings.py b/sw_utils/typings.py index fdd6040..f827e6a 100644 --- a/sw_utils/typings.py +++ b/sw_utils/typings.py @@ -9,6 +9,7 @@ Bytes32 = NewType('Bytes32', bytes) Finality: TypeAlias = Literal['finalized', 'current_justified', 'previous_justified'] +State: TypeAlias = Literal['genesis', 'finalized', 'justified'] | str @dataclass From 89c67128410572d8b12247de863a96f846424d34 Mon Sep 17 00:00:00 2001 From: cyc60 Date: Thu, 22 Aug 2024 18:04:42 +0300 Subject: [PATCH 5/5] Review fixes --- sw_utils/consensus.py | 55 ++++++++++++++++++++++++++----------------- sw_utils/typings.py | 1 - 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/sw_utils/consensus.py b/sw_utils/consensus.py index 29af785..2c671f8 100644 --- a/sw_utils/consensus.py +++ b/sw_utils/consensus.py @@ -3,16 +3,18 @@ from typing import TYPE_CHECKING, Any, Sequence import aiohttp +from aiohttp import ClientResponseError from eth_typing import URI, BlockNumber, HexStr from web3 import AsyncWeb3, Web3 from web3._utils.request import async_json_make_get_request from web3.beacon import AsyncBeacon from web3.beacon.api_endpoints import GET_VOLUNTARY_EXITS +from web3.exceptions import BlockNotFound from web3.types import Timestamp from sw_utils.common import urljoin from sw_utils.decorators import can_be_retried_aiohttp_error, retry_aiohttp_errors -from sw_utils.typings import ChainHead, ConsensusFork, State +from sw_utils.typings import ChainHead, ConsensusFork logger = logging.getLogger(__name__) @@ -206,7 +208,7 @@ def get_consensus_client( async def get_chain_finalized_head( consensus_client: ExtendedAsyncBeacon, slots_per_epoch: int, - state: State = 'finalized', + state: str = 'finalized', ) -> ChainHead: """Fetches the fork safe chain head.""" block_data = await consensus_client.get_block(state) @@ -232,23 +234,34 @@ async def get_chain_epoch_head( ) -> ChainHead: """Fetches the epoch chain head.""" slot_id: int = (epoch * slots_per_epoch) + slots_per_epoch - 1 - block_data = await consensus_client.get_block(str(slot_id)) - slot = int(block_data['data']['message']['slot']) + for i in range(slots_per_epoch): + try: + slot = await consensus_client.get_block(str(slot_id - i)) + except ClientResponseError as e: + if hasattr(e, 'status') and e.status == 404: + # slot was not proposed, try the previous one + continue + raise e + try: + execution_payload = slot['data']['message']['body']['execution_payload'] + return ChainHead( + epoch=epoch, + slot=slot_id - i, + block_number=BlockNumber(int(execution_payload['block_number'])), + execution_ts=Timestamp(int(execution_payload['timestamp'])), + ) + except KeyError: # pre shapella slot + block_hash = slot['data']['message']['body']['eth1_data']['block_hash'] + try: + block = await execution_client.eth.get_block(block_hash) + except BlockNotFound: + continue + + return ChainHead( + epoch=epoch, + slot=slot_id - i, + block_number=BlockNumber(int(block['number'])), + execution_ts=Timestamp(int(block['timestamp'])), + ) - try: - execution_payload = block_data['data']['message']['body']['execution_payload'] - return ChainHead( - epoch=epoch, - slot=slot, - block_number=BlockNumber(int(execution_payload['block_number'])), - execution_ts=Timestamp(int(execution_payload['timestamp'])), - ) - except KeyError: # pre shapella slot - block_hash = block_data['data']['message']['body']['eth1_data']['block_hash'] - block = await execution_client.eth.get_block(block_hash) - return ChainHead( - epoch=epoch, - slot=slot_id, - block_number=BlockNumber(int(block['number'])), - execution_ts=Timestamp(int(block['timestamp'])), - ) + raise RuntimeError(f'Failed to fetch slot for epoch {epoch}') diff --git a/sw_utils/typings.py b/sw_utils/typings.py index f827e6a..fdd6040 100644 --- a/sw_utils/typings.py +++ b/sw_utils/typings.py @@ -9,7 +9,6 @@ Bytes32 = NewType('Bytes32', bytes) Finality: TypeAlias = Literal['finalized', 'current_justified', 'previous_justified'] -State: TypeAlias = Literal['genesis', 'finalized', 'justified'] | str @dataclass