Skip to content

Commit 233f6de

Browse files
authored
Merge pull request #790 from lidofinance/feat/validator-stages
feat: use lazy oracle to check validator stages
2 parents 7582f00 + d5a05c3 commit 233f6de

26 files changed

+577
-297
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,8 @@ In manual mode all sleeps are disabled and `ALLOW_REPORTING_IN_BUNKER_MODE` is T
222222
| `CACHE_PATH` | Directory to store cache for CSM module | False | `.` |
223223
| `OPSGENIE_API_KEY` | OpsGenie API key for authentication with the OpsGenie API. Used to send alerts from lido-oracle health-checks. | False | `<api-key>` |
224224
| `OPSGENIE_API_URL` | Base URL for the OpsGenie API. | False | `http://localhost:8080` |
225-
| `VAULT_PAGINATION_LIMIT` | The limit for getting staking vaults with pagination. Default 1000 | False | `http://localhost:8080` |
225+
| `VAULT_PAGINATION_LIMIT` | The limit for getting staking vaults with pagination. Default 1000 | False | `1000` |
226+
| `VAULT_VALIDATOR_STAGES_BATCH_SIZE` | The limit for getting validators stages in one request. Default 100 | False | `100` |
226227
227228
### Mainnet variables
228229
> LIDO_LOCATOR_ADDRESS=0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb

assets/LazyOracle.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ license = "GPL 3.0"
1818

1919
[tool.poetry.dependencies]
2020
python = "^3.12"
21-
prometheus-client = "0.21.1"
21+
prometheus-client = "^0.21.1"
2222
timeout-decorator = "^0.5.0"
2323
pytest = "^7.2.1"
2424
pytest-xdist = "^3.2.1"

src/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414
# https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#gwei-values
1515
EFFECTIVE_BALANCE_INCREMENT = Gwei(2**0 * 10**9)
1616
MAX_EFFECTIVE_BALANCE = Gwei(32 * 10**9)
17+
MIN_DEPOSIT_AMOUNT = Gwei(2**0 * 10**9)
1718
# https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#gwei-values
1819
MAX_EFFECTIVE_BALANCE_ELECTRA = Gwei(2**11 * 10**9)
1920
MIN_ACTIVATION_BALANCE = Gwei(2**5 * 10**9)
20-
PDG_ACTIVATION_DEPOSIT = Gwei(31 * 10**9)
2121
# https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#execution
2222
MAX_WITHDRAWALS_PER_PAYLOAD = 2**4
2323
# https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#withdrawal-prefixes

src/modules/accounting/accounting.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -243,12 +243,14 @@ def _get_consensus_lido_state(self, blockstamp: ReferenceBlockStamp) -> tuple[Va
243243
total_lido_balance = lido_validators_state_balance = sum((validator.balance for validator in lido_validators), Gwei(0))
244244
logger.info({
245245
'msg': 'Calculate Lido validators state balance (in Gwei)',
246-
'value': lido_validators_state_balance
246+
'value': lido_validators_state_balance,
247247
})
248248

249249
return ValidatorsCount(len(lido_validators)), ValidatorsBalance(Gwei(total_lido_balance))
250250

251-
def _get_finalization_data(self, blockstamp: ReferenceBlockStamp) -> tuple[FinalizationBatches, FinalizationShareRate]:
251+
def _get_finalization_data(
252+
self, blockstamp: ReferenceBlockStamp
253+
) -> tuple[FinalizationBatches, FinalizationShareRate]:
252254
simulation = self.simulate_full_rebase(blockstamp)
253255
chain_config = self.get_chain_config(blockstamp)
254256
frame_config = self.get_frame_config(blockstamp)
@@ -412,7 +414,7 @@ def _handle_vaults_report(self, blockstamp: ReferenceBlockStamp) -> VaultsReport
412414

413415
vaults = self.staking_vaults.get_vaults(blockstamp.block_hash)
414416
if len(vaults) == 0:
415-
return b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ''
417+
return ZERO_HASH, ''
416418

417419
current_frame = self.get_frame_number_by_slot(blockstamp)
418420
validators = self.w3.cc.get_validators(blockstamp)
@@ -421,6 +423,13 @@ def _handle_vaults_report(self, blockstamp: ReferenceBlockStamp) -> VaultsReport
421423
frame_config = self.get_frame_config(blockstamp)
422424
simulation = self.simulate_full_rebase(blockstamp)
423425

426+
vaults_total_values = self.staking_vaults.get_vaults_total_values(
427+
vaults=vaults,
428+
validators=validators,
429+
pending_deposits=pending_deposits,
430+
block_identifier=blockstamp.block_hash
431+
)
432+
424433
core_apr_ratio = calculate_gross_core_apr(
425434
pre_total_ether=simulation.pre_total_pooled_ether,
426435
pre_total_shares=simulation.pre_total_shares,
@@ -430,12 +439,6 @@ def _handle_vaults_report(self, blockstamp: ReferenceBlockStamp) -> VaultsReport
430439
time_elapsed_seconds=self._get_time_elapsed_seconds_from_prev_report(blockstamp),
431440
)
432441

433-
vaults_total_values = self.staking_vaults.get_vaults_total_values(
434-
vaults=vaults,
435-
validators=validators,
436-
pending_deposits=pending_deposits,
437-
)
438-
439442
latest_onchain_ipfs_report_data = self.staking_vaults.get_latest_onchain_ipfs_report_data(blockstamp.block_hash)
440443
vaults_fees = self.staking_vaults.get_vaults_fees(
441444
blockstamp=blockstamp,

src/modules/accounting/events.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from dataclasses import dataclass
2+
from typing import Any, Dict, Union
23

34
from eth_typing import BlockNumber
45
from hexbytes import HexBytes
5-
from typing import Any, Dict, Union
66
from web3.types import EventData
77

8+
89
@dataclass(frozen=True)
910
class EventBase:
1011
event: str
@@ -144,6 +145,7 @@ def from_log(cls, log: EventData) -> "VaultConnectedEvent":
144145
**cls._extract_common(log),
145146
)
146147

148+
147149
@dataclass(frozen=True)
148150
class BadDebtWrittenOffToBeInternalizedEvent(EventBase):
149151
vault: str
@@ -158,14 +160,15 @@ def from_log(cls, log: EventData) -> "BadDebtWrittenOffToBeInternalizedEvent":
158160
**cls._extract_common(log),
159161
)
160162

