Skip to content

Commit

Permalink
Merge pull request #364 from lidofinance/fix/oracle-v3-fixes-5
Browse files Browse the repository at this point in the history
Oracle v3 fixes 5
  • Loading branch information
F4ever authored Apr 17, 2023
2 parents 0661a89 + 8f46c53 commit 73a655c
Show file tree
Hide file tree
Showing 9 changed files with 128 additions and 38 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,18 @@ Full variables list could be found [here](https://github.com/lidofinance/lido-or
> docker run --env EXECUTION_CLIENT_URI={value} --env CONSENSUS_CLIENT_URI={value} --env KEYS_API_URI={value} --env LIDO_LOCATOR_ADDRESS={value} lidofinance/oracle:{tag} {type}
> ```

## Manual mode

Oracle could be executed once in "manual" mode. To do this setup `DAEMON` variable to 'False'.

**Note**: Use `-it` option to run manual mode in Docker container in interactive mode.
Example `docker run -ti --env-file .env --rm lidofinance/oracle:{tag} {type}`

In this mode Oracle will build report as usual (if contracts are reportable) and before submitting transactions
Oracle will ask for manual input to send transaction.

In manual mode all sleeps are disabled and `ALLOW_REPORTING_IN_BUNKER_MODE` is True.

## Env variables

| Name | Description | Required | Example value |
Expand All @@ -152,6 +164,7 @@ Full variables list could be found [here](https://github.com/lidofinance/lido-or
| `MEMBER_PRIV_KEY_FILE` | A path to the file contained the private key of the Oracle member account. It takes precedence over `MEMBER_PRIV_KEY` | False | `/app/private_key` |
| `FINALIZATION_BATCH_MAX_REQUEST_COUNT` | The size of the batch to be finalized per request (The larger the batch size, the more memory of the contract is used but the fewer requests are needed) | False | `1000` |
| `ALLOW_REPORTING_IN_BUNKER_MODE` | Allow the Oracle to do report if bunker mode is active | False | `True` |
| `DAEMON` | If False Oracle runs one cycle and ask for manual input to send report. | False | `True` |
| `TX_GAS_ADDITION` | Used to modify gas parameter that used in transaction. (gas = estimated_gas + TX_GAS_ADDITION) | False | `100000` |
| `CYCLE_SLEEP_IN_SECONDS` | The time between cycles of the oracle's activity | False | `12` |
| `SUBMIT_DATA_DELAY_IN_SLOTS` | The difference in slots between submit data transactions from Oracles. It is used to prevent simultaneous sending of transactions and, as a result, transactions revert. | False | `6` |
Expand Down
34 changes: 20 additions & 14 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@
logger = logging.getLogger()


def main(module: OracleModule):
def main(module_name: OracleModule):
build_info = get_build_info()
logger.info({
'msg': 'Oracle startup.',
'variables': {
**build_info,
'module': module,
'module': module_name,
'ACCOUNT': variables.ACCOUNT.address if variables.ACCOUNT else 'Dry',
'LIDO_LOCATOR_ADDRESS': variables.LIDO_LOCATOR_ADDRESS,
'MAX_CYCLE_LIFETIME_IN_SECONDS': variables.MAX_CYCLE_LIFETIME_IN_SECONDS,
Expand Down Expand Up @@ -84,16 +84,21 @@ def main(module: OracleModule):

logger.info({'msg': 'Sanity checks.'})

if module == OracleModule.ACCOUNTING:
if module_name == OracleModule.ACCOUNTING:
logger.info({'msg': 'Initialize Accounting module.'})
accounting = Accounting(web3)
accounting.check_contract_configs()
accounting.run_as_daemon()
elif module == OracleModule.EJECTOR:
instance = Accounting(web3)
elif module_name == OracleModule.EJECTOR:
logger.info({'msg': 'Initialize Ejector module.'})
ejector = Ejector(web3)
ejector.check_contract_configs()
ejector.run_as_daemon()
instance = Ejector(web3) # type: ignore[assignment]
else:
raise ValueError(f'Unexpected arg: {module_name=}.')

instance.check_contract_configs()

if variables.DAEMON:
instance.run_as_daemon()
else:
instance.cycle_handler()


def check():
Expand All @@ -117,12 +122,13 @@ def check_providers_chain_ids(web3: Web3, cc: ConsensusClientModule, kac: KeysAP


if __name__ == '__main__':
module_name = sys.argv[-1]
if module_name not in iter(OracleModule):
msg = f'Last arg should be one of {[str(item) for item in OracleModule]}, received {module_name}.'
module_name_arg = sys.argv[-1]
if module_name_arg not in iter(OracleModule):
msg = f'Last arg should be one of {[str(item) for item in OracleModule]}, received {module_name_arg}.'
logger.error({'msg': msg})
raise ValueError(msg)
module = OracleModule(module_name)

module = OracleModule(module_name_arg)
if module == OracleModule.CHECK:
errors = variables.check_uri_required_variables()
variables.raise_from_errors(errors)
Expand Down
8 changes: 7 additions & 1 deletion src/modules/submodules/consensus.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from web3.contract import AsyncContract, Contract

from src import variables
from src.metrics.prometheus.basic import ORACLE_SLOT_NUMBER, ORACLE_BLOCK_NUMBER, GENESIS_TIME
from src.metrics.prometheus.basic import ORACLE_SLOT_NUMBER, ORACLE_BLOCK_NUMBER, GENESIS_TIME, ACCOUNT_BALANCE
from src.typings import BlockStamp, ReferenceBlockStamp, SlotNumber
from src.metrics.prometheus.business import (
ORACLE_MEMBER_LAST_REPORT_REF_SLOT,
Expand Down Expand Up @@ -401,6 +401,12 @@ def _get_latest_blockstamp(self) -> BlockStamp:
logger.debug({'msg': 'Fetch latest blockstamp.', 'value': bs})
ORACLE_SLOT_NUMBER.labels('head').set(bs.slot_number)
ORACLE_BLOCK_NUMBER.labels('head').set(bs.block_number)

if variables.ACCOUNT:
ACCOUNT_BALANCE.labels(str(variables.ACCOUNT.address)).set(
self.w3.eth.get_balance(variables.ACCOUNT.address)
)

return bs

@lru_cache(maxsize=1)
Expand Down
6 changes: 4 additions & 2 deletions src/modules/submodules/oracle_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ def run_as_daemon(self):
logger.info({'msg': 'Run module as daemon.'})
while True:
logger.info({'msg': 'Startup new cycle.'})
self._cycle_handler()
self.cycle_handler()

@timeout(variables.MAX_CYCLE_LIFETIME_IN_SECONDS)
def _cycle_handler(self):
def cycle_handler(self):
blockstamp = self._receive_last_finalized_slot()

if blockstamp.slot_number > self._slot_threshold:
Expand Down Expand Up @@ -108,6 +108,8 @@ def run_cycle(self, blockstamp: BlockStamp) -> ModuleExecuteDelay:
logger.error({'msg': 'Keys API service returns outdated data.', 'error': str(error)})
except CountOfKeysDiffersException as error:
logger.error({'msg': 'Keys API service returned incorrect number of keys.', 'error': str(error)})
except ValueError as error:
logger.error({'msg': 'Unexpected error.', 'error': str(error)})

return ModuleExecuteDelay.NEXT_SLOT

Expand Down
16 changes: 16 additions & 0 deletions src/utils/input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
def get_input():
return input()


def prompt(prompt_message: str) -> bool:
print(prompt_message, end='')
while True:
choice = get_input().lower()

if choice in ['Y', 'y']:
return True

if choice in ['N', 'n']:
return False

print('Please respond with [y or n]: ', end='')
21 changes: 15 additions & 6 deletions src/variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
# - App specific -
LIDO_LOCATOR_ADDRESS = os.getenv('LIDO_LOCATOR_ADDRESS')
FINALIZATION_BATCH_MAX_REQUEST_COUNT = int(os.getenv('FINALIZATION_BATCH_MAX_REQUEST_COUNT', 1000))
ALLOW_REPORTING_IN_BUNKER_MODE = os.getenv('ALLOW_REPORTING_IN_BUNKER_MODE', 'False').lower() == 'true'

# We add some gas to the transaction to be sure that we have enough gas to execute corner cases
# eg when we tried to submit a few reports in a single block
# In this case the second report will force report finalization and will consume more gas
Expand All @@ -37,11 +37,20 @@
MAX_PRIORITY_FEE = int(os.getenv('MIN_PRIORITY_FEE', 100_000_000_000))
PRIORITY_FEE_PERCENTILE = int(os.getenv('PRIORITY_FEE_PERCENTILE', 3))

# Default delay for default Oracle members. Member with submit data role should submit data first.
# If contract is reportable each member in order will submit data with difference with this amount of slots
SUBMIT_DATA_DELAY_IN_SLOTS = int(os.getenv('SUBMIT_DATA_DELAY_IN_SLOTS', 6))
CYCLE_SLEEP_IN_SECONDS = int(os.getenv('CYCLE_SLEEP_IN_SECONDS', 12))

DAEMON = os.getenv('DAEMON', 'True').lower() == 'true'
if DAEMON:
# Default delay for default Oracle members. Member with submit data role should submit data first.
# If contract is reportable each member in order will submit data with difference with this amount of slots
SUBMIT_DATA_DELAY_IN_SLOTS = int(os.getenv('SUBMIT_DATA_DELAY_IN_SLOTS', 6))
CYCLE_SLEEP_IN_SECONDS = int(os.getenv('CYCLE_SLEEP_IN_SECONDS', 12))
ALLOW_REPORTING_IN_BUNKER_MODE = os.getenv('ALLOW_REPORTING_IN_BUNKER_MODE', 'False').lower() == 'true'
else:
# Remove all sleep in manual mode
ALLOW_REPORTING_IN_BUNKER_MODE = True
SUBMIT_DATA_DELAY_IN_SLOTS = 0
CYCLE_SLEEP_IN_SECONDS = 0

# HTTP variables
HTTP_REQUEST_TIMEOUT_CONSENSUS = int(os.getenv('HTTP_REQUEST_TIMEOUT_CONSENSUS', 5 * 60))
HTTP_REQUEST_RETRY_COUNT_CONSENSUS = int(os.getenv('HTTP_REQUEST_RETRY_COUNT_CONSENSUS', 5))
HTTP_REQUEST_SLEEP_BEFORE_RETRY_IN_SECONDS_CONSENSUS = int(
Expand Down
30 changes: 20 additions & 10 deletions src/web3py/extensions/tx_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
from web3.types import TxReceipt, Wei, TxParams, BlockData

from src import variables, constants
from src.metrics.prometheus.basic import TRANSACTIONS_COUNT, Status, ACCOUNT_BALANCE
from src.metrics.prometheus.basic import TRANSACTIONS_COUNT, Status
from src.utils.input import prompt

logger = logging.getLogger(__name__)

Expand All @@ -19,15 +20,27 @@ def check_and_send_transaction(self, transaction, account: Optional[LocalAccount
logger.info({'msg': 'No account provided to submit extra data. Dry mode'})
return None

ACCOUNT_BALANCE.labels(str(account.address)).set(self.w3.eth.get_balance(account.address))

params = self._get_transaction_params(transaction, account)

if self._check_transaction(transaction, params):
if not variables.DAEMON:
return self._manual_tx_processing(transaction, params, account)

return self._sign_and_send_transaction(transaction, params, account)

return None

def _manual_tx_processing(self, transaction, params: TxParams, account: LocalAccount):
logger.warning({'msg': 'Send transaction in manual mode.'})
msg = (
'\n'
'Going to send transaction to blockchain: \n'
f'Tx args:\n{transaction.args}\n'
f'Tx params:\n{params}\n'
)
if prompt(f'{msg}Should we send this TX? [y/n]: '):
self._sign_and_send_transaction(transaction, params, account)

@staticmethod
def _check_transaction(transaction, params: TxParams) -> bool:
"""
Expand All @@ -39,11 +52,8 @@ def _check_transaction(transaction, params: TxParams) -> bool:

try:
result = transaction.call(params)
except ContractLogicError as error:
logger.warning({"msg": "Transaction reverted.", "error": str(error)})
return False
except ValueError as error:
logger.error({"msg": "Not enough funds.", "error": str(error)})
except (ValueError, ContractLogicError) as error:
logger.error({"msg": "Transaction reverted.", "error": str(error)})
return False

logger.info({"msg": "Transaction executed successfully.", "value": result})
Expand Down Expand Up @@ -82,10 +92,10 @@ def _estimate_gas(transaction: ContractFunction, account: LocalAccount) -> Optio
try:
gas = transaction.estimate_gas({'from': account.address})
except ContractLogicError as error:
logger.warning({'msg': 'Contract logic error.', 'error': str(error)})
logger.warning({'msg': 'Can not estimate gas. Contract logic error.', 'error': str(error)})
return None
except ValueError as error:
logger.warning({'msg': 'Execution reverted.', 'error': str(error)})
logger.warning({'msg': 'Can not estimate gas. Execution reverted.', 'error': str(error)})
return None

return min(
Expand Down
10 changes: 5 additions & 5 deletions tests/modules/submodules/test_oracle_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,16 @@ def test_receive_last_finalized_slot(oracle):
def test_cycle_handler_run_once_per_slot(oracle, contracts, web3):
web3.lido_contracts.has_contract_address_changed = Mock()
oracle._receive_last_finalized_slot = Mock(return_value=ReferenceBlockStampFactory.build(slot_number=1))
oracle._cycle_handler()
oracle.cycle_handler()
assert oracle.call_count == 1
assert web3.lido_contracts.has_contract_address_changed.call_count == 1

oracle._cycle_handler()
oracle.cycle_handler()
assert oracle.call_count == 1
assert web3.lido_contracts.has_contract_address_changed.call_count == 1

oracle._receive_last_finalized_slot = Mock(return_value=ReferenceBlockStampFactory.build(slot_number=2))
oracle._cycle_handler()
oracle.cycle_handler()
assert oracle.call_count == 2
assert web3.lido_contracts.has_contract_address_changed.call_count == 2

Expand All @@ -80,12 +80,12 @@ def _throw_on_third_call():
if times == 3:
raise Exception("Cycle failed")

oracle._cycle_handler = Mock(side_effect=_throw_on_third_call)
oracle.cycle_handler = Mock(side_effect=_throw_on_third_call)

with pytest.raises(Exception, match="Cycle failed"):
oracle.run_as_daemon()

assert oracle._cycle_handler.call_count == 3
assert oracle.cycle_handler.call_count == 3


@pytest.mark.unit
Expand Down
28 changes: 28 additions & 0 deletions tests/web3_extentions/test_tx_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
from src import variables
from src.constants import MAX_BLOCK_GAS_LIMIT
from src.modules.accounting.typings import Account
from src.utils import input


class Transaction:
args = {}

def estimate_gas(self, params: dict) -> int:
return 0

Expand Down Expand Up @@ -72,3 +75,28 @@ def test_get_tx_params(web3, tx_utils, tx, account):
params = web3.transaction._get_transaction_params(tx, account)

assert params['maxPriorityFeePerGas'] == variables.MAX_PRIORITY_FEE


def test_manual_tx_processing(web3, tx_utils, tx, account):
input.get_input = Mock(return_value='y')
web3.transaction._sign_and_send_transaction = Mock()
web3.transaction._manual_tx_processing(tx, {}, account)
web3.transaction._sign_and_send_transaction.assert_called_once()


def test_manual_tx_processing_decline(web3, tx_utils, tx, account):
input.get_input = Mock(return_value='n')
web3.transaction._sign_and_send_transaction = Mock()
web3.transaction._manual_tx_processing(tx, {}, account)
web3.transaction._sign_and_send_transaction.assert_not_called()


def test_daemon_check_and_send_transaction(web3, tx_utils, tx, account, monkeypatch):
input.get_input = Mock(return_value='n')
with monkeypatch.context():
monkeypatch.setattr(variables, "DAEMON", False)
web3.transaction._sign_and_send_transaction = Mock()
web3.transaction._get_transaction_params = Mock(return_value={})
web3.transaction._check_transaction = Mock(return_value=True)
web3.transaction.check_and_send_transaction(tx, account)
web3.transaction._sign_and_send_transaction.assert_not_called()

0 comments on commit 73a655c

Please sign in to comment.