Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Abstract contract interaction #432

Merged
merged 21 commits into from
May 6, 2024
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ branch = true

[tool.pylint.format]
max-line-length = "120"
min-similarity-lines=6


[tool.pylint."messages control"]
Expand Down
4 changes: 2 additions & 2 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from src.modules.accounting.accounting import Accounting
from src.modules.ejector.ejector import Ejector
from src.modules.checks.checks_module import ChecksModule
from src.typings import OracleModule
from src.types import OracleModule
from src.utils.build import get_build_info
from src.web3py.extensions import (
LidoContracts,
Expand All @@ -22,7 +22,7 @@
FallbackProviderModule
)
from src.web3py.middleware import metrics_collector
from src.web3py.typings import Web3
from src.web3py.types import Web3

from src.web3py.contract_tweak import tweak_w3_contracts

Expand Down
60 changes: 15 additions & 45 deletions src/modules/accounting/accounting.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@

from src import variables
from src.constants import SHARE_RATE_PRECISION_E27
from src.modules.accounting.typings import (
from src.modules.accounting.types import (
ReportData,
AccountingProcessingState,
LidoReportRebase,
SharesRequestedToBurn,
)
from src.metrics.prometheus.accounting import (
ACCOUNTING_IS_BUNKER,
Expand All @@ -19,17 +17,17 @@
ACCOUNTING_WITHDRAWAL_VAULT_BALANCE_WEI
)
from src.metrics.prometheus.duration_meter import duration_meter
from src.providers.execution.contracts.accounting_oracle import AccountingOracleContract
from src.services.validator_state import LidoValidatorStateService
from src.modules.submodules.consensus import ConsensusModule
from src.modules.submodules.oracle_module import BaseModule, ModuleExecuteDelay
from src.services.withdrawal import Withdrawal
from src.services.bunker import BunkerService
from src.typings import BlockStamp, Gwei, ReferenceBlockStamp
from src.utils.abi import named_tuple_to_dataclass
from src.types import BlockStamp, Gwei, ReferenceBlockStamp, StakingModuleId, NodeOperatorGlobalIndex
from src.utils.cache import global_lru_cache as lru_cache
from src.variables import ALLOW_REPORTING_IN_BUNKER_MODE
from src.web3py.typings import Web3
from src.web3py.extensions.lido_validators import StakingModule, NodeOperatorGlobalIndex, StakingModuleId
from src.web3py.types import Web3
from src.web3py.extensions.lido_validators import StakingModule


logger = logging.getLogger(__name__)
Expand All @@ -50,7 +48,7 @@ class Accounting(BaseModule, ConsensusModule):
CONTRACT_VERSION = 1

def __init__(self, w3: Web3):
self.report_contract = w3.lido_contracts.accounting_oracle
self.report_contract: AccountingOracleContract = w3.lido_contracts.accounting_oracle
super().__init__(w3)

self.lido_validator_state_service = LidoValidatorStateService(self.w3)
Expand Down Expand Up @@ -93,9 +91,9 @@ def _submit_extra_data(self, blockstamp: ReferenceBlockStamp) -> None:
extra_data = self.lido_validator_state_service.get_extra_data(blockstamp, self.get_chain_config(blockstamp))

if extra_data.extra_data:
tx = self.report_contract.functions.submitReportExtraDataList(extra_data.extra_data)
tx = self.report_contract.submit_report_extra_data_list(extra_data.extra_data)
else:
tx = self.report_contract.functions.submitReportExtraDataEmpty()
tx = self.report_contract.submit_report_extra_data_empty()

self.w3.transaction.check_and_send_transaction(tx, variables.ACCOUNT)

Expand All @@ -108,13 +106,13 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple:

def is_main_data_submitted(self, blockstamp: BlockStamp) -> bool:
# Consensus module: if contract got report data (second phase)
processing_state = self._get_processing_state(blockstamp)
processing_state = self.report_contract.get_processing_state(blockstamp.block_hash)
logger.debug({'msg': 'Check if main data was submitted.', 'value': processing_state.main_data_submitted})
return processing_state.main_data_submitted

def can_submit_extra_data(self, blockstamp: BlockStamp) -> bool:
"""Check if Oracle can submit extra data. Can only be submitted after second phase."""
processing_state = self._get_processing_state(blockstamp)
processing_state = self.report_contract.get_processing_state(blockstamp.block_hash)
return processing_state.main_data_submitted and not processing_state.extra_data_submitted

def is_contract_reportable(self, blockstamp: BlockStamp) -> bool:
Expand All @@ -132,15 +130,6 @@ def is_reporting_allowed(self, blockstamp: ReferenceBlockStamp) -> bool:
logger.warning({'msg': '!' * 50})
return ALLOW_REPORTING_IN_BUNKER_MODE

@lru_cache(maxsize=1)
def _get_processing_state(self, blockstamp: BlockStamp) -> AccountingProcessingState:
ps = named_tuple_to_dataclass(
self.report_contract.functions.getProcessingState().call(block_identifier=blockstamp.block_hash),
AccountingProcessingState,
)
logger.info({'msg': 'Fetch processing state.', 'value': ps})
return ps

# ---------------------------------------- Build report ----------------------------------------
def _calculate_report(self, blockstamp: ReferenceBlockStamp) -> ReportData:
validators_count, cl_balance = self._get_consensus_lido_state(blockstamp)
Expand Down Expand Up @@ -183,7 +172,7 @@ def _get_newly_exited_validators_by_modules(
Calculate exited validators count in all modules.
Exclude modules without changes from the report.
"""
staking_modules = self.w3.lido_validators.get_staking_modules(blockstamp)
staking_modules = self.w3.lido_contracts.staking_router.get_staking_modules(blockstamp.block_hash)
exited_validators = self.lido_validator_state_service.get_exited_lido_validators(blockstamp)

return self.get_updated_modules_stats(staking_modules, exited_validators)
Expand Down Expand Up @@ -260,7 +249,7 @@ def simulate_rebase_after_report(

chain_conf = self.get_chain_config(blockstamp)

simulated_tx = self.w3.lido_contracts.lido.functions.handleOracleReport(
return self.w3.lido_contracts.lido.handle_oracle_report(
# We use block timestamp, instead of slot timestamp,
# because missed slot will break simulation contract logics
# Details: https://github.com/lidofinance/lido-oracle/issues/291
Expand All @@ -273,31 +262,12 @@ def simulate_rebase_after_report(
self.w3.lido_contracts.get_withdrawal_balance(blockstamp), # _withdrawalVaultBalance
el_rewards, # _elRewardsVaultBalance
self.get_shares_to_burn(blockstamp), # _sharesRequestedToBurn
# Decision about withdrawals processing
[], # _lastFinalizableRequestId
0, # _simulatedShareRate
)

logger.info({'msg': 'Simulate lido rebase for report.', 'value': simulated_tx.args})

result = simulated_tx.call(
transaction={'from': self.w3.lido_contracts.accounting_oracle.address},
block_identifier=blockstamp.block_hash,
self.w3.lido_contracts.accounting_oracle.address,
blockstamp.block_hash,
)

logger.info({'msg': 'Fetch simulated lido rebase for report.', 'value': result})

return LidoReportRebase(*result)

@lru_cache(maxsize=1)
def get_shares_to_burn(self, blockstamp: BlockStamp) -> int:
shares_data = named_tuple_to_dataclass(
self.w3.lido_contracts.burner.functions.getSharesRequestedToBurn().call(
block_identifier=blockstamp.block_hash,
),
SharesRequestedToBurn,
)

shares_data = self.w3.lido_contracts.burner.get_shares_requested_to_burn(blockstamp.block_hash)
return shares_data.cover_shares + shares_data.non_cover_shares

def _get_slots_elapsed_from_last_report(self, blockstamp: ReferenceBlockStamp):
Expand Down
6 changes: 3 additions & 3 deletions src/modules/accounting/extra_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

from hexbytes import HexBytes

from src.modules.submodules.typings import ZERO_HASH
from src.web3py.extensions.lido_validators import NodeOperatorGlobalIndex
from src.web3py.typings import Web3
from src.modules.submodules.types import ZERO_HASH
from src.types import NodeOperatorGlobalIndex
from src.web3py.types import Web3


class ItemType(Enum):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
from hexbytes import HexBytes
from web3.types import Wei

from src.typings import SlotNumber, Gwei
from src.web3py.extensions.lido_validators import StakingModuleId
from src.types import SlotNumber, Gwei, StakingModuleId


@dataclass
Expand Down Expand Up @@ -81,29 +80,33 @@ class LidoReportRebase:
el_reward: Wei


@dataclass
class Account:
address: ChecksumAddress
_private_key: HexBytes


@dataclass
class BatchState:
remaining_eth_budget: int
finished: bool
batches: list[int]
batches: tuple[int, ...]
batches_length: int

def as_tuple(self):
return (
self.remaining_eth_budget,
self.finished,
self.batches,
self.batches_length
self.batches_length,
)


@dataclass
class SharesRequestedToBurn:
cover_shares: int
non_cover_shares: int


@dataclass
class WithdrawalRequestStatus:
amountOfStETH: int
amountOfShares: int
owner: ChecksumAddress
timestamp: int
is_finalized: bool
is_claimed: bool
4 changes: 2 additions & 2 deletions src/modules/checks/suites/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from xdist.dsession import TerminalDistReporter # type: ignore[import]

from src import variables
from src.typings import EpochNumber, SlotNumber, BlockRoot
from src.types import EpochNumber, SlotNumber, BlockRoot
from src.utils.blockstamp import build_blockstamp
from src.utils.slot import get_reference_blockstamp
from src.web3py.contract_tweak import tweak_w3_contracts
Expand All @@ -16,7 +16,7 @@
LidoContracts,
FallbackProviderModule,
)
from src.web3py.typings import Web3
from src.web3py.types import Web3


TITLE_PROPERTY_NAME = "test_title"
Expand Down
2 changes: 1 addition & 1 deletion src/modules/checks/suites/consensus_node.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Consensus node"""
from src.web3py.typings import Web3
from src.web3py.types import Web3


def check_validators_provided(web3: Web3, blockstamp):
Expand Down
56 changes: 13 additions & 43 deletions src/modules/ejector/ejector.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,23 @@
)
from src.metrics.prometheus.duration_meter import duration_meter
from src.modules.ejector.data_encode import encode_data
from src.modules.ejector.typings import EjectorProcessingState, ReportData
from src.modules.ejector.types import ReportData
from src.modules.submodules.consensus import ConsensusModule
from src.modules.submodules.oracle_module import BaseModule, ModuleExecuteDelay
from src.providers.consensus.typings import Validator
from src.providers.consensus.types import Validator
from src.providers.execution.contracts.exit_bus_oracle import ExitBusOracleContract
from src.services.exit_order_iterator import ExitOrderIterator
from src.services.prediction import RewardsPredictionService
from src.services.validator_state import LidoValidatorStateService
from src.typings import BlockStamp, EpochNumber, ReferenceBlockStamp
from src.utils.abi import named_tuple_to_dataclass
from src.types import BlockStamp, EpochNumber, ReferenceBlockStamp, NodeOperatorGlobalIndex
from src.utils.cache import global_lru_cache as lru_cache
from src.utils.validator_state import (
is_active_validator,
is_fully_withdrawable_validator,
is_partially_withdrawable_validator,
)
from src.web3py.extensions.lido_validators import LidoValidator, NodeOperatorGlobalIndex
from src.web3py.typings import Web3
from src.web3py.extensions.lido_validators import LidoValidator
from src.web3py.types import Web3

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -64,7 +64,8 @@ class Ejector(BaseModule, ConsensusModule):
AVG_EXPECTING_WITHDRAWALS_SWEEP_DURATION_MULTIPLIER = 0.5

def __init__(self, w3: Web3):
self.report_contract = w3.lido_contracts.validators_exit_bus_oracle
self.report_contract: ExitBusOracleContract = w3.lido_contracts.validators_exit_bus_oracle

super().__init__(w3)

self.prediction_service = RewardsPredictionService(w3)
Expand Down Expand Up @@ -109,7 +110,7 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple:
return report_data.as_tuple()

def get_validators_to_eject(self, blockstamp: ReferenceBlockStamp) -> list[tuple[NodeOperatorGlobalIndex, LidoValidator]]:
to_withdraw_amount = self.get_total_unfinalized_withdrawal_requests_amount(blockstamp)
to_withdraw_amount = self.w3.lido_contracts.withdrawal_queue_nft.unfinalized_steth(blockstamp.block_hash)
logger.info({'msg': 'Calculate to withdraw amount.', 'value': to_withdraw_amount})

EJECTOR_TO_WITHDRAW_WEI_AMOUNT.set(to_withdraw_amount)
Expand Down Expand Up @@ -185,11 +186,8 @@ def get_validators_to_eject(self, blockstamp: ReferenceBlockStamp) -> list[tuple

return validators_to_eject

def _is_paused(self, blockstamp: ReferenceBlockStamp) -> bool:
return self.report_contract.functions.isPaused().call(block_identifier=blockstamp.block_hash)

def is_reporting_allowed(self, blockstamp: ReferenceBlockStamp) -> bool:
on_pause = self._is_paused(blockstamp)
on_pause = self.report_contract.is_paused(blockstamp.block_hash)
CONTRACT_ON_PAUSE.labels('reporting').set(on_pause)
logger.info({'msg': 'Fetch isPaused from ejector bus contract.', 'value': on_pause})
return not on_pause
Expand Down Expand Up @@ -218,31 +216,11 @@ def _get_predicted_withdrawable_balance(self, validator: Validator) -> Wei:
return self.w3.to_wei(min(int(validator.balance), MAX_EFFECTIVE_BALANCE), 'gwei')

def _get_total_el_balance(self, blockstamp: BlockStamp) -> Wei:
total_el_balance = Wei(
return Wei(
self.w3.lido_contracts.get_el_vault_balance(blockstamp) +
self.w3.lido_contracts.get_withdrawal_balance(blockstamp) +
self._get_buffer_ether(blockstamp)
self.w3.lido_contracts.lido.get_buffered_ether(blockstamp.block_hash)
)
return total_el_balance

def _get_buffer_ether(self, blockstamp: BlockStamp) -> Wei:
"""
The reserved buffered ether is min(current_buffered_ether, unfinalized_withdrawal_requests_amount)
We can skip calculating reserved buffer for ejector, because in case if
(unfinalized_withdrawal_requests_amount <= current_buffered_ether)
We won't eject validators at all, because we have enough eth to fulfill all requests.
"""
return Wei(
self.w3.lido_contracts.lido.functions.getBufferedEther().call(
block_identifier=blockstamp.block_hash
)
)

def get_total_unfinalized_withdrawal_requests_amount(self, blockstamp: BlockStamp) -> Wei:
unfinalized_steth = self.w3.lido_contracts.withdrawal_queue_nft.functions.unfinalizedStETH().call(
block_identifier=blockstamp.block_hash,
)
return unfinalized_steth

def _get_predicted_withdrawable_epoch(
self,
Expand Down Expand Up @@ -330,16 +308,8 @@ def _get_churn_limit(self, blockstamp: ReferenceBlockStamp) -> int:
logger.info({'msg': 'Calculate churn limit.', 'value': churn_limit})
return churn_limit

def _get_processing_state(self, blockstamp: BlockStamp) -> EjectorProcessingState:
ps = named_tuple_to_dataclass(
self.report_contract.functions.getProcessingState().call(block_identifier=blockstamp.block_hash),
EjectorProcessingState,
)
logger.info({'msg': 'Fetch processing state.', 'value': ps})
return ps

def is_main_data_submitted(self, blockstamp: BlockStamp) -> bool:
processing_state = self._get_processing_state(blockstamp)
processing_state = self.report_contract.get_processing_state(blockstamp.block_hash)
return processing_state.data_submitted

def is_contract_reportable(self, blockstamp: BlockStamp) -> bool:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from dataclasses import dataclass

from src.typings import SlotNumber
from src.types import SlotNumber


@dataclass
Expand Down
Loading
Loading