163+
161164
VaultEventType = Union[
162165
MintedSharesOnVaultEvent,
163166
BurnedSharesOnVaultEvent,
164167
VaultFeesUpdatedEvent,
165168
VaultRebalancedEvent,
166169
BadDebtSocializedEvent,
167170
BadDebtWrittenOffToBeInternalizedEvent,
168-
VaultConnectedEvent
171+
VaultConnectedEvent,
169172
]
170173

171174

src/modules/accounting/types.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ def from_response(cls, **kwargs) -> Self:
132132
class BatchState:
133133
remaining_eth_budget: int
134134
finished: bool
135-
batches: tuple[int, ...]
135+
batches: list[int]
136136
batches_length: int
137137

138138
def as_tuple(self):
@@ -231,7 +231,7 @@ class OnChainIpfsVaultReportData(Nested, FromResponse):
231231
@dataclass
232232
class VaultInfo(Nested, FromResponse):
233233
vault: ChecksumAddress
234-
aggregate_balance: Wei
234+
aggregated_balance: Wei
235235
in_out_delta: Wei
236236
withdrawal_credentials: str
237237
liability_shares: Shares
@@ -417,3 +417,10 @@ def total(self) -> Gwei:
417417
@property
418418
def max(self) -> Gwei:
419419
return Gwei(max((deposit.amount for deposit in self.pending_deposits), default=0))
420+
421+
class ValidatorStage(Enum):
422+
NONE = 0
423+
PREDEPOSITED = 1
424+
PROVEN = 2
425+
ACTIVATED = 3
426+
COMPENSATED = 4

src/providers/consensus/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ def get_state_view_no_cache(self, blockstamp: BlockStamp) -> BeaconStateView:
250250
def get_pending_deposits(self, blockstamp: BlockStamp) -> list[PendingDeposit]:
251251
return self.get_state_view(blockstamp).pending_deposits
252252

