diff --git a/poetry.lock b/poetry.lock index 7d6ad8ff0..ab54b3ea5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -856,6 +856,29 @@ files = [ {file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"}, ] +[[package]] +name = "deepdiff" +version = "8.5.0" +description = "Deep Difference and Search of any Python object/data. Recreate objects by adding adding deltas to each other." +optional = false +python-versions = ">=3.9" +files = [ + {file = "deepdiff-8.5.0-py3-none-any.whl", hash = "sha256:d4599db637f36a1c285f5fdfc2cd8d38bde8d8be8636b65ab5e425b67c54df26"}, + {file = "deepdiff-8.5.0.tar.gz", hash = "sha256:a4dd3529fa8d4cd5b9cbb6e3ea9c95997eaa919ba37dac3966c1b8f872dc1cd1"}, +] + +[package.dependencies] +orderly-set = ">=5.4.1,<6" + +[package.extras] +cli = ["click (>=8.1.0,<8.2.0)", "pyyaml (>=6.0.0,<6.1.0)"] +coverage = ["coverage (>=7.6.0,<7.7.0)"] +dev = ["bump2version (>=1.0.0,<1.1.0)", "ipdb (>=0.13.0,<0.14.0)", "jsonpickle (>=4.0.0,<4.1.0)", "nox (==2025.5.1)", "numpy (>=2.0,<3.0)", "numpy (>=2.2.0,<2.3.0)", "orjson (>=3.10.0,<3.11.0)", "pandas (>=2.2.0,<2.3.0)", "polars (>=1.21.0,<1.22.0)", "python-dateutil (>=2.9.0,<2.10.0)", "tomli (>=2.2.0,<2.3.0)", "tomli-w (>=1.2.0,<1.3.0)"] +docs = ["Sphinx (>=6.2.0,<6.3.0)", "sphinx-sitemap (>=2.6.0,<2.7.0)", "sphinxemoji (>=0.3.0,<0.4.0)"] +optimize = ["orjson"] +static = ["flake8 (>=7.1.0,<7.2.0)", "flake8-pyproject (>=1.2.3,<1.3.0)", "pydantic (>=2.10.0,<2.11.0)"] +test = ["pytest (>=8.3.0,<8.4.0)", "pytest-benchmark (>=5.1.0,<5.2.0)", "pytest-cov (>=6.0.0,<6.1.0)", "python-dotenv (>=1.0.0,<1.1.0)"] + [[package]] name = "dill" version = "0.4.0" @@ -1813,6 +1836,17 @@ files = [ {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] +[[package]] +name = "orderly-set" +version = "5.4.1" +description = "Orderly set" +optional = false +python-versions = ">=3.8" +files = [ + {file = "orderly_set-5.4.1-py3-none-any.whl", hash = "sha256:b5e21d21680bd9ef456885db800c5cb4f76a03879880c0175e1b077fb166fd83"}, + {file = "orderly_set-5.4.1.tar.gz", hash = "sha256:a1fb5a4fdc5e234e9e8d8e5c1bbdbc4540f4dfe50d12bf17c8bc5dbf1c9c878d"}, +] + [[package]] name = "oz-merkle-tree" version = "0.1.0" @@ -3212,4 +3246,4 @@ propcache = ">=0.2.1" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "bf7db38e210a00035d54a5f082f8d28f8af28fa70e248a4a0788ea8cc6a8e2dd" +content-hash = "8e648f9fe72065ce44ea1ff52fa873ffc611b2b385f89809e93b17420630a02b" diff --git a/pyproject.toml b/pyproject.toml index 94014e09b..01da5a1c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ json-stream = "^2.3.2" oz-merkle-tree = { git = "https://github.com/lidofinance/oz-merkle-tree" } py-multiformats-cid = "^0.4.4" conventional-pre-commit = "^4.0.0" +deepdiff = "^8.5.0" [tool.poetry.group.dev.dependencies] base58 = "^2.1.1" diff --git a/src/main.py b/src/main.py index a38541ce8..52bbbcebf 100644 --- a/src/main.py +++ b/src/main.py @@ -1,31 +1,44 @@ +import argparse +import re import sys -from typing import Iterator, cast +from dataclasses import asdict +from typing import Any, Dict, Iterator, Optional, cast +from deepdiff import DeepDiff +from hexbytes import HexBytes from packaging.version import Version from prometheus_client import start_http_server -from src import constants -from src import variables +from src import constants, variables from src.metrics.healthcheck_server import start_pulse_server from src.metrics.logging import logging -from src.metrics.prometheus.basic import ENV_VARIABLES_INFO, BUILD_INFO +from src.metrics.prometheus.basic import BUILD_INFO, ENV_VARIABLES_INFO from src.modules.accounting.accounting import Accounting -from src.modules.checks.checks_module import ChecksModule +from src.modules.checks.checks_module import execute_checks from src.modules.csm.csm import CSOracle from src.modules.ejector.ejector import Ejector -from src.providers.ipfs import GW3, IPFSProvider, Kubo, MultiIPFSProvider, Pinata, PublicIPFS -from src.types import OracleModule +from src.providers.execution.contracts.base_oracle import BaseOracleContract +from src.providers.ipfs import ( + GW3, + IPFSProvider, + Kubo, + MultiIPFSProvider, + Pinata, + PublicIPFS, +) +from src.types import BlockRoot, OracleModule, SlotNumber +from src.utils.blockstamp import build_blockstamp from src.utils.build import get_build_info from src.utils.exception import IncompatibleException from src.web3py.contract_tweak import tweak_w3_contracts from src.web3py.extensions import ( - LidoContracts, - TransactionUtils, ConsensusClientModule, - KeysAPIClientModule, - LidoValidatorsProvider, FallbackProviderModule, + KeysAPIClientModule, LazyCSM, + LidoContracts, + LidoValidatorsProvider, + TransactionUtils, ) from src.web3py.middleware import add_requests_metric_middleware from src.web3py.types import Web3 @@ -33,25 +46,7 @@ logger = logging.getLogger(__name__) -def main(module_name: OracleModule): - build_info = get_build_info() - logger.info({ - 'msg': 'Oracle startup.', - 'variables': { - **build_info, - 'module': module_name, - **variables.PUBLIC_ENV_VARS, - }, - }) - ENV_VARIABLES_INFO.info(variables.PUBLIC_ENV_VARS) - BUILD_INFO.info(build_info) - - logger.info({'msg': f'Start healthcheck server for Docker container on port {variables.HEALTHCHECK_SERVER_PORT}'}) - start_pulse_server() - - logger.info({'msg': f'Start http server with prometheus metrics on port {variables.PROMETHEUS_PORT}'}) - start_http_server(variables.PROMETHEUS_PORT) - +def _construct_web3() -> Web3: logger.info({'msg': 'Initialize multi web3 provider.'}) web3 = Web3(FallbackProviderModule( variables.EXECUTION_CLIENT_URI, @@ -92,34 +87,129 @@ def main(module_name: OracleModule): logger.info({'msg': 'Add metrics middleware for ETH1 requests.'}) add_requests_metric_middleware(web3) + return web3 - logger.info({'msg': 'Sanity checks.'}) +def _construct_module(web3: Web3, module_name: OracleModule, refslot: Optional[int] = None) -> Accounting | Ejector | CSOracle: instance: Accounting | Ejector | CSOracle if module_name == OracleModule.ACCOUNTING: logger.info({'msg': 'Initialize Accounting module.'}) - instance = Accounting(web3) + instance = Accounting(web3, refslot) elif module_name == OracleModule.EJECTOR: logger.info({'msg': 'Initialize Ejector module.'}) - instance = Ejector(web3) + instance = Ejector(web3, refslot) elif module_name == OracleModule.CSM: logger.info({'msg': 'Initialize CSM performance oracle module.'}) - instance = CSOracle(web3) + instance = CSOracle(web3, refslot) else: raise ValueError(f'Unexpected arg: {module_name=}.') + logger.info({'msg': 'Sanity checks.'}) instance.check_contract_configs() + return instance + +def main(module_name: OracleModule): + build_info = get_build_info() + logger.info({ + 'msg': 'Oracle startup.', + 'variables': { + **build_info, + 'module': module_name, + **variables.PUBLIC_ENV_VARS, + }, + }) + ENV_VARIABLES_INFO.info(variables.PUBLIC_ENV_VARS) + BUILD_INFO.info(build_info) + start_pulse_server() + + logger.info({'msg': f'Start http server with prometheus metrics on port {variables.PROMETHEUS_PORT}'}) + start_http_server(variables.PROMETHEUS_PORT) + web3 = _construct_web3() + instance: Accounting | Ejector | CSOracle = _construct_module(web3, module_name) if variables.DAEMON: instance.run_as_daemon() else: instance.cycle_handler() -def check(): - logger.info({'msg': 'Check oracle is ready to work in the current environment.'}) +def get_transactions(w3, contract: BaseOracleContract, limit: int, block_offset: int): + event_abi = "ProcessingStarted(uint256,bytes32)" + event_topic = Web3.to_hex(primitive=Web3.keccak(text=event_abi)) + + def get_processing_started_logs(from_block: int, to_block: int): + return w3.eth.get_logs({ + "fromBlock": from_block, + "toBlock": to_block, + "address": contract.address, + "topics": [event_topic], + }) + + latest = w3.eth.block_number + logs = get_processing_started_logs(from_block=latest - block_offset, to_block=latest) + tx_hashes = [log['transactionHash'].hex() for log in logs] + print(f"Found {len(tx_hashes)} submitReportData transactions in latest {block_offset} blocks.") + + txs = [] + for tx_hash in tx_hashes[:limit]: + tx = w3.eth.get_transaction(tx_hash) + _, params = contract.decode_function_input(tx['input']) + data = { + k: HexBytes(v.hex()) if isinstance(v, (bytes, bytearray)) else v + for k, v in params['data'].items() + } + txs.append(data) + + print(f"Will process {len(txs)} transactions") + return txs + + +def run_on_refslot(module_name: OracleModule, limit, block_offset): + logging.getLogger().setLevel(logging.WARNING) + w3 = _construct_web3() + instance: Accounting | Ejector | CSOracle = _construct_module(w3, module_name, True) + instance.check_contract_configs() - return ChecksModule().execute_module() + txs = get_transactions(w3, instance.report_contract, limit, block_offset) + if not txs: + logger.error("No submitReportData transactions found!") + sys.exit(0) + + _camel_to_snake_pattern = re.compile(r'(.)([A-Z][a-z]+)') + _camel_to_snake_pattern2 = re.compile(r'([a-z0-9])([A-Z])') + + def camel_to_snake(name: str) -> str: + s1 = _camel_to_snake_pattern.sub(r'\1_\2', name) + return _camel_to_snake_pattern2.sub(r'\1_\2', s1).lower() + + def normalize_keys(d: Dict[str, Any]) -> Dict[str, Any]: + return {camel_to_snake(k): v for k, v in d.items()} + + for tx in txs: + refslot = int(tx['refSlot']) + print("Input data:", tx) + block_root = BlockRoot(w3.cc.get_block_root(SlotNumber(refslot + 3 * 32)).root) + block_details = w3.cc.get_block_details(block_root) + bs = build_blockstamp(block_details) + instance.refslot = refslot + instance.refresh_contracts_if_address_change() + report_blockstamp = instance.get_blockstamp_for_report(bs) + report = instance.build_report(report_blockstamp) + + normalized_input = normalize_keys(tx) + report_dict = asdict(report) + report_dict = { + k: HexBytes(v.hex()) if isinstance(v, (bytes, bytearray)) else v + for k, v in report_dict.items() + } + print("Output data:", report_dict) + diff = DeepDiff(normalized_input, report_dict, ignore_order=True) + if diff: + print("🚨 Differences found:") + print(diff) + else: + print("✅ All fields match!") + instance = _construct_module(w3, module_name, refslot) def check_providers_chain_ids(web3: Web3, cc: ConsensusClientModule, kac: KeysAPIClientModule): @@ -163,19 +253,64 @@ def ipfs_providers() -> Iterator[IPFSProvider]: yield PublicIPFS(timeout=variables.HTTP_REQUEST_TIMEOUT_IPFS) +def parse_args(): + """ + Parse command-line arguments using argparse. + The 'module' argument is restricted to valid OracleModule values. + """ + valid_modules = [str(item) for item in OracleModule] + + parser = argparse.ArgumentParser(description="Run the Oracle module process.") + subparsers = parser.add_subparsers(dest="module", required=True, help=f"Module to run. One of: {valid_modules}") + check_parser = subparsers.add_parser("check", help="Run the check module.") + check_parser.add_argument( + "--name", + "-n", + type=str, + default=None, + help="Module name to check for a refslot execution." + ) + check_parser.add_argument( + "--limit", + type=int, + default=10, + help="Maximum number of items to process." + ) + check_parser.add_argument( + "--offset", + type=int, + default=100_000, + help="Starting index offset for processing." + ) + for mod in OracleModule: + if mod == OracleModule.CHECK: + continue + subparsers.add_parser( + mod.value, + help=f"Run the {mod.value} module." + ) + + return parser.parse_args() + + if __name__ == '__main__': - module_name_arg = sys.argv[-1] - if module_name_arg not in OracleModule: - msg = f'Last arg should be one of {[str(item) for item in OracleModule]}, received {module_name_arg}.' + args = parse_args() + if args.module not in OracleModule: + msg = f'Last arg should be one of {[str(item) for item in OracleModule]}, received {args.module}.' logger.error({'msg': msg}) raise ValueError(msg) - module = OracleModule(module_name_arg) + module = OracleModule(args.module) if module is OracleModule.CHECK: - errors = variables.check_uri_required_variables() - variables.raise_from_errors(errors) - - sys.exit(check()) + if args.name is None: + errors = variables.check_uri_required_variables() + variables.raise_from_errors(errors) + sys.exit(execute_checks()) + else: + errors = variables.check_all_required_variables(module) + variables.raise_from_errors(errors) + run_on_refslot(args.name, args.limit, args.offset) + sys.exit(0) errors = variables.check_all_required_variables(module) variables.raise_from_errors(errors) diff --git a/src/metrics/healthcheck_server.py b/src/metrics/healthcheck_server.py index 83c5efe40..ea166b73d 100644 --- a/src/metrics/healthcheck_server.py +++ b/src/metrics/healthcheck_server.py @@ -9,7 +9,6 @@ from src import variables from src.variables import MAX_CYCLE_LIFETIME_IN_SECONDS - _last_pulse = datetime.now() logger = logging.getLogger(__name__) @@ -50,6 +49,7 @@ def start_pulse_server(): If bot didn't call pulse for a while (5 minutes but should be changed individually) Docker healthcheck fails to do request """ + logger.info({'msg': f'Start healthcheck server for Docker container on port {variables.HEALTHCHECK_SERVER_PORT}'}) server = HTTPServer(('localhost', variables.HEALTHCHECK_SERVER_PORT), RequestHandlerClass=PulseRequestHandler) thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() diff --git a/src/modules/accounting/accounting.py b/src/modules/accounting/accounting.py index 8d540d3be..dd21985a5 100644 --- a/src/modules/accounting/accounting.py +++ b/src/modules/accounting/accounting.py @@ -1,6 +1,7 @@ import logging from collections import defaultdict from time import sleep +from typing import Optional from hexbytes import HexBytes from web3.exceptions import ContractCustomError @@ -8,35 +9,21 @@ from src import variables from src.constants import SHARE_RATE_PRECISION_E27 -from src.metrics.prometheus.accounting import ( - ACCOUNTING_IS_BUNKER, - ACCOUNTING_CL_BALANCE_GWEI, - ACCOUNTING_EL_REWARDS_VAULT_BALANCE_WEI, - ACCOUNTING_WITHDRAWAL_VAULT_BALANCE_WEI -) +from src.metrics.prometheus.accounting import (ACCOUNTING_CL_BALANCE_GWEI, ACCOUNTING_EL_REWARDS_VAULT_BALANCE_WEI, ACCOUNTING_IS_BUNKER, + ACCOUNTING_WITHDRAWAL_VAULT_BALANCE_WEI) from src.metrics.prometheus.duration_meter import duration_meter from src.modules.accounting.third_phase.extra_data import ExtraDataService from src.modules.accounting.third_phase.types import ExtraData, FormatList -from src.modules.accounting.types import ( - ReportData, - LidoReportRebase, - GenericExtraData, - WqReport, - RebaseReport, - BunkerMode, - FinalizationShareRate, - ValidatorsCount, - ValidatorsBalance, - AccountingProcessingState, -) -from src.modules.submodules.consensus import ConsensusModule, InitialEpochIsYetToArriveRevert +from src.modules.accounting.types import (AccountingProcessingState, BunkerMode, FinalizationShareRate, GenericExtraData, LidoReportRebase, + RebaseReport, ReportData, ValidatorsBalance, ValidatorsCount, WqReport) +from src.modules.submodules.consensus import ConsensusModule, InitialEpochIsYetToArriveRevert, Report from src.modules.submodules.oracle_module import BaseModule, ModuleExecuteDelay from src.modules.submodules.types import ZERO_HASH from src.providers.execution.contracts.accounting_oracle import AccountingOracleContract from src.services.bunker import BunkerService from src.services.validator_state import LidoValidatorStateService from src.services.withdrawal import Withdrawal -from src.types import BlockStamp, Gwei, ReferenceBlockStamp, StakingModuleId, NodeOperatorGlobalIndex, FinalizationBatches +from src.types import BlockStamp, FinalizationBatches, Gwei, NodeOperatorGlobalIndex, ReferenceBlockStamp, StakingModuleId from src.utils.cache import global_lru_cache as lru_cache from src.utils.units import gwei_to_wei from src.variables import ALLOW_REPORTING_IN_BUNKER_MODE @@ -60,8 +47,9 @@ class Accounting(BaseModule, ConsensusModule): COMPATIBLE_CONTRACT_VERSION = 2 COMPATIBLE_CONSENSUS_VERSION = 3 - def __init__(self, w3: Web3): + def __init__(self, w3: Web3, refslot: Optional[int] = None): self.report_contract: AccountingOracleContract = w3.lido_contracts.accounting_oracle + self.refslot = refslot super().__init__(w3) self.lido_validator_state_service = LidoValidatorStateService(self.w3) @@ -113,10 +101,10 @@ def _submit_extra_data(self, blockstamp: ReferenceBlockStamp) -> None: @lru_cache(maxsize=1) @duration_meter() - def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: + def build_report(self, blockstamp: ReferenceBlockStamp) -> Report: report_data = self._calculate_report(blockstamp) logger.info({'msg': 'Calculate report for accounting module.', 'value': report_data}) - return report_data.as_tuple() + return report_data def is_main_data_submitted(self, blockstamp: BlockStamp) -> bool: # Consensus module: if contract got report data (second phase) @@ -166,7 +154,7 @@ def _get_processing_state(self, blockstamp: BlockStamp) -> AccountingProcessingS ) # ---------------------------------------- Build report ---------------------------------------- - def _calculate_report(self, blockstamp: ReferenceBlockStamp): + def _calculate_report(self, blockstamp: ReferenceBlockStamp) -> ReportData: consensus_version = self.get_consensus_version(blockstamp) logger.info({'msg': 'Building the report', 'consensus_version': consensus_version}) rebase_part = self._calculate_rebase_report(blockstamp) @@ -370,7 +358,7 @@ def _calculate_extra_data_report(self, blockstamp: ReferenceBlockStamp) -> Extra @staticmethod def _update_metrics(report_data: ReportData): - ACCOUNTING_IS_BUNKER.set(report_data.is_bunker) + ACCOUNTING_IS_BUNKER.set(report_data.is_bunker_mode) ACCOUNTING_CL_BALANCE_GWEI.set(report_data.cl_balance_gwei) ACCOUNTING_EL_REWARDS_VAULT_BALANCE_WEI.set(report_data.el_rewards_vault_balance) ACCOUNTING_WITHDRAWAL_VAULT_BALANCE_WEI.set(report_data.withdrawal_vault_balance) @@ -390,16 +378,16 @@ def _combine_report_parts( return ReportData( consensus_version=consensus_version, ref_slot=blockstamp.ref_slot, - validators_count=validators_count, + num_validators=validators_count, cl_balance_gwei=cl_balance, - staking_module_ids_with_exited_validators=staking_module_ids_list, - count_exited_validators_by_staking_module=exit_validators_count_list, + staking_module_ids_with_newly_exited_validators=staking_module_ids_list, + num_exited_validators_by_staking_module=exit_validators_count_list, withdrawal_vault_balance=withdrawal_vault_balance, el_rewards_vault_balance=el_rewards_vault_balance, shares_requested_to_burn=shares_requested_to_burn, withdrawal_finalization_batches=finalization_batches, - finalization_share_rate=finalization_share_rate, - is_bunker=is_bunker, + simulated_share_rate=finalization_share_rate, + is_bunker_mode=is_bunker, extra_data_format=extra_data.format, extra_data_hash=extra_data.data_hash, extra_data_items_count=extra_data.items_count, diff --git a/src/modules/accounting/third_phase/extra_data.py b/src/modules/accounting/third_phase/extra_data.py index 8861af1a8..fa74b2f2b 100644 --- a/src/modules/accounting/third_phase/extra_data.py +++ b/src/modules/accounting/third_phase/extra_data.py @@ -1,8 +1,8 @@ from dataclasses import dataclass -from itertools import groupby, batched +from itertools import batched, groupby from typing import Sequence -from src.modules.accounting.third_phase.types import ExtraData, ItemType, ExtraDataLengths, FormatList +from src.modules.accounting.third_phase.types import ExtraData, ExtraDataLengths, FormatList, ItemType from src.modules.submodules.types import ZERO_HASH from src.types import NodeOperatorGlobalIndex from src.web3py.types import Web3 diff --git a/src/modules/accounting/types.py b/src/modules/accounting/types.py index 9843c61ae..898b3774e 100644 --- a/src/modules/accounting/types.py +++ b/src/modules/accounting/types.py @@ -1,54 +1,47 @@ from dataclasses import dataclass -from typing import Self, NewType +from typing import NewType, Self from eth_typing import ChecksumAddress from hexbytes import HexBytes from web3.types import Wei -from src.types import ( - SlotNumber, - Gwei, - StakingModuleId, - FinalizationBatches, - ELVaultBalance, - WithdrawalVaultBalance, - OperatorsValidatorCount, -) +from src.types import (ELVaultBalance, FinalizationBatches, Gwei, OperatorsValidatorCount, SlotNumber, StakingModuleId, + WithdrawalVaultBalance) @dataclass class ReportData: consensus_version: int ref_slot: SlotNumber - validators_count: int + num_validators: int cl_balance_gwei: Gwei - staking_module_ids_with_exited_validators: list[StakingModuleId] - count_exited_validators_by_staking_module: list[int] + staking_module_ids_with_newly_exited_validators: list[StakingModuleId] + num_exited_validators_by_staking_module: list[int] withdrawal_vault_balance: Wei el_rewards_vault_balance: Wei shares_requested_to_burn: int withdrawal_finalization_batches: list[int] - finalization_share_rate: int - is_bunker: bool + simulated_share_rate: int + is_bunker_mode: bool extra_data_format: int extra_data_hash: bytes extra_data_items_count: int - def as_tuple(self): + def as_tuple(self) -> tuple: # Tuple with report in correct order return ( self.consensus_version, self.ref_slot, - self.validators_count, + self.num_validators, self.cl_balance_gwei, - self.staking_module_ids_with_exited_validators, - self.count_exited_validators_by_staking_module, + self.staking_module_ids_with_newly_exited_validators, + self.num_exited_validators_by_staking_module, self.withdrawal_vault_balance, self.el_rewards_vault_balance, self.shares_requested_to_burn, self.withdrawal_finalization_batches, - self.finalization_share_rate, - self.is_bunker, + self.simulated_share_rate, + self.is_bunker_mode, self.extra_data_format, self.extra_data_hash, self.extra_data_items_count, diff --git a/src/modules/checks/checks_module.py b/src/modules/checks/checks_module.py index 59dd9ee09..8f29053ae 100644 --- a/src/modules/checks/checks_module.py +++ b/src/modules/checks/checks_module.py @@ -1,22 +1,13 @@ +import logging + import pytest +logger = logging.getLogger(__name__) -class ChecksModule: - """ - Module that executes all tests to figure out that environment is ready for Oracle. - Checks: - - Consensus Layer node - - Execution Layer node - - Keys API service - if LIDO_LOCATOR address provided - - Checks configs in Accounting module and Ejector module - if CSM_MODULE_ADDRESS provided - - Checks configs in CSM oracle module - - Checks with special blockstamp value (6300 slots in the past) - """ - def execute_module(self): - return pytest.main([ - 'src/modules/checks/suites', - '-c', 'src/modules/checks/pytest.ini', - ]) +def execute_checks(): + logger.info({'msg': 'Check oracle is ready to work in the current environment.'}) + return pytest.main([ + 'src/modules/checks/suites', + '-c', 'src/modules/checks/pytest.ini', + ]) diff --git a/src/modules/checks/suites/conftest.py b/src/modules/checks/suites/conftest.py index 06cf97977..c78b2a9dd 100644 --- a/src/modules/checks/suites/conftest.py +++ b/src/modules/checks/suites/conftest.py @@ -5,8 +5,8 @@ from src import variables from src.types import EpochNumber, SlotNumber, BlockRoot -from src.utils.blockstamp import build_blockstamp from src.utils.api import opsgenie_api +from src.utils.blockstamp import build_blockstamp from src.utils.slot import get_reference_blockstamp from src.web3py.contract_tweak import tweak_w3_contracts from src.web3py.extensions import ( @@ -20,7 +20,6 @@ ) from src.web3py.types import Web3 - TITLE_PROPERTY_NAME = "test_title" _config = None diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 5e212d8a0..78de29673 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -1,6 +1,6 @@ import logging from collections import defaultdict -from typing import Iterator +from typing import Iterator, Optional from hexbytes import HexBytes @@ -16,7 +16,7 @@ from src.modules.csm.state import State from src.modules.csm.tree import Tree from src.modules.csm.types import ReportData, Shares -from src.modules.submodules.consensus import ConsensusModule +from src.modules.submodules.consensus import ConsensusModule, Report from src.modules.submodules.oracle_module import BaseModule, ModuleExecuteDelay from src.modules.submodules.types import ZERO_HASH from src.providers.execution.contracts.cs_fee_oracle import CSFeeOracleContract @@ -65,9 +65,10 @@ class CSOracle(BaseModule, ConsensusModule): report_contract: CSFeeOracleContract module_id: StakingModuleId - def __init__(self, w3: Web3): + def __init__(self, w3: Web3, refslot: Optional[int] = None): self.report_contract = w3.csm.oracle self.state = State.load() + self.refslot = refslot super().__init__(w3) self.module_id = self._get_module_id() @@ -93,7 +94,7 @@ def execute_module(self, last_finalized_blockstamp: BlockStamp) -> ModuleExecute @lru_cache(maxsize=1) @duration_meter() - def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: + def build_report(self, blockstamp: ReferenceBlockStamp) -> Report: self.validate_state(blockstamp) prev_root = self.w3.csm.get_csm_tree_root(blockstamp) @@ -118,7 +119,7 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: tree_cid=prev_cid or "", log_cid=log_cid, distributed=0, - ).as_tuple() + ) if prev_cid and prev_root != ZERO_HASH: # Update cumulative amount of shares for all operators. @@ -137,7 +138,7 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: tree_cid=tree_cid, log_cid=log_cid, distributed=distributed, - ).as_tuple() + ) def is_main_data_submitted(self, blockstamp: BlockStamp) -> bool: last_ref_slot = self.w3.csm.get_csm_last_processing_ref_slot(blockstamp) diff --git a/src/modules/csm/types.py b/src/modules/csm/types.py index d305a6e0b..3478a6399 100644 --- a/src/modules/csm/types.py +++ b/src/modules/csm/types.py @@ -1,6 +1,6 @@ import logging from dataclasses import dataclass -from typing import TypeAlias, Literal +from typing import Literal, TypeAlias from hexbytes import HexBytes diff --git a/src/modules/ejector/data_encode.py b/src/modules/ejector/data_encode.py index 2ca8ad771..e9468a6dd 100644 --- a/src/modules/ejector/data_encode.py +++ b/src/modules/ejector/data_encode.py @@ -4,7 +4,6 @@ from src.utils.types import hex_str_to_bytes from src.web3py.extensions.lido_validators import LidoValidator, NodeOperatorGlobalIndex - DATA_FORMAT_LIST = 1 MODULE_ID_LENGTH = 3 diff --git a/src/modules/ejector/ejector.py b/src/modules/ejector/ejector.py index e936b798f..00dd52792 100644 --- a/src/modules/ejector/ejector.py +++ b/src/modules/ejector/ejector.py @@ -1,5 +1,5 @@ -import dataclasses import logging +from typing import Optional from web3.exceptions import ContractCustomError from web3.types import Wei @@ -19,10 +19,10 @@ from src.modules.ejector.data_encode import encode_data from src.modules.ejector.sweep import get_sweep_delay_in_epochs from src.modules.ejector.types import EjectorProcessingState, ReportData -from src.modules.submodules.consensus import ConsensusModule, InitialEpochIsYetToArriveRevert +from src.modules.submodules.consensus import ConsensusModule, InitialEpochIsYetToArriveRevert, Report from src.modules.submodules.oracle_module import BaseModule, ModuleExecuteDelay from src.modules.submodules.types import ZERO_HASH -from src.providers.consensus.types import Validator, BeaconStateView +from src.providers.consensus.types import BeaconStateView, Validator from src.providers.execution.contracts.exit_bus_oracle import ExitBusOracleContract from src.services.exit_order_iterator import ValidatorExitIterator from src.services.prediction import RewardsPredictionService @@ -30,14 +30,8 @@ from src.types import BlockStamp, EpochNumber, Gwei, NodeOperatorGlobalIndex, ReferenceBlockStamp from src.utils.cache import global_lru_cache as lru_cache from src.utils.units import gwei_to_wei -from src.utils.validator_state import ( - compute_activation_exit_epoch, - get_activation_exit_churn_limit, - get_validator_churn_limit, - get_max_effective_balance, - is_active_validator, - is_fully_withdrawable_validator, -) +from src.utils.validator_state import (compute_activation_exit_epoch, get_activation_exit_churn_limit, get_max_effective_balance, + is_active_validator, is_fully_withdrawable_validator) from src.web3py.extensions.lido_validators import LidoValidator from src.web3py.types import Web3 @@ -67,9 +61,9 @@ class Ejector(BaseModule, ConsensusModule): AVG_EXPECTING_WITHDRAWALS_SWEEP_DURATION_MULTIPLIER = 0.5 - def __init__(self, w3: Web3): + def __init__(self, w3: Web3, refslot: Optional[int] = None): self.report_contract: ExitBusOracleContract = w3.lido_contracts.validators_exit_bus_oracle - + self.refslot = refslot super().__init__(w3) self.prediction_service = RewardsPredictionService(w3) @@ -89,7 +83,7 @@ def execute_module(self, last_finalized_blockstamp: BlockStamp) -> ModuleExecute @lru_cache(maxsize=1) @duration_meter() - def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: + def build_report(self, blockstamp: ReferenceBlockStamp) -> Report: # For metrics only self.w3.lido_contracts.get_ejector_last_processing_ref_slot(blockstamp) @@ -110,8 +104,7 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: ) EJECTOR_VALIDATORS_COUNT_TO_EJECT.set(report_data.requests_count) - - return dataclasses.astuple(report_data) + return report_data def get_validators_to_eject(self, blockstamp: ReferenceBlockStamp) -> list[tuple[NodeOperatorGlobalIndex, LidoValidator]]: to_withdraw_amount = self.w3.lido_contracts.withdrawal_queue_nft.unfinalized_steth(blockstamp.block_hash) @@ -290,14 +283,6 @@ def _get_sweep_delay_in_epochs(self, blockstamp: ReferenceBlockStamp) -> int: state = self.w3.cc.get_state_view(blockstamp) return get_sweep_delay_in_epochs(state, chain_config) - @lru_cache(maxsize=1) - def _get_churn_limit(self, blockstamp: ReferenceBlockStamp) -> int: - total_active_validators = len(self._get_active_validators(blockstamp)) - logger.info({'msg': 'Calculate total active validators.', 'value': total_active_validators}) - churn_limit = get_validator_churn_limit(total_active_validators) - logger.info({'msg': 'Calculate churn limit.', 'value': churn_limit}) - return churn_limit - # https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#get_total_active_balance def _get_total_active_balance(self, blockstamp: ReferenceBlockStamp) -> Gwei: active_validators = self._get_active_validators(blockstamp) diff --git a/src/modules/ejector/types.py b/src/modules/ejector/types.py index 07c81c643..3d354b556 100644 --- a/src/modules/ejector/types.py +++ b/src/modules/ejector/types.py @@ -1,3 +1,4 @@ +import dataclasses from dataclasses import dataclass from src.types import SlotNumber @@ -21,3 +22,6 @@ class ReportData: requests_count: int data_format: int data: bytes + + def as_tuple(self) -> tuple: + return dataclasses.astuple(self) diff --git a/src/modules/submodules/consensus.py b/src/modules/submodules/consensus.py index 46cb9dbf1..332ed11ec 100644 --- a/src/modules/submodules/consensus.py +++ b/src/modules/submodules/consensus.py @@ -1,25 +1,21 @@ import logging from abc import ABC, abstractmethod from time import sleep -from typing import cast +from typing import cast, Optional, Protocol from eth_abi import encode from hexbytes import HexBytes from web3.exceptions import ContractCustomError from src import variables -from src.metrics.prometheus.basic import ORACLE_SLOT_NUMBER, ORACLE_BLOCK_NUMBER, GENESIS_TIME, ACCOUNT_BALANCE -from src.metrics.prometheus.business import ( - ORACLE_MEMBER_LAST_REPORT_REF_SLOT, - FRAME_CURRENT_REF_SLOT, - FRAME_DEADLINE_SLOT, - ORACLE_MEMBER_INFO -) -from src.modules.submodules.exceptions import IsNotMemberException, IncompatibleOracleVersion, ContractVersionMismatch -from src.modules.submodules.types import ChainConfig, MemberInfo, ZERO_HASH, CurrentFrame, FrameConfig +from src.metrics.prometheus.basic import ACCOUNT_BALANCE, GENESIS_TIME, ORACLE_BLOCK_NUMBER, ORACLE_SLOT_NUMBER +from src.metrics.prometheus.business import (FRAME_CURRENT_REF_SLOT, FRAME_DEADLINE_SLOT, ORACLE_MEMBER_INFO, + ORACLE_MEMBER_LAST_REPORT_REF_SLOT) +from src.modules.submodules.exceptions import ContractVersionMismatch, IncompatibleOracleVersion, IsNotMemberException +from src.modules.submodules.types import ChainConfig, CurrentFrame, FrameConfig, MemberInfo, ZERO_HASH from src.providers.execution.contracts.base_oracle import BaseOracleContract from src.providers.execution.contracts.hash_consensus import HashConsensusContract -from src.types import BlockStamp, ReferenceBlockStamp, SlotNumber, FrameNumber +from src.types import BlockStamp, FrameNumber, ReferenceBlockStamp, SlotNumber from src.utils.blockstamp import build_blockstamp from src.utils.cache import global_lru_cache as lru_cache from src.utils.slot import get_reference_blockstamp @@ -32,6 +28,11 @@ InitialEpochIsYetToArriveRevert = Web3.to_hex(primitive=Web3.keccak(text="InitialEpochIsYetToArrive()")[:4]) +class Report(Protocol): + def as_tuple(self) -> tuple: + ... + + class ConsensusModule(ABC): """ Module that works with Hash Consensus Contract. @@ -44,6 +45,7 @@ class ConsensusModule(ABC): report_contract should contain getConsensusContract method. """ report_contract: BaseOracleContract + refslot: Optional[int] = None COMPATIBLE_CONTRACT_VERSION: int COMPATIBLE_CONSENSUS_VERSION: int @@ -192,10 +194,26 @@ def get_member_info(self, blockstamp: BlockStamp) -> MemberInfo: current_frame_member_report=current_frame_member_report, deadline_slot=current_frame.report_processing_deadline_slot, ) - logger.debug({'msg': 'Fetch member info.', 'value': mi}) + logger.info({'msg': 'Fetch member info.', 'value': mi}) return mi + def _calculate_reference_blockstamp(self, last_finalized_blockstamp: BlockStamp) -> ReferenceBlockStamp: + converter = self._get_web3_converter(last_finalized_blockstamp) + member_info = self.get_member_info(self._get_latest_blockstamp()) + + refslot = member_info.current_frame_ref_slot + if self.refslot: + refslot = self.refslot + bs = get_reference_blockstamp( + cc=self.w3.cc, + ref_slot=refslot, + ref_epoch=converter.get_epoch_by_slot(member_info.current_frame_ref_slot), + last_finalized_slot_number=last_finalized_blockstamp.slot_number, + ) + logger.info({'msg': 'Calculate blockstamp for report.', 'value': bs}) + return bs + # ----- Calculation reference slot for report ----- def get_blockstamp_for_report(self, last_finalized_blockstamp: BlockStamp) -> ReferenceBlockStamp | None: """ @@ -203,6 +221,8 @@ def get_blockstamp_for_report(self, last_finalized_blockstamp: BlockStamp) -> Re Returns: Non-missed reference slot blockstamp in case contract is reportable. """ + if self.refslot: + return self._calculate_reference_blockstamp(last_finalized_blockstamp) latest_blockstamp = self._get_latest_blockstamp() # Check if contract is currently reportable @@ -211,7 +231,6 @@ def get_blockstamp_for_report(self, last_finalized_blockstamp: BlockStamp) -> Re return None member_info = self.get_member_info(latest_blockstamp) - logger.info({'msg': 'Fetch member info.', 'value': member_info}) # Check if current slot is higher than member slot if last_finalized_blockstamp.slot_number < member_info.current_frame_ref_slot: @@ -223,17 +242,7 @@ def get_blockstamp_for_report(self, last_finalized_blockstamp: BlockStamp) -> Re logger.info({'msg': 'Deadline missed.'}) return None - converter = self._get_web3_converter(last_finalized_blockstamp) - - bs = get_reference_blockstamp( - cc=self.w3.cc, - ref_slot=member_info.current_frame_ref_slot, - ref_epoch=converter.get_epoch_by_slot(member_info.current_frame_ref_slot), - last_finalized_slot_number=last_finalized_blockstamp.slot_number, - ) - logger.info({'msg': 'Calculate blockstamp for report.', 'value': bs}) - - return bs + return self._calculate_reference_blockstamp(last_finalized_blockstamp) def _check_compatability(self, blockstamp: BlockStamp) -> bool: """ @@ -271,7 +280,8 @@ def _check_compatability(self, blockstamp: BlockStamp) -> bool: # ----- Working with report ----- def process_report(self, blockstamp: ReferenceBlockStamp) -> None: """Builds and sends report for current frame with provided blockstamp.""" - report_data = self.build_report(blockstamp) + x = self.build_report(blockstamp) + report_data = x.as_tuple() logger.info({'msg': 'Build report.', 'value': report_data}) report_hash = self._encode_data_hash(report_data) @@ -389,8 +399,7 @@ def _encode_data_hash(self, report_data: tuple) -> HexBytes: # Transform str abi to tuple, because ReportData is struct encoded = encode([f'({report_str_abi})'], [report_data]) - report_hash = self.w3.keccak(encoded) - return report_hash + return self.w3.keccak(encoded) def _send_report_hash(self, blockstamp: ReferenceBlockStamp, report_hash: bytes, consensus_version: int): consensus_contract = self._get_consensus_contract(blockstamp) @@ -405,7 +414,10 @@ def _submit_report(self, report: tuple, contract_version: int): self.w3.transaction.check_and_send_transaction(tx, variables.ACCOUNT) def _get_latest_blockstamp(self) -> BlockStamp: - root = self.w3.cc.get_block_root('head').root + if self.refslot: + root = self.w3.cc.get_block_root(SlotNumber(self.refslot + 3 * 32)).root + else: + root = self.w3.cc.get_block_root('head').root block_details = self.w3.cc.get_block_details(root) bs = build_blockstamp(block_details) logger.debug({'msg': 'Fetch latest blockstamp.', 'value': bs}) @@ -455,7 +467,7 @@ def _get_web3_converter(self, blockstamp: BlockStamp) -> Web3Converter: @abstractmethod @lru_cache(maxsize=1) - def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: + def build_report(self, blockstamp: ReferenceBlockStamp) -> Report: """Returns ReportData struct with calculated data.""" @abstractmethod diff --git a/src/modules/submodules/oracle_module.py b/src/modules/submodules/oracle_module.py index d2bd42241..da81a8c54 100644 --- a/src/modules/submodules/oracle_module.py +++ b/src/modules/submodules/oracle_module.py @@ -1,29 +1,28 @@ import logging import time import traceback -from abc import abstractmethod, ABC +from abc import ABC, abstractmethod from dataclasses import asdict from enum import Enum from requests.exceptions import ConnectionError as RequestsConnectionError from timeout_decorator import timeout, TimeoutError as DecoratorTimeoutError from web3.exceptions import Web3Exception +from web3_multi_provider import NoActiveProviderError +from src import variables from src.metrics.healthcheck_server import pulse from src.metrics.prometheus.basic import ORACLE_BLOCK_NUMBER, ORACLE_SLOT_NUMBER -from src.modules.submodules.exceptions import IsNotMemberException, IncompatibleOracleVersion, ContractVersionMismatch +from src.modules.submodules.exceptions import ContractVersionMismatch, IncompatibleOracleVersion, IsNotMemberException from src.providers.http_provider import NotOkResponse from src.providers.ipfs import IPFSError from src.providers.keys.client import KeysOutdatedException +from src.types import BlockRoot, BlockStamp, SlotNumber +from src.utils.blockstamp import build_blockstamp from src.utils.cache import clear_global_cache +from src.utils.slot import InconsistentData, NoSlotsAvailable, SlotNotFinalized from src.web3py.extensions.lido_validators import CountOfKeysDiffersException -from src.utils.blockstamp import build_blockstamp -from src.utils.slot import NoSlotsAvailable, SlotNotFinalized, InconsistentData from src.web3py.types import Web3 -from web3_multi_provider import NoActiveProviderError - -from src import variables -from src.types import SlotNumber, BlockStamp, BlockRoot logger = logging.getLogger(__name__) @@ -60,6 +59,10 @@ def cycle_handler(self): self._cycle() self._sleep_cycle() + def refresh_contracts_and_run_cycle(self, blockstamp: BlockStamp): + self.refresh_contracts_if_address_change() + self.run_cycle(blockstamp) + @timeout(variables.MAX_CYCLE_LIFETIME_IN_SECONDS) def _cycle(self): """ @@ -78,8 +81,8 @@ def _cycle(self): }) return - self.refresh_contracts_if_address_change() - self.run_cycle(blockstamp) + self.refresh_contracts_and_run_cycle(blockstamp) + pulse() except IsNotMemberException as error: logger.error({'msg': 'Provided account is not part of Oracle`s committee.'}) raise error @@ -130,7 +133,6 @@ def _receive_last_finalized_slot(self) -> BlockStamp: def run_cycle(self, blockstamp: BlockStamp): logger.info({'msg': 'Execute module.', 'value': blockstamp}) result = self.execute_module(blockstamp) - pulse() if result is ModuleExecuteDelay.NEXT_FINALIZED_EPOCH: self._slot_threshold = blockstamp.slot_number diff --git a/src/modules/submodules/types.py b/src/modules/submodules/types.py index 5cd940b86..6b07a8ac3 100644 --- a/src/modules/submodules/types.py +++ b/src/modules/submodules/types.py @@ -2,7 +2,6 @@ from src.types import SlotNumber - ZERO_HASH = bytes([0]*32) diff --git a/src/providers/execution/contracts/base_oracle.py b/src/providers/execution/contracts/base_oracle.py index 54d161b4e..e580fc275 100644 --- a/src/providers/execution/contracts/base_oracle.py +++ b/src/providers/execution/contracts/base_oracle.py @@ -1,5 +1,4 @@ import logging -from src.utils.cache import global_lru_cache as lru_cache from eth_typing import ChecksumAddress, Hash32 from web3.contract.contract import ContractFunction @@ -7,7 +6,7 @@ from src.providers.execution.base_interface import ContractInterface from src.types import SlotNumber - +from src.utils.cache import global_lru_cache as lru_cache logger = logging.getLogger(__name__) diff --git a/src/providers/execution/contracts/hash_consensus.py b/src/providers/execution/contracts/hash_consensus.py index 0d909f549..a697c03bd 100644 --- a/src/providers/execution/contracts/hash_consensus.py +++ b/src/providers/execution/contracts/hash_consensus.py @@ -1,6 +1,4 @@ import logging -from src.types import SlotNumber -from src.utils.cache import global_lru_cache as lru_cache from eth_typing import ChecksumAddress from web3.contract.contract import ContractFunction @@ -8,7 +6,9 @@ from src.modules.submodules.types import ChainConfig, CurrentFrame, FrameConfig from src.providers.execution.base_interface import ContractInterface +from src.types import SlotNumber from src.utils.abi import named_tuple_to_dataclass +from src.utils.cache import global_lru_cache as lru_cache logger = logging.getLogger(__name__) diff --git a/src/providers/execution/contracts/lido.py b/src/providers/execution/contracts/lido.py index 90bfb569a..5b0513022 100644 --- a/src/providers/execution/contracts/lido.py +++ b/src/providers/execution/contracts/lido.py @@ -3,9 +3,9 @@ from eth_typing import ChecksumAddress, HexStr from web3 import Web3 from web3.exceptions import Web3RPCError -from web3.types import Wei, BlockIdentifier, StateOverride, StateOverrideParams +from web3.types import BlockIdentifier, StateOverride, StateOverrideParams, Wei -from src.modules.accounting.types import LidoReportRebase, BeaconStat +from src.modules.accounting.types import BeaconStat, LidoReportRebase from src.providers.execution.base_interface import ContractInterface from src.types import SlotNumber from src.utils.abi import named_tuple_to_dataclass @@ -61,7 +61,6 @@ def handle_oracle_report( 'error': repr(error), }) hex_ref_slot = HexStr(hex(ref_slot)) - return self._handle_oracle_report( timestamp, time_elapsed, diff --git a/src/providers/keys/client.py b/src/providers/keys/client.py index ae0ba65a6..f527930cf 100644 --- a/src/providers/keys/client.py +++ b/src/providers/keys/client.py @@ -1,9 +1,9 @@ from time import sleep -from typing import cast, TypedDict, List +from typing import cast, List, TypedDict -from src.metrics.prometheus.basic import KEYS_API_REQUESTS_DURATION, KEYS_API_LATEST_BLOCKNUMBER +from src.metrics.prometheus.basic import KEYS_API_LATEST_BLOCKNUMBER, KEYS_API_REQUESTS_DURATION from src.providers.http_provider import HTTPProvider, NotOkResponse -from src.providers.keys.types import LidoKey, KeysApiStatus +from src.providers.keys.types import KeysApiStatus, LidoKey from src.types import BlockStamp, StakingModuleAddress from src.utils.cache import global_lru_cache as lru_cache diff --git a/src/services/bunker.py b/src/services/bunker.py index 1294570d1..4895e5491 100644 --- a/src/services/bunker.py +++ b/src/services/bunker.py @@ -4,19 +4,14 @@ from src.constants import TOTAL_BASIS_POINTS from src.metrics.prometheus.duration_meter import duration_meter -from src.metrics.prometheus.validators import ( - ALL_VALIDATORS, - LIDO_VALIDATORS, - ALL_SLASHED_VALIDATORS, - LIDO_SLASHED_VALIDATORS, -) +from src.metrics.prometheus.validators import (ALL_SLASHED_VALIDATORS, ALL_VALIDATORS, LIDO_SLASHED_VALIDATORS, LIDO_VALIDATORS) from src.modules.accounting.types import LidoReportRebase -from src.modules.submodules.consensus import FrameConfig, ChainConfig +from src.modules.submodules.consensus import ChainConfig, FrameConfig from src.services.bunker_cases.abnormal_cl_rebase import AbnormalClRebase from src.services.bunker_cases.midterm_slashing_penalty import MidtermSlashingPenalty from src.services.bunker_cases.types import BunkerConfig from src.services.safe_border import filter_slashed_validators -from src.types import BlockStamp, ReferenceBlockStamp, Gwei +from src.types import BlockStamp, Gwei, ReferenceBlockStamp from src.utils.units import wei_to_gwei from src.utils.web3converter import Web3Converter from src.web3py.types import Web3 diff --git a/src/services/bunker_cases/abnormal_cl_rebase.py b/src/services/bunker_cases/abnormal_cl_rebase.py index 72941e5a7..5da205c2c 100644 --- a/src/services/bunker_cases/abnormal_cl_rebase.py +++ b/src/services/bunker_cases/abnormal_cl_rebase.py @@ -1,6 +1,6 @@ import logging import math -from typing import Sequence, cast +from typing import cast, Sequence from web3.contract.contract import ContractEvent from web3.types import EventData @@ -10,7 +10,7 @@ from src.providers.consensus.types import Validator from src.providers.keys.types import LidoKey from src.services.bunker_cases.types import BunkerConfig -from src.types import ReferenceBlockStamp, Gwei, BlockNumber, SlotNumber, BlockStamp, EpochNumber +from src.types import BlockNumber, BlockStamp, EpochNumber, Gwei, ReferenceBlockStamp, SlotNumber from src.utils.events import get_events_in_range from src.utils.slot import get_blockstamp, get_reference_blockstamp from src.utils.units import wei_to_gwei diff --git a/src/services/exit_order_iterator.py b/src/services/exit_order_iterator.py index 7d59146ce..32495bf81 100644 --- a/src/services/exit_order_iterator.py +++ b/src/services/exit_order_iterator.py @@ -1,13 +1,13 @@ import logging from dataclasses import dataclass -from src.constants import TOTAL_BASIS_POINTS, LIDO_DEPOSIT_AMOUNT +from src.constants import LIDO_DEPOSIT_AMOUNT, TOTAL_BASIS_POINTS from src.metrics.prometheus.duration_meter import duration_meter from src.providers.consensus.types import Validator from src.services.validator_state import LidoValidatorStateService -from src.types import ReferenceBlockStamp, NodeOperatorGlobalIndex, StakingModuleId, Gwei -from src.utils.validator_state import is_on_exit, get_validator_age -from src.web3py.extensions.lido_validators import LidoValidator, StakingModule, NodeOperator, NodeOperatorLimitMode +from src.types import Gwei, NodeOperatorGlobalIndex, ReferenceBlockStamp, StakingModuleId +from src.utils.validator_state import get_validator_age, is_on_exit +from src.web3py.extensions.lido_validators import LidoValidator, NodeOperator, NodeOperatorLimitMode, StakingModule from src.web3py.types import Web3 logger = logging.getLogger(__name__) diff --git a/src/services/prediction.py b/src/services/prediction.py index 8896b5f98..9ac3576f9 100644 --- a/src/services/prediction.py +++ b/src/services/prediction.py @@ -9,7 +9,6 @@ from src.utils.events import get_events_in_past from src.web3py.types import Web3 - logger = logging.getLogger(__name__) diff --git a/src/services/validator_state.py b/src/services/validator_state.py index 062eefc8e..ca7c33291 100644 --- a/src/services/validator_state.py +++ b/src/services/validator_state.py @@ -5,20 +5,14 @@ from eth_typing import HexStr from src.constants import FAR_FUTURE_EPOCH, SHARD_COMMITTEE_PERIOD -from src.metrics.prometheus.accounting import ( - ACCOUNTING_STUCK_VALIDATORS, - ACCOUNTING_EXITED_VALIDATORS, -) +from src.metrics.prometheus.accounting import (ACCOUNTING_EXITED_VALIDATORS, ACCOUNTING_STUCK_VALIDATORS) from src.modules.submodules.types import ChainConfig -from src.types import BlockStamp, ReferenceBlockStamp, EpochNumber, OperatorsValidatorCount +from src.types import BlockStamp, EpochNumber, OperatorsValidatorCount, ReferenceBlockStamp from src.utils.cache import global_lru_cache as lru_cache from src.utils.events import get_events_in_past from src.utils.types import bytes_to_hex_str -from src.utils.validator_state import is_exited_validator, is_validator_eligible_to_exit, is_on_exit -from src.web3py.extensions.lido_validators import ( - NodeOperatorGlobalIndex, - LidoValidator, -) +from src.utils.validator_state import is_exited_validator, is_on_exit, is_validator_eligible_to_exit +from src.web3py.extensions.lido_validators import (LidoValidator, NodeOperatorGlobalIndex) from src.web3py.types import Web3 logger = logging.getLogger(__name__) diff --git a/src/services/withdrawal.py b/src/services/withdrawal.py index ada3d5a41..f458f2533 100644 --- a/src/services/withdrawal.py +++ b/src/services/withdrawal.py @@ -1,12 +1,12 @@ from web3.types import Wei from src.metrics.prometheus.business import CONTRACT_ON_PAUSE +from src.modules.accounting.types import BatchState +from src.modules.submodules.consensus import ChainConfig, FrameConfig +from src.services.safe_border import SafeBorder +from src.types import FinalizationBatches, ReferenceBlockStamp from src.variables import FINALIZATION_BATCH_MAX_REQUEST_COUNT from src.web3py.types import Web3 -from src.types import ReferenceBlockStamp, FinalizationBatches -from src.services.safe_border import SafeBorder -from src.modules.submodules.consensus import ChainConfig, FrameConfig -from src.modules.accounting.types import BatchState class Withdrawal: diff --git a/src/web3py/extensions/contracts.py b/src/web3py/extensions/contracts.py index 39f30536c..e861ddb96 100644 --- a/src/web3py/extensions/contracts.py +++ b/src/web3py/extensions/contracts.py @@ -16,12 +16,10 @@ from src.providers.execution.contracts.oracle_daemon_config import OracleDaemonConfigContract from src.providers.execution.contracts.oracle_report_sanity_checker import OracleReportSanityCheckerContract from src.providers.execution.contracts.staking_router import StakingRouterContract - from src.providers.execution.contracts.withdrawal_queue_nft import WithdrawalQueueNftContract -from src.types import BlockStamp, SlotNumber, WithdrawalVaultBalance, ELVaultBalance +from src.types import BlockStamp, ELVaultBalance, SlotNumber, WithdrawalVaultBalance from src.utils.cache import global_lru_cache as lru_cache - logger = logging.getLogger(__name__) diff --git a/src/web3py/extensions/csm.py b/src/web3py/extensions/csm.py index 78e0669a2..cb8e15cfa 100644 --- a/src/web3py/extensions/csm.py +++ b/src/web3py/extensions/csm.py @@ -2,7 +2,7 @@ from functools import partial from itertools import groupby from time import sleep -from typing import Callable, Iterator, cast +from typing import Callable, cast, Iterator from eth_typing import BlockNumber from hexbytes import HexBytes diff --git a/src/web3py/extensions/lido_validators.py b/src/web3py/extensions/lido_validators.py index df071a364..2b643be28 100644 --- a/src/web3py/extensions/lido_validators.py +++ b/src/web3py/extensions/lido_validators.py @@ -8,9 +8,9 @@ from src.providers.consensus.types import Validator from src.providers.keys.types import LidoKey -from src.types import BlockStamp, StakingModuleId, NodeOperatorId, NodeOperatorGlobalIndex, StakingModuleAddress +from src.types import BlockStamp, NodeOperatorGlobalIndex, NodeOperatorId, StakingModuleAddress, StakingModuleId from src.utils.cache import global_lru_cache as lru_cache -from src.utils.dataclass import Nested, FromResponse +from src.utils.dataclass import FromResponse, Nested logger = logging.getLogger(__name__) diff --git a/tests/modules/accounting/bunker/conftest.py b/tests/modules/accounting/bunker/conftest.py index 674b9dbc3..52ee7de96 100644 --- a/tests/modules/accounting/bunker/conftest.py +++ b/tests/modules/accounting/bunker/conftest.py @@ -5,8 +5,8 @@ from src.constants import FAR_FUTURE_EPOCH from src.modules.submodules.types import ChainConfig from src.providers.consensus.types import Validator, ValidatorState -from src.services.bunker import BunkerService from src.providers.keys.types import LidoKey +from src.services.bunker import BunkerService from src.services.bunker_cases.abnormal_cl_rebase import AbnormalClRebase from src.services.bunker_cases.types import BunkerConfig from src.types import BlockNumber, BlockStamp, EpochNumber, Gwei, ReferenceBlockStamp, ValidatorIndex diff --git a/tests/modules/accounting/bunker/test_bunker_abnormal_cl_rebase.py b/tests/modules/accounting/bunker/test_bunker_abnormal_cl_rebase.py index e28baa20e..9d226b680 100644 --- a/tests/modules/accounting/bunker/test_bunker_abnormal_cl_rebase.py +++ b/tests/modules/accounting/bunker/test_bunker_abnormal_cl_rebase.py @@ -6,13 +6,13 @@ from src.providers.consensus.types import Validator, ValidatorState from src.services.bunker_cases.abnormal_cl_rebase import AbnormalClRebase from src.services.bunker_cases.types import BunkerConfig -from src.types import Gwei, ValidatorIndex, EpochNumber +from src.types import EpochNumber, Gwei, ValidatorIndex from src.web3py.extensions import LidoValidatorsProvider from src.web3py.types import Web3 from tests.factory.blockstamp import ReferenceBlockStampFactory -from tests.factory.configs import ChainConfigFactory, BunkerConfigFactory +from tests.factory.configs import BunkerConfigFactory, ChainConfigFactory from tests.factory.no_registry import LidoValidatorFactory -from tests.modules.accounting.bunker.conftest import simple_ref_blockstamp, simple_key, simple_blockstamp +from tests.modules.accounting.bunker.conftest import simple_blockstamp, simple_key, simple_ref_blockstamp def simple_validators( diff --git a/tests/modules/accounting/bunker/test_bunker_midterm_penalty.py b/tests/modules/accounting/bunker/test_bunker_midterm_penalty.py index 30d2626bc..903572aca 100644 --- a/tests/modules/accounting/bunker/test_bunker_midterm_penalty.py +++ b/tests/modules/accounting/bunker/test_bunker_midterm_penalty.py @@ -1,10 +1,10 @@ import pytest from src.constants import ( + EPOCHS_PER_SLASHINGS_VECTOR, FAR_FUTURE_EPOCH, MAX_EFFECTIVE_BALANCE, MAX_EFFECTIVE_BALANCE_ELECTRA, - EPOCHS_PER_SLASHINGS_VECTOR, ) from src.modules.submodules.consensus import FrameConfig from src.modules.submodules.types import ChainConfig diff --git a/tests/modules/accounting/test_accounting_module.py b/tests/modules/accounting/test_accounting_module.py index 1f09cdb03..6ee9c537d 100644 --- a/tests/modules/accounting/test_accounting_module.py +++ b/tests/modules/accounting/test_accounting_module.py @@ -1,4 +1,4 @@ -from typing import Iterable, cast +from typing import cast, Iterable from unittest.mock import Mock, patch import pytest @@ -7,12 +7,11 @@ from src import variables from src.modules.accounting import accounting as accounting_module -from src.modules.accounting.accounting import Accounting -from src.modules.accounting.accounting import logger as accounting_logger +from src.modules.accounting.accounting import Accounting, logger as accounting_logger from src.modules.accounting.third_phase.types import FormatList -from src.modules.accounting.types import LidoReportRebase, AccountingProcessingState +from src.modules.accounting.types import AccountingProcessingState, LidoReportRebase from src.modules.submodules.oracle_module import ModuleExecuteDelay -from src.modules.submodules.types import ChainConfig, FrameConfig, CurrentFrame, ZERO_HASH +from src.modules.submodules.types import ChainConfig, CurrentFrame, FrameConfig, ZERO_HASH from src.services.withdrawal import Withdrawal from src.types import BlockStamp, ReferenceBlockStamp from src.web3py.extensions.lido_validators import NodeOperatorId, StakingModule @@ -378,7 +377,7 @@ def test_build_report( accounting._calculate_report = Mock(return_value=Mock(as_tuple=Mock(return_value=REPORT))) - report = accounting.build_report(ref_bs) + report = accounting.build_report(ref_bs).as_tuple() assert report is REPORT, "build_report returned unexpected value" accounting._calculate_report.assert_called_once_with(ref_bs) diff --git a/tests/modules/accounting/test_validator_state.py b/tests/modules/accounting/test_validator_state.py index 1149e473f..6dd434869 100644 --- a/tests/modules/accounting/test_validator_state.py +++ b/tests/modules/accounting/test_validator_state.py @@ -9,12 +9,8 @@ from src.providers.consensus.types import Validator, ValidatorState from src.providers.keys.types import LidoKey from src.services.validator_state import LidoValidatorStateService -from src.types import EpochNumber, Gwei, StakingModuleId, NodeOperatorId, ValidatorIndex -from src.web3py.extensions.lido_validators import ( - NodeOperator, - StakingModule, - LidoValidator, -) +from src.types import EpochNumber, Gwei, NodeOperatorId, StakingModuleId, ValidatorIndex +from src.web3py.extensions.lido_validators import LidoValidator, NodeOperator, StakingModule from tests.factory.blockstamp import ReferenceBlockStampFactory TESTING_REF_EPOCH = 100 @@ -142,9 +138,7 @@ def chain_config(): def test_get_lido_new_stuck_validators(web3, validator_state, chain_config): validator_state.get_last_requested_to_exit_pubkeys = Mock(return_value={"0x8"}) validator_state.w3.lido_contracts.oracle_daemon_config.validator_delinquent_timeout_in_slots = Mock(return_value=0) - stuck_validators = validator_state.get_lido_newly_stuck_validators(blockstamp, chain_config) - assert stuck_validators == {(1, 0): 1} diff --git a/tests/modules/csm/test_csm_module.py b/tests/modules/csm/test_csm_module.py index 0265b6596..4114695dd 100644 --- a/tests/modules/csm/test_csm_module.py +++ b/tests/modules/csm/test_csm_module.py @@ -1,7 +1,7 @@ import logging from collections import defaultdict from dataclasses import dataclass -from typing import NoReturn, Iterable, Literal, Type +from typing import Iterable, Literal, NoReturn, Type from unittest.mock import Mock, patch, PropertyMock import pytest @@ -13,9 +13,8 @@ from src.modules.csm.tree import Tree from src.modules.submodules.oracle_module import ModuleExecuteDelay from src.modules.submodules.types import CurrentFrame, ZERO_HASH -from src.providers.ipfs import CIDv0, CID +from src.providers.ipfs import CID, CIDv0 from src.types import EpochNumber, NodeOperatorId, SlotNumber, StakingModuleId, ValidatorIndex -from src.web3py.extensions.csm import CSM from tests.factory.blockstamp import BlockStampFactory, ReferenceBlockStampFactory from tests.factory.configs import ChainConfigFactory, FrameConfigFactory @@ -707,7 +706,7 @@ def test_build_report(module: CSOracle, param: BuildReportTestParam): module.publish_tree = Mock(return_value=param.curr_tree_cid) module.publish_log = Mock(return_value=param.curr_log_cid) - report = module.build_report(blockstamp=Mock(ref_slot=100500)) + report = module.build_report(blockstamp=Mock(ref_slot=100500)).as_tuple() assert module.make_tree.call_args == param.expected_make_tree_call_args assert report == param.expected_func_result diff --git a/tests/modules/csm/test_log.py b/tests/modules/csm/test_log.py index ff31bcbad..3a35a2f18 100644 --- a/tests/modules/csm/test_log.py +++ b/tests/modules/csm/test_log.py @@ -1,4 +1,5 @@ import json + import pytest from src.modules.csm.log import FramePerfLog diff --git a/tests/modules/ejector/test_data_encode.py b/tests/modules/ejector/test_data_encode.py index bf368dcca..6ebeea923 100644 --- a/tests/modules/ejector/test_data_encode.py +++ b/tests/modules/ejector/test_data_encode.py @@ -5,18 +5,17 @@ import pytest from src.modules.ejector.data_encode import ( + encode_data, MODULE_ID_LENGTH, NODE_OPERATOR_ID_LENGTH, + sort_validators_to_eject, VALIDATOR_INDEX_LENGTH, VALIDATOR_PUB_KEY_LENGTH, - encode_data, - sort_validators_to_eject, ) -from src.types import StakingModuleId, NodeOperatorId +from src.types import NodeOperatorId, StakingModuleId from src.web3py.extensions.lido_validators import LidoValidator from tests.factory.no_registry import LidoValidatorFactory - RECORD_LENGTH = sum( [ MODULE_ID_LENGTH, diff --git a/tests/modules/ejector/test_ejector.py b/tests/modules/ejector/test_ejector.py index 4acdf1ed1..894826108 100644 --- a/tests/modules/ejector/test_ejector.py +++ b/tests/modules/ejector/test_ejector.py @@ -1,11 +1,9 @@ -from typing import Iterable, cast +from typing import cast from unittest.mock import Mock import pytest from web3.exceptions import ContractCustomError -from src.providers.execution.contracts.exit_bus_oracle import ExitBusOracleContract -from src import constants from src.constants import ( EFFECTIVE_BALANCE_INCREMENT, GWEI_TO_WEI, @@ -16,19 +14,15 @@ MIN_VALIDATOR_WITHDRAWABILITY_DELAY, ) from src.modules.ejector import ejector as ejector_module -from src.modules.ejector.ejector import ( - Ejector, -) -from src.modules.ejector.ejector import logger as ejector_logger +from src.modules.ejector.ejector import Ejector, logger as ejector_logger from src.modules.ejector.types import EjectorProcessingState from src.modules.submodules.oracle_module import ModuleExecuteDelay from src.modules.submodules.types import ChainConfig, CurrentFrame from src.providers.consensus.types import ( BeaconStateView, ) +from src.providers.execution.contracts.exit_bus_oracle import ExitBusOracleContract from src.types import BlockStamp, Gwei, ReferenceBlockStamp, SlotNumber -from src.utils import validator_state -from src.web3py.extensions.contracts import LidoContracts from src.web3py.extensions.lido_validators import NodeOperatorId, StakingModuleId from src.web3py.types import Web3 from tests.factory.base_oracle import EjectorProcessingStateFactory @@ -36,6 +30,7 @@ from tests.factory.configs import ChainConfigFactory from tests.factory.no_registry import LidoValidatorFactory from tests.modules.accounting.test_safe_border_unit import FAR_FUTURE_EPOCH +from types import SimpleNamespace @pytest.fixture(autouse=True) @@ -93,7 +88,7 @@ def test_ejector_execute_module_on_pause(ejector: Ejector, blockstamp: BlockStam ejector.w3.lido_contracts.validators_exit_bus_oracle.get_contract_version = Mock(return_value=1) ejector.w3.lido_contracts.validators_exit_bus_oracle.get_consensus_version = Mock(return_value=3) ejector.get_blockstamp_for_report = Mock(return_value=blockstamp) - ejector.build_report = Mock(return_value=(1, 294271, 0, 1, b'')) + ejector.build_report = ejector.build_report = lambda bs: SimpleNamespace(as_tuple=lambda: (1, 294271, 0, 1, b'')) ejector.w3.lido_contracts.validators_exit_bus_oracle.is_paused = Mock(return_value=True) result = ejector.execute_module(last_finalized_blockstamp=blockstamp) @@ -106,7 +101,7 @@ def test_ejector_build_report(ejector: Ejector, ref_blockstamp: ReferenceBlockSt ejector.get_validators_to_eject = Mock(return_value=[]) ejector.w3.lido_contracts.validators_exit_bus_oracle.get_last_processing_ref_slot.return_value = SlotNumber(0) - _, ref_slot, _, _, data = ejector.build_report(ref_blockstamp) + _, ref_slot, _, _, data = ejector.build_report(ref_blockstamp).as_tuple() ejector.build_report(ref_blockstamp) assert ref_slot == ref_blockstamp.ref_slot, "Unexpected blockstamp.ref_slot" @@ -422,54 +417,6 @@ def test_get_total_balance(ejector: Ejector, blockstamp: BlockStamp) -> None: ejector.w3.lido_contracts.lido.get_buffered_ether.assert_called_once_with(blockstamp.block_hash) -@pytest.mark.unit -class TestChurnLimit: - """_get_churn_limit tests""" - - @pytest.fixture(autouse=True) - def mock_is_active_validator(self, monkeypatch: pytest.MonkeyPatch) -> Iterable: - with monkeypatch.context() as m: - m.setattr( - ejector_module, - "is_active_validator", - Mock(side_effect=lambda v, _: bool(v)), - ) - yield - - def test_get_churn_limit_no_validators(self, ejector: Ejector, ref_blockstamp: ReferenceBlockStamp) -> None: - ejector.w3.cc.get_validators = Mock(return_value=[]) - result = ejector._get_churn_limit(ref_blockstamp) - assert result == constants.MIN_PER_EPOCH_CHURN_LIMIT, "Unexpected churn limit" - ejector.w3.cc.get_validators.assert_called_once_with(ref_blockstamp) - - def test_get_churn_limit_validators_less_than_min_churn( - self, - ejector: Ejector, - ref_blockstamp: ReferenceBlockStamp, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - with monkeypatch.context() as m: - ejector.w3.cc.get_validators = Mock(return_value=[1, 1, 0]) - result = ejector._get_churn_limit(ref_blockstamp) - assert result == 4, "Unexpected churn limit" - ejector.w3.cc.get_validators.assert_called_once_with(ref_blockstamp) - - def test_get_churn_limit_basic( - self, - ejector: Ejector, - ref_blockstamp: ReferenceBlockStamp, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - with monkeypatch.context() as m: - ejector.w3.cc.get_validators = Mock(return_value=[1] * 99) - m.setattr(validator_state, "MIN_PER_EPOCH_CHURN_LIMIT", 0) - m.setattr(validator_state, "CHURN_LIMIT_QUOTIENT", 2) - result = ejector._get_churn_limit(ref_blockstamp) - assert result == 49, "Unexpected churn limit" - ejector._get_churn_limit(ref_blockstamp) - ejector.w3.cc.get_validators.assert_called_once_with(ref_blockstamp) - - @pytest.mark.unit def test_get_latest_exit_epoch(ejector: Ejector, blockstamp: BlockStamp) -> None: ejector.w3.cc.get_validators = Mock( diff --git a/tests/modules/ejector/test_validator_exit_order_iterator.py b/tests/modules/ejector/test_validator_exit_order_iterator.py index f315c6516..7d4cba631 100644 --- a/tests/modules/ejector/test_validator_exit_order_iterator.py +++ b/tests/modules/ejector/test_validator_exit_order_iterator.py @@ -3,15 +3,11 @@ import pytest -from src.services.exit_order_iterator import ValidatorExitIterator, StakingModuleStats, NodeOperatorStats +from src.services.exit_order_iterator import NodeOperatorStats, StakingModuleStats, ValidatorExitIterator from src.types import Gwei from src.web3py.extensions.lido_validators import NodeOperatorLimitMode from tests.factory.blockstamp import ReferenceBlockStampFactory -from tests.factory.no_registry import ( - NodeOperatorFactory, - StakingModuleFactory, - LidoValidatorFactory, -) +from tests.factory.no_registry import LidoValidatorFactory, NodeOperatorFactory, StakingModuleFactory from tests.factory.web3_factory import Web3DataclassFactory diff --git a/tests/modules/submodules/consensus/conftest.py b/tests/modules/submodules/consensus/conftest.py index 39658b72a..0c8a38e9b 100644 --- a/tests/modules/submodules/consensus/conftest.py +++ b/tests/modules/submodules/consensus/conftest.py @@ -1,6 +1,6 @@ import pytest -from src.modules.submodules.consensus import ConsensusModule +from src.modules.submodules.consensus import ConsensusModule, Report from src.types import BlockStamp, ReferenceBlockStamp @@ -12,8 +12,8 @@ def __init__(self, w3): self.report_contract = w3.lido_contracts.accounting_oracle super().__init__(w3) - def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: - return tuple() + def build_report(self, blockstamp: ReferenceBlockStamp) -> Report: + return type("", (), {"as_tuple": lambda self: ()})() def is_main_data_submitted(self, blockstamp: BlockStamp) -> bool: return True diff --git a/tests/modules/submodules/consensus/test_consensus.py b/tests/modules/submodules/consensus/test_consensus.py index 80c4fb272..c72cad70e 100644 --- a/tests/modules/submodules/consensus/test_consensus.py +++ b/tests/modules/submodules/consensus/test_consensus.py @@ -1,5 +1,5 @@ -from typing import cast from dataclasses import dataclass +from typing import cast from unittest.mock import Mock import pytest @@ -9,18 +9,17 @@ from src import variables from src.modules.submodules import consensus as consensus_module -from src.modules.submodules.consensus import ZERO_HASH, ConsensusModule, IsNotMemberException, MemberInfo -from src.modules.submodules.exceptions import IncompatibleOracleVersion, ContractVersionMismatch +from src.modules.submodules.consensus import ConsensusModule, IsNotMemberException, MemberInfo, ZERO_HASH +from src.modules.submodules.exceptions import ContractVersionMismatch, IncompatibleOracleVersion from src.modules.submodules.types import ChainConfig from src.providers.consensus.types import BeaconSpecResponse from src.types import BlockStamp, ReferenceBlockStamp - -from tests.factory.blockstamp import ReferenceBlockStampFactory, BlockStampFactory +from tests.factory.blockstamp import BlockStampFactory, ReferenceBlockStampFactory from tests.factory.configs import ( BeaconSpecResponseFactory, + BlockDetailsResponseFactory, ChainConfigFactory, FrameConfigFactory, - BlockDetailsResponseFactory, ) from tests.factory.member_info import MemberInfoFactory @@ -328,7 +327,7 @@ class ConsensusImpl(ConsensusModule): """Consensus module implementation for testing purposes""" def build_report(self, _: ReferenceBlockStamp) -> tuple: - return tuple() + return type("", (), {"as_tuple": lambda self: ()})() def is_main_data_submitted(self, _: BlockStamp) -> bool: return True diff --git a/tests/modules/submodules/consensus/test_reports.py b/tests/modules/submodules/consensus/test_reports.py index d2952dd86..b412b0340 100644 --- a/tests/modules/submodules/consensus/test_reports.py +++ b/tests/modules/submodules/consensus/test_reports.py @@ -1,15 +1,14 @@ +from dataclasses import dataclass from unittest.mock import Mock import pytest from eth_typing import ChecksumAddress from hexbytes import HexBytes -from dataclasses import dataclass from src import variables from src.modules.accounting.accounting import Accounting from src.modules.accounting.types import ReportData from src.modules.submodules.types import ChainConfig, FrameConfig, ZERO_HASH - from tests.factory.blockstamp import ReferenceBlockStampFactory from tests.factory.member_info import MemberInfoFactory @@ -54,7 +53,7 @@ def test_process_report_main(consensus, caplog): consensus._send_report_hash = Mock() report_data = ReportData( 1, 2, 3, 4, [5, 6], [7, 8], 9, 10, 11, [12], 13, True, 13, HexBytes(int.to_bytes(14, 32)), 15 - ).as_tuple() + ) consensus.build_report = Mock(return_value=report_data) consensus.process_report(blockstamp) diff --git a/tests/providers_clients/test_keys_api_client.py b/tests/providers_clients/test_keys_api_client.py index cb1008304..ea7ff8a6e 100644 --- a/tests/providers_clients/test_keys_api_client.py +++ b/tests/providers_clients/test_keys_api_client.py @@ -9,8 +9,7 @@ from web3 import Web3 import src.providers.keys.client as keys_api_client_module -from src import constants -from src import variables +from src import constants, variables from src.providers.keys.client import KAPIClientError, KeysAPIClient, KeysOutdatedException from src.providers.keys.types import LidoKey from src.types import StakingModuleAddress