Skip to content

Commit 9b19831

Browse files
authored
Merge pull request #325 from lidofinance/develop
Develop
2 parents adc7d30 + 5b721a6 commit 9b19831

File tree

11 files changed

+112
-102
lines changed

11 files changed

+112
-102
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,6 @@ repos:
2222
hooks:
2323
# Run the linter.
2424
- id: ruff
25-
args: [ --fix ]
25+
args: [ --fix, --extend-ignore=B019 ]
2626
# Run the formatter.
2727
- id: ruff-format

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ Unvetting is the proces of decreasing approved depositable signing keys.
4545
4. Send metrics and logs to grafana
4646
5. Setup alerts
4747

48+
## Redeployment
49+
50+
Next cases requires bot restart:
51+
- Contract update
52+
- New staking module added or removed
53+
- DSM contract update
54+
- New guardian added or removed
55+
4856
## Variables
4957

5058
### Required variables

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ markers = [
4141
line-length = 140
4242

4343
[tool.ruff.lint]
44+
ignore = ["B019"]
4445
extend-select = [
4546
# pycodestyle
4647
"E",

src/blockchain/contracts/deposit_security_module.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import logging
2+
from functools import lru_cache
23

34
from blockchain.contracts.base_interface import ContractInterface
45
from eth_account.account import VRS
56
from eth_typing import ChecksumAddress, Hash32
67
from metrics.metrics import CAN_DEPOSIT
78
from web3.contract.contract import ContractFunction
8-
from web3.exceptions import ABIFunctionNotFound, ContractLogicError
99
from web3.types import BlockIdentifier
1010

1111
logger = logging.getLogger(__name__)
@@ -14,18 +14,21 @@
1414
class DepositSecurityModuleContract(ContractInterface):
1515
abi_path = './interfaces/DepositSecurityModule.json'
1616

17+
@lru_cache(maxsize=1)
1718
def get_guardian_quorum(self, block_identifier: BlockIdentifier = 'latest') -> int:
1819
"""Returns number of valid guardian signatures required to vet (depositRoot, nonce) pair."""
1920
response = self.functions.getGuardianQuorum().call(block_identifier=block_identifier)
2021
logger.info({'msg': 'Call `getGuardianQuorum()`.', 'value': response, 'block_identifier': repr(block_identifier)})
2122
return response
2223

24+
@lru_cache(maxsize=1)
2325
def get_guardians(self, block_identifier: BlockIdentifier = 'latest') -> list[ChecksumAddress]:
2426
"""Returns guardian committee member list."""
2527
response = self.functions.getGuardians().call(block_identifier=block_identifier)
2628
logger.info({'msg': 'Call `getGuardians()`.', 'value': response, 'block_identifier': repr(block_identifier)})
2729
return response
2830

31+
@lru_cache(maxsize=1)
2932
def get_attest_message_prefix(self, block_identifier: BlockIdentifier = 'latest') -> bytes:
3033
response = self.functions.ATTEST_MESSAGE_PREFIX().call(block_identifier=block_identifier)
3134
logger.info({'msg': 'Call `ATTEST_MESSAGE_PREFIX()`.', 'value': response.hex(), 'block_identifier': repr(block_identifier)})
@@ -85,11 +88,13 @@ def deposit_buffered_ether(
8588
)
8689
return tx
8790

91+
@lru_cache(maxsize=1)
8892
def get_pause_message_prefix(self, block_identifier: BlockIdentifier = 'latest') -> bytes:
8993
response = self.functions.PAUSE_MESSAGE_PREFIX().call(block_identifier=block_identifier)
9094
logger.info({'msg': 'Call `PAUSE_MESSAGE_PREFIX()`.', 'value': response.hex(), 'block_identifier': repr(block_identifier)})
9195
return response
9296

97+
@lru_cache(maxsize=1)
9398
def get_pause_intent_validity_period_blocks(self, block_identifier: BlockIdentifier = 'latest') -> int:
9499
"""Returns current `pauseIntentValidityPeriodBlocks` contract parameter (see `pauseDeposits`)."""
95100
response = self.functions.getPauseIntentValidityPeriodBlocks().call(block_identifier=block_identifier)
@@ -120,18 +125,9 @@ def pause_deposits(
120125
logger.info({'msg': f'Build `pauseDeposits({block_number}, {staking_module_id}, {guardian_signature})` tx.'})
121126
return tx
122127

128+
@lru_cache(maxsize=1)
123129
def version(self, block_identifier: BlockIdentifier = 'latest') -> int:
124-
try:
125-
response = self.functions.VERSION().call(block_identifier=block_identifier)
126-
except (ContractLogicError, ABIFunctionNotFound):
127-
logger.info(
128-
{
129-
'msg': 'Call `VERSION()`.',
130-
'value': 'Error: Contract logic error',
131-
'block_identifier': repr(block_identifier),
132-
}
133-
)
134-
return 1
130+
response = self.functions.VERSION().call(block_identifier=block_identifier)
135131

136132
logger.info(
137133
{
@@ -195,6 +191,7 @@ def pause_deposits_v2(
195191
logger.info({'msg': f'Build `pauseDeposits({block_number}, {guardian_signature})` tx.'})
196192
return tx
197193

194+
@lru_cache(maxsize=1)
198195
def get_unvet_message_prefix(self, block_identifier: BlockIdentifier = 'latest') -> bytes:
199196
response = self.functions.UNVET_MESSAGE_PREFIX().call(block_identifier=block_identifier)
200197
logger.info({'msg': 'Call `UNVET_MESSAGE_PREFIX()`.', 'value': response.hex(), 'block_identifier': repr(block_identifier)})
@@ -241,6 +238,7 @@ def is_deposits_paused(self, block_identifier: BlockIdentifier = 'latest') -> bo
241238
)
242239
return response
243240

241+
@lru_cache(maxsize=1)
244242
def get_max_operators_per_unvetting(self, block_identifier: BlockIdentifier = 'latest') -> int:
245243
response = self.functions.getMaxOperatorsPerUnvetting().call(block_identifier=block_identifier)
246244
logger.info(

src/blockchain/contracts/staking_router.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
from functools import lru_cache
23

34
from blockchain.contracts.base_interface import ContractInterface
45
from web3.types import BlockIdentifier, Wei
@@ -9,6 +10,7 @@
910
class StakingRouterContract(ContractInterface):
1011
abi_path = './interfaces/StakingRouter.json'
1112

13+
@lru_cache(maxsize=1)
1214
def get_contract_version(self, block_identifier: BlockIdentifier = 'latest') -> int:
1315
response = self.functions.getContractVersion().call(block_identifier=block_identifier)
1416
logger.info(
@@ -20,6 +22,7 @@ def get_contract_version(self, block_identifier: BlockIdentifier = 'latest') ->
2022
)
2123
return response
2224

25+
@lru_cache(maxsize=1)
2326
def get_staking_module_ids(self, block_identifier: BlockIdentifier = 'latest') -> list[int]:
2427
"""Returns the ids of all registered staking modules"""
2528
response = self.functions.getStakingModuleIds().call(block_identifier=block_identifier)

src/blockchain/executor.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,10 @@ def execute_as_daemon(self) -> None:
4747
def _wait_for_new_block_and_execute(self) -> Any:
4848
healthcheck_pulse.pulse()
4949

50-
if self.w3.lido.has_contract_address_changed():
51-
logger.info({'msg': 'Contract addresses have been updated.'})
50+
# To avoid re-fetching contract addresses every cycle and decrease consumtion
51+
# Requires restart each protocol upgrade including DSM contract or Staking Modules.
52+
# if self.w3.lido.has_contract_address_changed():
53+
# logger.info({'msg': 'Contract addresses have been updated.'})
5254

5355
latest_block = self._exception_handler(self._wait_until_next_block)
5456
result = self._exception_handler(self._execute_function, latest_block)

src/bots/depositor.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -105,17 +105,21 @@ def __init__(
105105
def execute(self, block: BlockData) -> bool:
106106
self._check_balance()
107107

108-
for module_id in self._get_preferred_to_deposit_modules():
108+
modules_to_deposit = self._get_preferred_to_deposit_modules()
109+
110+
if not modules_to_deposit:
111+
# No modules expected. Long sleep.
112+
return True
113+
114+
for module_id in modules_to_deposit:
109115
logger.info({'msg': f'Do deposit to module with id: {module_id}.'})
110116

111117
result = self._deposit_to_module(module_id)
112118
logger.info({'msg': f'Deposit status to Module[{module_id}]: {result}.', 'value': result})
113119

114-
if variables.DEPOSIT_TO_FIRST_HEALTHY_MODULE_ONLY or result:
120+
if result:
115121
return result
116122

117-
logger.warning({'msg': f'Deposit to module with id: {module_id} failed.'})
118-
119123
return False
120124

121125
def _check_balance(self):
@@ -128,11 +132,13 @@ def _check_balance(self):
128132

129133
guardians = self.w3.lido.deposit_security_module.get_guardians()
130134
providers = [self.w3]
135+
131136
if self._onchain_transport_w3 is not None:
132137
providers.append(self._onchain_transport_w3)
138+
133139
for address in guardians:
134140
for provider in providers:
135-
balance = self.w3.eth.get_balance(address)
141+
balance = provider.eth.get_balance(address)
136142
GUARDIAN_BALANCE.labels(address=address, chain_id=provider.eth.chain_id).set(balance)
137143

138144
def _deposit_to_module(self, module_id: int) -> bool:
@@ -251,18 +257,22 @@ def _fetch_actual_messages(self) -> list[BotMessage]:
251257
return self.message_storage.get_messages_and_actualize(lambda x: sign_filter(x) and actualize_filter(x))
252258

253259
def _get_preferred_to_deposit_modules(self) -> list[int]:
254-
# gather quorum
255-
now = datetime.now()
256-
for module_id in variables.DEPOSIT_MODULES_WHITELIST:
257-
if self._get_quorum(module_id):
258-
self._module_last_heart_beat[module_id] = now
259-
260260
# filter out non allow-listed modules
261261
module_ids = [
262262
module_id
263263
for module_id in self.w3.lido.staking_router.get_staking_module_ids()
264264
if module_id in variables.DEPOSIT_MODULES_WHITELIST
265265
]
266+
267+
# gather quorum
268+
now = datetime.now()
269+
for module_id in module_ids:
270+
# Just for metrics
271+
self._select_strategy(module_id).is_gas_price_ok(module_id)
272+
273+
if self._get_quorum(module_id):
274+
self._module_last_heart_beat[module_id] = now
275+
266276
# get digests for all the modules
267277
module_digests = self.w3.lido.staking_router.get_staking_module_digests(module_ids)
268278
# sort modules by validator count
@@ -277,6 +287,7 @@ def _get_preferred_to_deposit_modules(self) -> list[int]:
277287
# take all the modules in sorted order until the first healthy one(including)
278288
result = self._take_until_first_healthy_module(modules_healthiness)
279289
logger.info({'msg': f'Module iteration order {result}.'})
290+
280291
return result
281292

282293
def _is_module_healthy(self, module_id: int) -> bool:
@@ -305,4 +316,7 @@ def _take_until_first_healthy_module(sorted_modules_healthiness: list[Tuple[int,
305316
module_ids.append(module_id)
306317
if is_healthy:
307318
break
319+
else:
320+
# If all modules are unhealthy
321+
return []
308322
return module_ids

src/bots/pauser.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ def run_pauser(w3: Web3):
2626
e = Executor(
2727
w3,
2828
pause.execute,
29-
variables.BLOCKS_BETWEEN_EXECUTION,
29+
# Always one due to high priority
30+
1,
3031
variables.MAX_CYCLE_LIFETIME_IN_SECONDS,
3132
)
3233
logger.info({'msg': 'Execute pauser as daemon.'})

src/variables.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@
8888
# List of ids of staking modules in which the depositor bot will make deposits
8989
_env_whitelist = os.getenv('DEPOSIT_MODULES_WHITELIST', '').strip()
9090
DEPOSIT_MODULES_WHITELIST = [int(module_id) for module_id in _env_whitelist.split(',')] if _env_whitelist else []
91-
BLOCKS_BETWEEN_EXECUTION = int(os.getenv('BLOCKS_BETWEEN_EXECUTION', 1))
91+
# Same as min deposit block distance on mainnet for all modules
92+
# https://etherscan.io/address/0xFdDf38947aFB03C621C71b06C9C70bce73f12999#readProxyContract#F38
93+
BLOCKS_BETWEEN_EXECUTION = int(os.getenv('BLOCKS_BETWEEN_EXECUTION', 25))
9294

9395
"""
9496
GAS_ADDENDUM is used to increase number of deposits during to calm market. The value should be increased if bot

tests/bots/test_depositor.py

Lines changed: 28 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,22 @@ def test_get_preferred_to_deposit_modules(self, mock_datetime):
4343
# Mock staking router module IDs and digests
4444
self.mock_w3.lido.staking_router.get_staking_module_ids.return_value = [1, 2, 3]
4545
self.mock_w3.lido.staking_router.get_staking_module_digests.return_value = [
46-
[0, 0, [1], [5, 2]], # Module 1: 3 validators
47-
[0, 0, [2], [7, 6]], # Module 2: 1 validator
48-
[0, 0, [3], [10, 4]], # Module 3: 6 validators
46+
[0, 0, [1], [5, 8]], # Module 1: 3 active validators
47+
[0, 0, [2], [7, 8]], # Module 2: 1 active validator
48+
[0, 0, [3], [10, 16]], # Module 3: 6 active validators
4949
]
50+
self.bot._get_quorum = Mock()
51+
self.bot._select_strategy = Mock()
5052

5153
# Mock module healthiness and quorum
52-
self.bot._get_quorum = MagicMock(side_effect=[True, False, True])
54+
# Module ID: 2 1 3
5355
self.bot._is_module_healthy = MagicMock(side_effect=[True, False, True])
5456

5557
# Call the method
5658
result = self.bot._get_preferred_to_deposit_modules()
5759

58-
# Expected output: Module 3 (6 validators, healthy)
59-
self.assertEqual([3], result)
60+
# Expected output: Module 2 (1 active)
61+
self.assertEqual([2], result)
6062

6163
# Verify calls to dependent methods
6264
self.bot._get_quorum.assert_any_call(1)
@@ -84,27 +86,26 @@ def test_no_healthy_modules(self):
8486
# Mock staking router module IDs and digests
8587
self.mock_w3.lido.staking_router.get_staking_module_ids.return_value = [1, 2]
8688
self.mock_w3.lido.staking_router.get_staking_module_digests.return_value = [
87-
[0, 0, [1], [3, 1]], # Module 1: 2 validators
88-
[0, 0, [2], [7, 5]], # Module 2: 2 validators
89+
[0, 0, [1], [3, 5]], # Module 1: 2 validators
90+
[0, 0, [2], [7, 9]], # Module 2: 2 validators
8991
]
92+
self.bot._get_quorum = Mock()
9093

9194
# Mock module healthiness and quorum
92-
self.bot._get_quorum = MagicMock(side_effect=[True, True, True])
9395
self.bot._is_module_healthy = MagicMock(side_effect=[False, False])
9496

9597
# Call the method
9698
result = self.bot._get_preferred_to_deposit_modules()
9799

98-
# Expected output: Include all modules as no healthy modules are found
99-
self.assertEqual([1, 2], result)
100+
self.assertEqual([], result)
100101

101102
def test_module_sorting_by_validator_difference(self):
102103
# Mock staking router module IDs and digests
103104
self.mock_w3.lido.staking_router.get_staking_module_ids.return_value = [1, 2, 3]
104105
self.mock_w3.lido.staking_router.get_staking_module_digests.return_value = [
105-
[0, 0, [1], [6, 2]], # Module 1: 4 validators
106-
[0, 0, [2], [8, 3]], # Module 2: 5 validators
107-
[0, 0, [3], [7, 6]], # Module 3: 1 validator
106+
[0, 0, [1], [6, 10]], # Module 1: 4 validators
107+
[0, 0, [2], [8, 13]], # Module 2: 5 validators
108+
[0, 0, [3], [7, 8]], # Module 3: 1 validator
108109
]
109110

110111
# Mock module healthiness and quorum
@@ -114,8 +115,8 @@ def test_module_sorting_by_validator_difference(self):
114115
# Call the method
115116
result = self.bot._get_preferred_to_deposit_modules()
116117

117-
# Expected output: Sorted by validator difference: Module 2, Module 1, Module 3
118-
self.assertEqual([2], result)
118+
# Expected output: Sorted by validator difference: Module 3, Module 1, Module 2
119+
self.assertEqual([3], result)
119120

120121

121122
@pytest.fixture
@@ -171,13 +172,23 @@ def test_depositor_one_module_deposited(depositor_bot, block_data):
171172
(0, 0, (2,), (0, 10, 10)),
172173
]
173174
)
175+
depositor_bot._general_strategy.is_gas_price_ok = Mock(return_value=True)
176+
depositor_bot._general_strategy.deposited_keys_amount = Mock(return_value=10)
174177
depositor_bot._check_balance = Mock()
175178
depositor_bot._deposit_to_module = Mock(return_value=True)
176-
depositor_bot.execute(block_data)
179+
assert depositor_bot.execute(block_data)
177180

178181
assert depositor_bot._deposit_to_module.call_count == 1
179182

180183

184+
@pytest.mark.unit
185+
def test_depositor_no_modules_to_deposit(depositor_bot, block_data):
186+
depositor_bot._check_balance = Mock()
187+
depositor_bot._get_preferred_to_deposit_modules = Mock(return_value=[])
188+
# Make sure if no modules are deposited, the bot goes to long sleep
189+
assert depositor_bot.execute(block_data)
190+
191+
181192
@pytest.mark.unit
182193
@pytest.mark.parametrize(
183194
'is_depositable,quorum,is_gas_price_ok,is_deposited_keys_amount_ok',
@@ -379,27 +390,3 @@ def test_depositor_bot(
379390
db.message_storage.messages = deposit_messages
380391
assert db.execute(latest)
381392
assert web3_lido_integration.lido.staking_router.get_staking_module_nonce(module_id) == old_module_nonce + 1
382-
383-
384-
@pytest.mark.unit
385-
def test_depositor_execute(web3_lido_unit, depositor_bot):
386-
depositor_bot._check_balance = Mock()
387-
388-
depositor_bot._get_preferred_to_deposit_modules = Mock(return_value=[1, 2])
389-
390-
# Check unsuccess deposit to first module only
391-
variables.DEPOSIT_TO_FIRST_HEALTHY_MODULE_ONLY = True
392-
depositor_bot._deposit_to_module = Mock(return_value=False)
393-
depositor_bot.execute(None)
394-
assert depositor_bot._deposit_to_module.call_count == 1
395-
396-
# Check deposit to both modules
397-
variables.DEPOSIT_TO_FIRST_HEALTHY_MODULE_ONLY = False
398-
depositor_bot._deposit_to_module = Mock(return_value=False)
399-
depositor_bot.execute(None)
400-
assert depositor_bot._deposit_to_module.call_count == 2
401-
402-
# Check success to first module
403-
depositor_bot._deposit_to_module = Mock(return_value=True)
404-
depositor_bot.execute(None)
405-
assert depositor_bot._deposit_to_module.call_count == 1

0 commit comments

Comments
 (0)