253-
def get_validator_state(self, state_id: SlotNumber | BlockRoot, validator_id: int) -> Validator:
253+
def get_validator_state(self, state_id: SlotNumber, validator_id: int) -> Validator:
254254
"""Spec: https://ethereum.github.io/beacon-APIs/#/Beacon/getStateValidator"""
255255
data, _ = self._get(
256256
self.API_GET_VALIDATOR,

src/providers/execution/contracts/accounting.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
from web3.types import BlockIdentifier
44

5-
from src.modules.accounting.types import ReportSimulationPayload, ReportSimulationResults
5+
from src.modules.accounting.types import (
6+
ReportSimulationPayload,
7+
ReportSimulationResults,
8+
)
69
from src.providers.execution.base_interface import ContractInterface
710

811
logger = logging.getLogger(__name__)
@@ -23,9 +26,7 @@ def simulate_oracle_report(
2326
plugging the returned values to the following formula: `_simulatedShareRate = (postTotalPooledEther * 1e27) / postTotalShares`
2427
"""
2528

26-
response = self.functions.simulateOracleReport(payload.as_tuple()).call(
27-
block_identifier=block_identifier
28-
)
29+
response = self.functions.simulateOracleReport(payload.as_tuple()).call(block_identifier=block_identifier)
2930

3031
response = ReportSimulationResults(*response)
3132

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
import logging
22

3+
from hexbytes import HexBytes
34
from web3 import Web3
45
from web3.types import BlockIdentifier
56

67
from src import variables
7-
from src.modules.accounting.types import OnChainIpfsVaultReportData, VaultInfo
8+
from src.modules.accounting.types import (
9+
OnChainIpfsVaultReportData,
10+
ValidatorStage,
11+
VaultInfo,
12+
)
813
from src.providers.execution.base_interface import ContractInterface
914
from src.utils.abi import named_tuple_to_dataclass
1015
from src.utils.cache import global_lru_cache as lru_cache
1116

1217
logger = logging.getLogger(__name__)
1318

1419

15-
class VaultsLazyOracleContract(ContractInterface):
20+
class LazyOracleContract(ContractInterface):
1621
abi_path = './assets/LazyOracle.json'
1722

1823
@lru_cache(maxsize=1)
@@ -22,28 +27,24 @@ def get_vaults_count(self, block_identifier: BlockIdentifier = 'latest') -> int:
2227
"""
2328
response = self.functions.vaultsCount.call(block_identifier=block_identifier)
2429

25-
logger.info(
26-
{
27-
'msg': 'Call `vaultsCount().',
28-
'value': response,
29-
'block_identifier': repr(block_identifier),
30-
'to': self.address,
31-
}
32-
)
30+
logger.info({
31+
'msg': 'Call `vaultsCount().',
32+
'value': response,
33+
'block_identifier': repr(block_identifier),
34+
'to': self.address,
35+
})
3336

3437
return response
3538

3639
def get_latest_report_data(self, block_identifier: BlockIdentifier = 'latest') -> OnChainIpfsVaultReportData:
3740
response = self.functions.latestReportData.call(block_identifier=block_identifier)
3841

39-
logger.info(
40-
{
41-
'msg': 'Call `latestReportData()`.',
42-
'value': response,
43-
'block_identifier': repr(block_identifier),
44-
'to': self.address,
45-
}
46-
)
42+
logger.info({
43+
'msg': 'Call `latestReportData()`.',
44+
'value': response,
45+
'block_identifier': repr(block_identifier),
46+
'to': self.address,
47+
})
4748

4849
response = named_tuple_to_dataclass(response, OnChainIpfsVaultReportData)
4950
return response
@@ -58,7 +59,7 @@ def get_vaults(self, offset: int, limit: int, block_identifier: BlockIdentifier
5859
for vault in response:
5960
out.append(VaultInfo(
6061
vault=vault.vault,
61-
aggregate_balance=vault.aggregateBalance,
62+
aggregated_balance=vault.aggregatedBalance,
6263
in_out_delta=vault.inOutDelta,
6364
withdrawal_credentials=Web3.to_hex(vault.withdrawalCredentials),
6465
liability_shares=vault.liabilityShares,
@@ -73,14 +74,12 @@ def get_vaults(self, offset: int, limit: int, block_identifier: BlockIdentifier
7374
pending_disconnect=vault.pendingDisconnect,
7475
))
7576

76-
logger.info(
77-
{
78-
'msg': f'Call `batchVaultsInfo({offset}, {limit}).',
79-
'value': response,
80-
'block_identifier': repr(block_identifier),
81-
'to': self.address,
82-
}
83-
)
77+
logger.info({
78+
'msg': f'Call `batchVaultsInfo({offset}, {limit}).',
79+
'value': response,
80+
'block_identifier': repr(block_identifier),
81+
'to': self.address,
82+
})
8483

8584
return out
8685

@@ -96,10 +95,40 @@ def get_all_vaults(self, block_identifier: BlockIdentifier = 'latest') -> list[V
9695
return []
9796

9897
while offset < total_count:
99-
batch = self.get_vaults(block_identifier=block_identifier, offset=offset, limit=variables.VAULT_PAGINATION_LIMIT)
98+
batch = self.get_vaults(
99+
block_identifier=block_identifier, offset=offset, limit=variables.VAULT_PAGINATION_LIMIT
100+
)
100101
if not batch:
101102
break
102103
vaults.extend(batch)
103104
offset += variables.VAULT_PAGINATION_LIMIT
104105

105106
return vaults
107+
108+
def get_validator_stages(
109+
self,
110+
pubkeys: list[str],
111+
batch_size: int = variables.VAULT_VALIDATOR_STAGES_BATCH_SIZE,
112+
block_identifier: BlockIdentifier = 'latest'
113+
) -> dict[str, ValidatorStage]:
114+
"""
115+
Fetch validator stages for a list of pubkeys, batching requests for efficiency.
116+
"""
117+
out: dict[str, ValidatorStage] = {}
118+
119+
for i in range(0, len(pubkeys), batch_size):
120+
batch = list(map(HexBytes, pubkeys[i : i + batch_size]))
121+
response = self.functions.batchValidatorStages(batch).call(block_identifier=block_identifier)
122+
123+
logger.debug({
124+
'msg': 'Call `batchValidatorStages()`.',
125+
'count': len(batch),
126+
'block_identifier': repr(block_identifier),
127+
'to': self.address,
128+
})
129+
130+
out.update({
131+
str(pk): ValidatorStage(stage) for pk, stage in zip(batch, response)
132+
})
133+
134+
return out

0 commit comments

Comments
 (0)