diff --git a/skale/contracts/base_contract.py b/skale/contracts/base_contract.py index a91cc2f1..9532e3be 100644 --- a/skale/contracts/base_contract.py +++ b/skale/contracts/base_contract.py @@ -19,7 +19,7 @@ """ SKALE base contract class """ import logging -from functools import wraps +from functools import partial, wraps from web3 import Web3 @@ -31,38 +31,40 @@ logger = logging.getLogger(__name__) -def transaction_method(gas_limit): - def real_decorator(transaction): - @wraps(transaction) - def wrapper(self, *args, wait_for=True, timeout=4, blocks_to_wait=50, - gas_price=None, nonce=None, - dry_run_only=False, skip_dry_run=False, - raise_for_status=True, **kwargs): - method = transaction(self, *args, **kwargs) - tx_res = TxRes() - if not skip_dry_run: - tx_res.dry_run_result = make_dry_run_call(self.skale.wallet, - method, gas_limit) +def transaction_method(transaction=None, *, gas_limit=10): + if transaction is None: + return partial(transaction_method, gas_limit=gas_limit) - if not dry_run_only and (skip_dry_run or tx_res.dry_run_passed()): - gas_price = gas_price or self.skale.gas_price - tx_res.hash = post_transaction(self.skale.wallet, method, - gas_limit, gas_price, nonce) - if wait_for: - tx_res.receipt = wait_for_receipt_by_blocks( - self.skale.web3, - tx_res.hash, - timeout=timeout, - blocks_to_wait=blocks_to_wait - ) + @wraps(transaction) + def wrapper(self, *args, wait_for=True, + wait_timeout=4, blocks_to_wait=50, + gas_price=None, nonce=None, + dry_run_only=False, skip_dry_run=False, raise_for_status=True, + **kwargs): + method = transaction(self, *args, **kwargs) + tx_res = TxRes() + if not skip_dry_run: + tx_res.dry_run_result = make_dry_run_call(self.skale.wallet, + method, gas_limit) - if raise_for_status: - tx_res.raise_for_status() + if not dry_run_only and (skip_dry_run or tx_res.dry_run_passed()): + gas_price = gas_price or self.skale.gas_price + tx_res.hash = post_transaction(self.skale.wallet, method, + gas_limit, gas_price, nonce) + if wait_for: + tx_res.receipt = wait_for_receipt_by_blocks( + self.skale.web3, + tx_res.hash, + timeout=wait_timeout, + blocks_to_wait=blocks_to_wait + ) - return tx_res + if raise_for_status: + tx_res.raise_for_status() - return wrapper - return real_decorator + return tx_res + + return wrapper class BaseContract: diff --git a/skale/contracts/constants_holder.py b/skale/contracts/constants_holder.py index eae8b930..0a16d4d5 100644 --- a/skale/contracts/constants_holder.py +++ b/skale/contracts/constants_holder.py @@ -22,7 +22,7 @@ class ConstantsHolder(BaseContract): - @transaction_method(GAS['set_periods']) + @transaction_method(gas_limit=GAS['set_periods']) def set_periods(self, new_reward_period, new_delta_period): return self.contract.functions.setPeriods(new_reward_period, new_delta_period) @@ -32,14 +32,14 @@ def get_reward_period(self): def get_delta_period(self): return self.contract.functions.deltaPeriod().call() - @transaction_method(GAS['set_check_time']) + @transaction_method(gas_limit=GAS['set_check_time']) def set_check_time(self, new_check_time): return self.contract.functions.setCheckTime(new_check_time) def get_check_time(self): return self.contract.functions.checkTime().call() - @transaction_method(GAS['set_latency']) + @transaction_method(gas_limit=GAS['set_latency']) def set_latency(self, new_allowable_latency): return self.contract.functions.setLatency(new_allowable_latency) @@ -54,7 +54,7 @@ def msr(self) -> int: """ return self.contract.functions.msr().call() - @transaction_method(GAS['set_msr']) + @transaction_method(gas_limit=GAS['set_msr']) def _set_msr(self, new_msr: int) -> None: """For internal usage only""" return self.contract.functions.setMSR(new_msr) @@ -62,6 +62,6 @@ def _set_msr(self, new_msr: int) -> None: def get_launch_timestamp(self) -> int: return self.contract.functions.launchTimestamp().call() - @transaction_method(GAS['set_launch_timestamp']) + @transaction_method(gas_limit=GAS['set_launch_timestamp']) def set_launch_timestamp(self, launch_timestamp: int): return self.contract.functions.setLaunchTimestamp(launch_timestamp) diff --git a/skale/contracts/delegation/delegation_controller.py b/skale/contracts/delegation/delegation_controller.py index 777d7147..d1701ddc 100644 --- a/skale/contracts/delegation/delegation_controller.py +++ b/skale/contracts/delegation/delegation_controller.py @@ -126,7 +126,7 @@ def get_all_delegations_by_validator(self, validator_id: int) -> list: delegation_ids = self._get_delegation_ids_by_validator(validator_id) return self.get_all_delegations(delegation_ids) - @transaction_method(GAS['delegate']) + @transaction_method(gas_limit=GAS['delegate']) def delegate(self, validator_id: int, amount: int, delegation_period: int, info: str) -> TxRes: """Creates request to delegate amount of tokens to validator_id. @@ -143,7 +143,7 @@ def delegate(self, validator_id: int, amount: int, delegation_period: int, info: """ return self.contract.functions.delegate(validator_id, amount, delegation_period, info) - @transaction_method(GAS['accept_pending_delegation']) + @transaction_method(gas_limit=GAS['accept_pending_delegation']) def accept_pending_delegation(self, delegation_id: int) -> TxRes: """Accepts a pending delegation by delegation ID. @@ -154,7 +154,7 @@ def accept_pending_delegation(self, delegation_id: int) -> TxRes: """ return self.contract.functions.acceptPendingDelegation(delegation_id) - @transaction_method(GAS['cancel_pending_delegation']) + @transaction_method(gas_limit=GAS['cancel_pending_delegation']) def cancel_pending_delegation(self, delegation_id: int) -> TxRes: """Cancel pending delegation request. @@ -165,7 +165,7 @@ def cancel_pending_delegation(self, delegation_id: int) -> TxRes: """ return self.contract.functions.cancelPendingDelegation(delegation_id) - @transaction_method(GAS['request_undelegation']) + @transaction_method(gas_limit=GAS['request_undelegation']) def request_undelegation(self, delegation_id: int) -> TxRes: """ This method is for undelegating request in the end of delegation period (3/6/12 months) diff --git a/skale/contracts/delegation/distributor.py b/skale/contracts/delegation/distributor.py index edf31885..2cbcaa4a 100644 --- a/skale/contracts/delegation/distributor.py +++ b/skale/contracts/delegation/distributor.py @@ -62,7 +62,7 @@ def get_earned_fee_amount(self, address: str) -> dict: 'from': address }) - @transaction_method(GAS['withdraw_bounty']) + @transaction_method(gas_limit=GAS['withdraw_bounty']) def withdraw_bounty(self, validator_id: int, to: str) -> TxRes: """Withdraw earned bounty to specified address @@ -75,7 +75,7 @@ def withdraw_bounty(self, validator_id: int, to: str) -> TxRes: """ return self.contract.functions.withdrawBounty(validator_id, to) - @transaction_method(GAS['withdraw_fee']) + @transaction_method(gas_limit=GAS['withdraw_fee']) def withdraw_fee(self, to: str) -> TxRes: """Withdraw earned fee to specified address diff --git a/skale/contracts/delegation/validator_service.py b/skale/contracts/delegation/validator_service.py index 1c23caa0..b637bd83 100644 --- a/skale/contracts/delegation/validator_service.py +++ b/skale/contracts/delegation/validator_service.py @@ -157,12 +157,12 @@ def get_validator_node_indices(self, validator_id: int) -> list: """ return self.contract.functions.getValidatorNodeIndexes(validator_id).call() - @transaction_method(GAS['enable_validator']) + @transaction_method(gas_limit=GAS['enable_validator']) def _enable_validator(self, validator_id: int) -> TxRes: """For internal usage only""" return self.contract.functions.enableValidator(validator_id) - @transaction_method(GAS['disable_validator']) + @transaction_method(gas_limit=GAS['disable_validator']) def _disable_validator(self, validator_id: int) -> TxRes: """For internal usage only""" return self.contract.functions.disableValidator(validator_id) @@ -175,7 +175,7 @@ def is_accepting_new_requests(self, validator_id: int) -> bool: """For internal usage only""" return self.contract.functions.isAcceptingNewRequests(validator_id).call() - @transaction_method(GAS['register_validator']) + @transaction_method(gas_limit=GAS['register_validator']) def register_validator(self, name: str, description: str, fee_rate: int, min_delegation_amount: int) -> TxRes: """Registers a new validator in the SKALE Manager contracts. @@ -199,7 +199,7 @@ def get_link_node_signature(self, validator_id: int) -> str: signed_hash = self.skale.wallet.sign_hash(unsigned_hash.hex()) return signed_hash.signature.hex() - @transaction_method(GAS['link_node_address']) + @transaction_method(gas_limit=GAS['link_node_address']) def link_node_address(self, node_address: str, signature: str) -> TxRes: """Link node address to your validator account. @@ -212,7 +212,7 @@ def link_node_address(self, node_address: str, signature: str) -> TxRes: """ return self.contract.functions.linkNodeAddress(node_address, signature) - @transaction_method(GAS['unlink_node_address']) + @transaction_method(gas_limit=GAS['unlink_node_address']) def unlink_node_address(self, node_address: str) -> TxRes: """Unlink node address from your validator account. diff --git a/skale/contracts/dkg.py b/skale/contracts/dkg.py index a18427a1..df324a29 100644 --- a/skale/contracts/dkg.py +++ b/skale/contracts/dkg.py @@ -18,6 +18,7 @@ # along with SKALE.py. If not, see . from skale.contracts import BaseContract, transaction_method +from skale.transactions.tools import retry_tx from skale.utils.constants import GAS @@ -25,25 +26,29 @@ class DKG(BaseContract): def gas_price(self): return self.skale.gas_price * 5 // 4 - @transaction_method(GAS['dkg_broadcast']) + @retry_tx + @transaction_method(gas_limit=GAS['dkg_broadcast']) def broadcast(self, group_index, node_index, verification_vector, secret_key_contribution): return self.contract.functions.broadcast(group_index, node_index, verification_vector, secret_key_contribution) - @transaction_method(GAS['dkg_response']) + @retry_tx + @transaction_method(gas_limit=GAS['dkg_response']) def response(self, group_index, from_node_index, secret_number, multiplied_share): return self.contract.functions.response(group_index, from_node_index, secret_number, multiplied_share) - @transaction_method(GAS['dkg_alright']) + @retry_tx + @transaction_method(gas_limit=GAS['dkg_alright']) def alright(self, group_index, from_node_index): return self.contract.functions.alright(group_index, from_node_index) - @transaction_method(GAS['dkg_complaint']) + @retry_tx + @transaction_method(gas_limit=GAS['dkg_complaint']) def complaint(self, group_index, from_node_index, to_node_index): return self.contract.functions.complaint(group_index, from_node_index, to_node_index) diff --git a/skale/contracts/manager.py b/skale/contracts/manager.py index 7060aab7..f8696b7d 100644 --- a/skale/contracts/manager.py +++ b/skale/contracts/manager.py @@ -31,7 +31,8 @@ class Manager(BaseContract): - @transaction_method(GAS['create_node']) + + @transaction_method(gas_limit=GAS['create_node']) def create_node(self, ip, port, name, public_ip=None): logger.info( f'create_node: {ip}:{port}, public ip: {public_ip} name: {name}') @@ -74,11 +75,12 @@ def create_node_data_to_bytes(self, ip, public_ip, port, name, pk, nonce): def create_default_schain(self, name): lifetime = 3600 nodes_type = 4 - price_in_wei = self.skale.schains.get_schain_price(nodes_type, lifetime) + price_in_wei = self.skale.schains.get_schain_price( + nodes_type, lifetime) return self.create_schain(lifetime, nodes_type, price_in_wei, name, wait_for=True) - @transaction_method(GAS['create_schain']) + @transaction_method(gas_limit=GAS['create_schain']) def create_schain(self, lifetime, type_of_nodes, deposit, name): logger.info( f'create_schain: type_of_nodes: {type_of_nodes}, name: {name}') @@ -108,28 +110,28 @@ def create_schain_data_to_bytes(self, lifetime, type_of_nodes, name, ) return data_bytes - @transaction_method(GAS['get_bounty']) + @transaction_method(gas_limit=GAS['get_bounty']) def get_bounty(self, node_id): return self.contract.functions.getBounty(node_id) - @transaction_method(GAS['send_verdict']) + @transaction_method(gas_limit=GAS['send_verdict']) def send_verdict(self, validator, node_id, downtime, latency): return self.contract.functions.sendVerdict(validator, node_id, downtime, latency) - @transaction_method(GAS['send_verdicts']) + @transaction_method(gas_limit=GAS['send_verdicts']) def send_verdicts(self, validator, nodes_ids, downtimes, latencies): return self.contract.functions.sendVerdicts(validator, nodes_ids, downtimes, latencies) - @transaction_method(GAS['delete_node']) + @transaction_method(gas_limit=GAS['delete_node']) def deregister(self, node_id): return self.contract.functions.deleteNode(node_id) - @transaction_method(GAS['delete_schain']) + @transaction_method(gas_limit=GAS['delete_schain']) def delete_schain(self, schain_name): return self.contract.functions.deleteSchain(schain_name) - @transaction_method(GAS['delete_node_by_root']) + @transaction_method(gas_limit=GAS['delete_node_by_root']) def delete_node_by_root(self, node_id): return self.contract.functions.deleteNodeByRoot(node_id) diff --git a/skale/contracts/test/time_helpers_with_debug.py b/skale/contracts/test/time_helpers_with_debug.py index cb56712c..24e93767 100644 --- a/skale/contracts/test/time_helpers_with_debug.py +++ b/skale/contracts/test/time_helpers_with_debug.py @@ -25,7 +25,7 @@ class TimeHelpersWithDebug(BaseContract): """Wrapper for TimeHelpersWithDebug.sol functions (internal usage only)""" - @transaction_method(GAS['skip_time']) + @transaction_method(gas_limit=GAS['skip_time']) def skip_time(self, sec: int) -> TxRes: """Skip time on contracts diff --git a/skale/contracts/token.py b/skale/contracts/token.py index 25efcb2d..5aaabb18 100644 --- a/skale/contracts/token.py +++ b/skale/contracts/token.py @@ -23,13 +23,13 @@ class Token(BaseContract): - @transaction_method(GAS['token_transfer']) + @transaction_method(gas_limit=GAS['token_transfer']) def transfer(self, address, value): return self.contract.functions.send(address, value, b'') def get_balance(self, address): return self.contract.functions.balanceOf(address).call() - @transaction_method(GAS['token_transfer']) + @transaction_method(gas_limit=GAS['token_transfer']) def add_authorized(self, address, wallet): # pragma: no cover return self.contract.functions.addAuthorized(address) diff --git a/skale/dataclasses/current_node_info.py b/skale/dataclasses/current_node_info.py index dc00717f..617ad207 100644 --- a/skale/dataclasses/current_node_info.py +++ b/skale/dataclasses/current_node_info.py @@ -23,7 +23,7 @@ class CurrentNodeInfo(NodeInfo): def __init__(self, node_id, node_name, base_port, bind_ip, ima_mainnet=None, ima_mp_schain=None, ima_mp_mainnet=None, wallets=None, rotate_after_block=64, - schain_log_level='trace', schain_log_level_config='trace'): + schain_log_level='info', schain_log_level_config='info'): self.bind_ip = bind_ip self.schain_log_level = schain_log_level self.schain_log_level_config = schain_log_level_config diff --git a/skale/schain_config/generator.py b/skale/schain_config/generator.py index bacd7345..7a8c7a7e 100644 --- a/skale/schain_config/generator.py +++ b/skale/schain_config/generator.py @@ -79,8 +79,8 @@ def generate_schain_config(base_config, node_info, schain_info): def generate_skale_schain_config(skale, schain_name, node_id, base_config=None, ima_mainnet=None, ima_mp_schain=None, ima_mp_mainnet=None, wallets=None, - rotate_after_block=64, schain_log_level='trace', - schain_log_level_config='trace'): + rotate_after_block=64, schain_log_level='info', + schain_log_level_config='info'): node = skale.nodes_data.get(node_id) schain = skale.schains_data.get_by_name(schain_name) diff --git a/skale/transactions/tools.py b/skale/transactions/tools.py index 3dfcca0c..d51617d6 100644 --- a/skale/transactions/tools.py +++ b/skale/transactions/tools.py @@ -18,7 +18,10 @@ # along with SKALE.py. If not, see . import logging +import time +from functools import partial, wraps +from skale.dataclasses.tx_res import TransactionFailedError, TxRes from skale.utils.web3_utils import get_eth_nonce logger = logging.getLogger(__name__) @@ -94,3 +97,47 @@ def send_eth(web3, account, amount, wallet): f'tx: {web3.toHex(tx)}' ) return tx + + +def retry_tx(tx=None, *, max_retries=3, timeout=-1): + if tx is None: + return partial(retry_tx, max_retries=3, timeout=timeout) + + @wraps(tx) + def wrapper(*args, **kwargs): + return run_tx_with_retry( + tx, *args, + max_retries=max_retries, + retry_timeout=timeout, **kwargs + ) + return wrapper + + +def run_tx_with_retry(transaction, *args, max_retries=3, + retry_timeout=-1, + **kwargs) -> TxRes: + success = False + attempt = 0 + tx_res = None + exp_timeout = 1 + while not success and attempt < max_retries: + try: + tx_res = transaction(*args, **kwargs) + tx_res.raise_for_status() + except TransactionFailedError as err: + logger.error(f'Tx attempt {attempt}/{max_retries} failed', + exc_info=err) + timeout = exp_timeout if retry_timeout < 0 else exp_timeout + time.sleep(timeout) + exp_timeout *= 2 + else: + success = True + attempt += 1 + if success: + logger.info(f'Tx {transaction.__name__} completed ' + f'after {attempt}/{max_retries} retries') + else: + logger.error( + f'Tx {transaction.__name__} failed after ' + f'{max_retries} retries') + return tx_res diff --git a/skale/utils/helper.py b/skale/utils/helper.py index a9eae7ac..45321e07 100644 --- a/skale/utils/helper.py +++ b/skale/utils/helper.py @@ -28,7 +28,6 @@ from logging import Formatter, StreamHandler from random import randint - logger = logging.getLogger(__name__) diff --git a/tests/schain_config/generator_test.py b/tests/schain_config/generator_test.py index 7decc570..6e467b45 100644 --- a/tests/schain_config/generator_test.py +++ b/tests/schain_config/generator_test.py @@ -50,7 +50,7 @@ def test_generate_node_info(): assert node_info['imaMessageProxyMainNet'] == ZERO_ADDRESS assert node_info['rotateAfterBlock'] == 128 - assert node_info['logLevel'] == 'trace' + assert node_info['logLevel'] == 'info' assert node_info['logLevelConfig'] == 'info' assert len(node_info) == NODE_INFO_LEN