diff --git a/src/commands/start.py b/src/commands/start.py index 24982a2d..10cbf2b8 100644 --- a/src/commands/start.py +++ b/src/commands/start.py @@ -41,7 +41,7 @@ '--max-fee-per-gas-gwei', type=int, envvar='MAX_FEE_PER_GAS_GWEI', - help=f'Maximum fee per gas limit for transactions. ' + help=f'Maximum fee per gas for transactions. ' f'Default is {DEFAULT_MAX_FEE_PER_GAS_GWEI} Gwei.', default=DEFAULT_MAX_FEE_PER_GAS_GWEI, ) diff --git a/src/commands/start_api.py b/src/commands/start_api.py index 7d80c485..acf31463 100644 --- a/src/commands/start_api.py +++ b/src/commands/start_api.py @@ -45,7 +45,7 @@ '--max-fee-per-gas-gwei', type=int, envvar='MAX_FEE_PER_GAS_GWEI', - help=f'Maximum fee per gas limit for transactions. ' + help=f'Maximum fee per gas for transactions. ' f'Default is {DEFAULT_MAX_FEE_PER_GAS_GWEI} Gwei.', default=DEFAULT_MAX_FEE_PER_GAS_GWEI, ) diff --git a/src/common/execution.py b/src/common/execution.py index a9a7326d..443c874a 100644 --- a/src/common/execution.py +++ b/src/common/execution.py @@ -1,12 +1,12 @@ import logging -import statistics from typing import cast import click from eth_typing import BlockNumber from web3 import Web3 -from web3.exceptions import BadFunctionCallOutput, MethodUnavailable -from web3.types import BlockIdentifier, Wei +from web3._utils.async_transactions import _max_fee_per_gas +from web3.exceptions import BadFunctionCallOutput +from web3.types import TxParams, Wei from src.common.clients import execution_client, ipfs_fetch_client from src.common.contracts import keeper_contract, multicall_contract, vault_contract @@ -149,8 +149,52 @@ async def get_oracles() -> Oracles: ) -async def check_gas_price() -> bool: - max_fee_per_gas = await _get_max_fee_per_gas() +async def get_high_priority_tx_params() -> TxParams: + """ + `maxPriorityFeePerGas <= maxFeePerGas` must be fulfilled + Because of that when increasing `maxPriorityFeePerGas` I have to adjust `maxFeePerGas`. + See https://eips.ethereum.org/EIPS/eip-1559 for details. + """ + tx_params: TxParams = {} + + max_priority_fee_per_gas = await _calc_high_priority_fee() + + # Reference: `_max_fee_per_gas` in web3/_utils/async_transactions.py + block = await execution_client.eth.get_block('latest') + max_fee_per_gas = Wei(max_priority_fee_per_gas + (2 * block['baseFeePerGas'])) + + tx_params['maxPriorityFeePerGas'] = max_priority_fee_per_gas + tx_params['maxFeePerGas'] = max_fee_per_gas + logger.debug('tx_params %s', tx_params) + + return tx_params + + +async def _calc_high_priority_fee() -> Wei: + """ + reference: "high" priority value from https://etherscan.io/gastracker + """ + num_blocks = settings.priority_fee_num_blocks + percentile = settings.priority_fee_percentile + history = await execution_client.eth.fee_history(num_blocks, 'pending', [percentile]) + validator_rewards = [r[0] for r in history['reward']] + mean_reward = int(sum(validator_rewards) / len(validator_rewards)) + + # prettify `mean_reward` + # same as `round(value, 1)` if value was in gwei + mean_reward = round(mean_reward, -8) + + return Wei(mean_reward) + + +async def check_gas_price(high_priority: bool = False) -> bool: + if high_priority: + tx_params = await get_high_priority_tx_params() + max_fee_per_gas = Wei(int(tx_params['maxFeePerGas'])) + else: + # fallback to logic from web3 + max_fee_per_gas = await _max_fee_per_gas(execution_client, {}) + if max_fee_per_gas >= Web3.to_wei(settings.max_fee_per_gas_gwei, 'gwei'): logging.warning( 'Current gas price (%s gwei) is too high. ' @@ -163,33 +207,6 @@ async def check_gas_price() -> bool: return True -async def _get_max_fee_per_gas() -> Wei: - try: - priority_fee = await execution_client.eth.max_priority_fee - except MethodUnavailable: - priority_fee = await _calculate_median_priority_fee() - latest_block = await execution_client.eth.get_block('latest') - base_fee = latest_block['baseFeePerGas'] - max_fee_per_gas = priority_fee + base_fee - return Wei(max_fee_per_gas) - - -async def _calculate_median_priority_fee(block_id: BlockIdentifier = 'latest') -> Wei: - block = await execution_client.eth.get_block(block_id) - - # collect maxPriorityFeePerGas for all transactions in the block - priority_fees = [] - for tx_hash in block.transactions: # type: ignore[attr-defined] - tx = await execution_client.eth.get_transaction(tx_hash) - if 'maxPriorityFeePerGas' in tx: - priority_fees.append(tx.maxPriorityFeePerGas) # type: ignore[attr-defined] - - if not priority_fees: - return await _calculate_median_priority_fee(block['number'] - 1) - - return Wei(statistics.median(priority_fees)) - - class WalletTask(BaseTask): async def process_block(self) -> None: await check_hot_wallet_balance() diff --git a/src/config/settings.py b/src/config/settings.py index d0bc9305..310e0b74 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -77,6 +77,12 @@ class Settings(metaclass=Singleton): validators_registration_mode: ValidatorsRegistrationMode skip_startup_checks: bool + # high priority fee + priority_fee_num_blocks: int = decouple_config('PRIORITY_FEE_NUM_BLOCKS', default=10, cast=int) + priority_fee_percentile: float = decouple_config( + 'PRIORITY_FEE_PERCENTILE', default=80.0, cast=float + ) + # pylint: disable-next=too-many-arguments,too-many-locals def set( self, diff --git a/src/validators/execution.py b/src/validators/execution.py index aeb9db6f..c7cf3ed1 100644 --- a/src/validators/execution.py +++ b/src/validators/execution.py @@ -15,6 +15,7 @@ validators_registry_contract, vault_contract, ) +from src.common.execution import get_high_priority_tx_params from src.common.ipfs import fetch_harvest_params from src.common.metrics import metrics from src.common.typings import OraclesApproval @@ -249,6 +250,8 @@ async def register_single_validator( multi_proof.proof, ] try: + tx_params = await get_high_priority_tx_params() + if update_state_call is not None: register_call = vault_contract.encode_abi( fn_name='registerValidator', @@ -256,9 +259,10 @@ async def register_single_validator( ) tx = await vault_contract.functions.multicall( [update_state_call, register_call] - ).transact() + ).transact(tx_params) else: - tx = await vault_contract.functions.registerValidator(*register_call_args).transact() + register_func = vault_contract.functions.registerValidator + tx = await register_func(*register_call_args).transact(tx_params) except Exception as e: logger.error('Failed to register validator: %s', format_error(e)) if settings.verbose: @@ -305,6 +309,8 @@ async def register_multiple_validator( multi_proof.proof, ] try: + tx_params = await get_high_priority_tx_params() + if update_state_call is not None: register_call = vault_contract.encode_abi( fn_name='registerValidators', @@ -312,9 +318,10 @@ async def register_multiple_validator( ) tx = await vault_contract.functions.multicall( [update_state_call, register_call] - ).transact() + ).transact(tx_params) else: - tx = await vault_contract.functions.registerValidators(*register_call_args).transact() + register_func = vault_contract.functions.registerValidators + tx = await register_func(*register_call_args).transact(tx_params) except Exception as e: logger.error('Failed to register validators: %s', format_error(e)) if settings.verbose: diff --git a/src/validators/tasks.py b/src/validators/tasks.py index 654d6d97..7627581f 100644 --- a/src/validators/tasks.py +++ b/src/validators/tasks.py @@ -143,7 +143,7 @@ async def register_validators( ) return None - if not await check_gas_price(): + if not await check_gas_price(high_priority=True): return None logger.info('Started registration of %d validator(s)', len(validators))