From bab907b8672e3c27c0bd74712641c4bfe40fd386 Mon Sep 17 00:00:00 2001 From: Andrew Falaleev Date: Thu, 31 Mar 2022 00:36:08 +0300 Subject: [PATCH] Extend errors from emulator. #678 --- proxy/common_neon/emulator_interactor.py | 287 +++++++++++++++------ proxy/common_neon/errors.py | 3 - proxy/common_neon/solana_interactor.py | 11 +- proxy/common_neon/solana_tx_list_sender.py | 3 +- proxy/environment.py | 2 + proxy/plugin/solana_rest_api.py | 17 +- proxy/plugin/solana_rest_api_tools.py | 28 -- 7 files changed, 229 insertions(+), 122 deletions(-) delete mode 100644 proxy/plugin/solana_rest_api_tools.py diff --git a/proxy/common_neon/emulator_interactor.py b/proxy/common_neon/emulator_interactor.py index d1c85e4b6..40a5d1ea9 100644 --- a/proxy/common_neon/emulator_interactor.py +++ b/proxy/common_neon/emulator_interactor.py @@ -20,7 +20,7 @@ def call_emulated(contract_id, caller_id, data=None, value=None, *, logger): def check_emulated_exit_status(result: Dict[str, Any], *, logger): exit_status = result['exit_status'] if exit_status == 'revert': - revert_data = result['result'] + revert_data = result.get('result') logger.debug(f"Got revert call emulated result with data: {revert_data}") result_value = decode_revert_message(revert_data) if result_value is None: @@ -30,7 +30,47 @@ def check_emulated_exit_status(result: Dict[str, Any], *, logger): if exit_status != "succeed": logger.debug(f"Got not succeed emulate exit_status: {exit_status}") - raise Exception("evm emulator error ", result) + reason = result.get('exit_reason') + if isinstance(reason, str): + raise EthereumError(code=3, message=f'execution finished with error: {reason}') + elif isinstance(reason, dict): + error = None + if 'Error' in reason: + error = decode_error_message(reason.get('Error')) + if (not error) and ('Fatal' in reason): + error = decode_fatal_message(reason.get('Fatal')) + if error: + raise EthereumError(code=3, message=f'execution finished with error: {str(error)}') + raise EthereumError(code=3, message=exit_status) + + +def decode_error_message(reason: str) -> Optional[str]: + ERROR_DICT = { + 'StackUnderflow': 'trying to pop from an empty stack', + 'StackOverflow': 'trying to push into a stack over stack limit', + 'InvalidJump': 'jump destination is invalid', + 'InvalidRange': 'an opcode accesses memory region, but the region is invalid', + 'DesignatedInvalid': 'encountered the designated invalid opcode', + 'CallTooDeep': 'call stack is too deep (runtime)', + 'CreateCollision': 'create opcode encountered collision (runtime)', + 'CreateContractLimit': 'create init code exceeds limit (runtime)', + 'OutOfOffset': 'an opcode accesses external information, but the request is off offset limit (runtime)', + 'OutOfGas': 'execution runs out of gas (runtime)', + 'OutOfFund': 'not enough fund to start the execution (runtime)', + 'PCUnderflow': 'PC underflow (unused)', + 'CreateEmpty': 'attempt to create an empty account (runtime, unused)', + 'StaticModeViolation': 'STATICCALL tried to change state', + } + return ERROR_DICT.get(reason) + + +def decode_fatal_message(reason: str) -> Optional[str]: + FATAL_DICT = { + 'NotSupported': 'the operation is not supported', + 'UnhandledInterrupt': 'the trap (interrupt) is unhandled', + 'CallErrorAsFatal': 'the environment explicitly set call errors as fatal error' + } + return FATAL_DICT.get(reason) @logged_group("neon.Proxy") @@ -42,10 +82,10 @@ def decode_revert_message(data: str, *, logger) -> Optional[str]: if data_len < 8: raise Exception(f"Too less bytes to decode revert signature: {data_len}, data: 0x{data}") - if data[:8] == '4e487b71': # keccak256("Panic(uint256)") + if data[:8] == '4e487b71': # keccak256("Panic(uint256)") return None - if data[:8] != '08c379a0': # keccak256("Error(string)") + if data[:8] != '08c379a0': # keccak256("Error(string)") logger.debug(f"Failed to decode revert_message, unknown revert signature: {data[:8]}") return None @@ -64,15 +104,170 @@ def decode_revert_message(data: str, *, logger) -> Optional[str]: return message -def parse_emulator_program_error(stderr): - last_line = stderr[-1] - if stderr[-1].find('NeonCli Error (111): Solana program error. InsufficientFunds'): - return 'insufficient funds for transfer' - hdr = 'NeonCli Error (111): ' - pos = last_line.find(hdr) - if pos == -1: - return last_line - return last_line[pos + len(hdr):] +class BaseNeonCliErrorParser: + def __init__(self, msg: str): + self._code = 3 + self._msg = msg + + def execute(self, _) -> (str, int): + return self._msg, self._code + + +class ProxyConfigErrorParser(BaseNeonCliErrorParser): + def __init__(self, msg: str): + BaseNeonCliErrorParser.__init__(self, msg) + self._code = 4 + + def execute(self, _) -> (str, int): + return f'error in Neon Proxy configuration: {self._msg}', self._code + + +class ElfParamErrorParser(BaseNeonCliErrorParser): + def __init__(self, msg: str): + BaseNeonCliErrorParser.__init__(self, msg) + self._code = 4 + + def execute(self, _) -> (str, int): + return f'error on reading ELF parameters from Neon EVM program: {self._msg}', self._code + + +class StorageErrorParser(BaseNeonCliErrorParser): + def execute(self, _) -> (str, int): + return f'error on reading storage of contract: {self._msg}', self._code + + +@logged_group("neon.Proxy") +class ProgramErrorParser(BaseNeonCliErrorParser): + def __init__(self, msg: str): + BaseNeonCliErrorParser.__init__(self, msg) + self._code = -32000 + + def execute(self, err: subprocess.CalledProcessError) -> (str, int): + value = None + msg = 'unknown error' + + is_first_hdr = True + hdr = 'NeonCli Error (111): ' + funds_hdr = 'NeonCli Error (111): Solana program error. InsufficientFunds' + + for line in reversed(err.stderr.split('\n')): + pos = line.find(hdr) + if pos == -1: + continue + + if is_first_hdr: + msg = line[pos + len(hdr):] + if line.find(funds_hdr) == -1: + break + + hdr = 'executor transfer from=' + is_first_hdr = False + continue + + if not value: + hdr = line[pos + len(hdr):] + value_hdr = 'value=' + pos = hdr.find(value_hdr) + value = hdr[pos + len(value_hdr):] + pos = hdr.find('…') + hdr = hdr[:pos] + else: + account = line[pos:] + pos = account.find(' ') + account = account[:pos] + msg = f'insufficient funds for transfer: address {account} want {value}' + break + return msg, self._code + + +class FindAccount(BaseNeonCliErrorParser): + def __init__(self, msg: str): + BaseNeonCliErrorParser.__init__(self, msg) + self._code = -32000 + + @staticmethod + def _find_account(line_list: [str], hdr: str) -> str: + account = None + for line in reversed(line_list): + pos = line.find(hdr) # NeonCli Error (212): Uninitialized account. account= + if pos == -1: + continue + if not account: + account = line[pos + len(hdr):] + pos = account.find(',') + account = account[:pos] + hdr = ' => ' + account # Not found account for 0x1c074b10a40b95d1cfad9da99a59fb6aab20b694 => kNEjs3pevk1fdhkQDUDc1E9eEj4V5puXAwLgMuf5KAE + else: + account = line[:pos] + pos = account.rfind(' ') + account = account[pos + 1:] + break + if not account: + account = 'Unknown' + return account + + +class AccountUninitializedParser(FindAccount): + def execute(self, err: subprocess.CalledProcessError) -> str: + msg = 'error on trying to call the not-initialized contract: ' + hdr = 'NeonCli Error (212): Uninitialized account. account=' + account = self._find_account(err.stderr.split('\n'), hdr) + return msg + account + + +class AccountAlreadyInitializedParser(FindAccount): + def execute(self, err: subprocess.CalledProcessError) -> str: + msg = 'error on trying to initialize already initialized contract: ' + hdr = 'NeonCli Error (213): Account is already initialized. account=' + account = self._find_account(err.stderr.split('\n'), hdr) + return msg + account + + +class DeployToExistingAccountParser(FindAccount): + def execute(self, err: subprocess.CalledProcessError) -> str: + msg = 'error on trying to deploy contract to user account: ' + hdr = 'NeonCli Error (221): Attempt to deploy to existing account at address ' + account = self._find_account(err.stderr.split('\n'), hdr) + return msg + account + + +class TooManyStepsErrorParser(BaseNeonCliErrorParser): + pass + + +class NeonCliErrorParser: + ERROR_PARSER_DICT = { + 102: ProxyConfigErrorParser('cannot read/write data to/from disk'), + 113: ProxyConfigErrorParser('connection problem with Solana node'), + 201: ProxyConfigErrorParser('evm loader is not specified'), + 202: ProxyConfigErrorParser('no information about signer'), + + 111: ProgramErrorParser('ProgramError'), + + 205: ElfParamErrorParser('account not found'), + 226: ElfParamErrorParser('account is not BPF compiled'), + 227: ElfParamErrorParser('account is not upgradeable'), + 241: ElfParamErrorParser('associated PDA not found'), + 242: ElfParamErrorParser('invalid associated PDA'), + + 206: StorageErrorParser('account not found at address'), + 208: StorageErrorParser('code account required'), + 215: StorageErrorParser('contract account expected'), + + 212: AccountUninitializedParser('AccountUninitialized'), + + 213: AccountAlreadyInitializedParser('AccountAlreadyInitialized'), + + 221: DeployToExistingAccountParser('DeployToExistingAccount'), + + 245: TooManyStepsErrorParser('execution requires too lot of EVM steps'), + } + + def execute(self, caption: str, err: subprocess.CalledProcessError) -> (str, int): + parser = self.ERROR_PARSER_DICT.get(err.returncode) + if not parser: + return f'Unknown {caption} error: {err.returncode}', 3 + return parser.execute(err) def emulator(contract, sender, data, value): @@ -81,67 +276,5 @@ def emulator(contract, sender, data, value): try: return neon_cli().call("emulate", "--token_mint", str(NEON_TOKEN_MINT), sender, contract, data, value) except subprocess.CalledProcessError as err: - if err.returncode == 111: - message = parse_emulator_program_error(err.stderr) - elif err.returncode == 102: - message = 'Emulator error: StdIoError' - elif err.returncode == 112: - message = 'Emulator error: SignerError' - elif err.returncode == 113: - message = 'Emulator error: ClientError' - elif err.returncode == 114: - message = 'Emulator error: CliError' - elif err.returncode == 115: - message = 'Emulator error: TpuSenderError' - elif err.returncode == 201: - message = 'Emulator error: EvmLoaderNotSpecified' - elif err.returncode == 202: - message = 'Emulator error: FeePayerNotSpecified' - elif err.returncode == 205: - message = 'Emulator error: AccountNotFound' - elif err.returncode == 206: - message = 'Emulator error: AccountNotFoundAtAddress' - elif err.returncode == 207: - message = 'Emulator error: CodeAccountNotFound' - elif err.returncode == 208: - message = 'Emulator error: CodeAccountRequired' - elif err.returncode == 209: - message = 'Emulator error: IncorrectAccount' - elif err.returncode == 210: - message = 'Emulator error: AccountAlreadyExists' - elif err.returncode == 212: - message = 'Emulator error: AccountUninitialized' - elif err.returncode == 213: - message = 'Emulator error: AccountAlreadyInitialized' - elif err.returncode == 215: - message = 'Emulator error: ContractAccountExpected' - elif err.returncode == 221: - message = 'Emulator error: DeploymentToExistingAccount' - elif err.returncode == 222: - message = 'Emulator error: InvalidStorageAccountOwner' - elif err.returncode == 223: - message = 'Emulator error: StorageAccountRequired' - elif err.returncode == 224: - message = 'Emulator error: AccountIncorrectType' - elif err.returncode == 225: - message = 'Emulator error: AccountDataTooSmall' - elif err.returncode == 226: - message = 'Emulator error: AccountIsNotBpf' - elif err.returncode == 227: - message = 'Emulator error: AccountIsNotUpgradeable' - elif err.returncode == 230: - message = 'Emulator error: ConvertNonceError' - elif err.returncode == 241: - message = 'Emulator error: AssociatedPdaNotFound' - elif err.returncode == 242: - message = 'Emulator error: InvalidAssociatedPda' - elif err.returncode == 243: - message = 'Emulator error: InvalidVerbosityMessage' - elif err.returncode == 244: - message = 'Emulator error: TransactionFailed' - elif err.returncode == 245: - message = 'Emulator error: Too many steps' - else: - message = 'Emulator error: UnknownError' - raise EthereumError(message=message) - + msg, code = NeonCliErrorParser().execute('emulator', err) + raise EthereumError(message=msg, code=code) diff --git a/proxy/common_neon/errors.py b/proxy/common_neon/errors.py index f897a49d7..2fc4937ec 100644 --- a/proxy/common_neon/errors.py +++ b/proxy/common_neon/errors.py @@ -1,6 +1,3 @@ -from enum import Enum - - class EthereumError(Exception): def __init__(self, message, code=-32000, data=None): self.code = code diff --git a/proxy/common_neon/solana_interactor.py b/proxy/common_neon/solana_interactor.py index 45e0a845e..be2175864 100644 --- a/proxy/common_neon/solana_interactor.py +++ b/proxy/common_neon/solana_interactor.py @@ -27,6 +27,7 @@ from ..common_neon.layouts import ACCOUNT_INFO_LAYOUT from ..common_neon.address import EthereumAddress, ether2program from ..common_neon.address import AccountInfoLayout +from ..common_neon.utils import get_from_dict class AccountInfo(NamedTuple): @@ -397,7 +398,15 @@ def _send_multiple_transactions(self, signer: SolanaAccount, tx_list: [Transacti request_list = self._fuzzing_transactions(signer, tx_list, opts, request_list) response_list = self._send_rpc_batch_request('sendTransaction', request_list) - return [SendResult(result=r.get('result'), error=r.get('error')) for r in response_list] + result_list = [] + for response, tx in zip(response_list, tx_list): + result = response.get('result') + error = response.get('error') + if error and get_from_dict('data', 'err') == 'AlreadyProcessed': + error = None + result = tx.signature() + result_list.append(SendResult(result=result, error=error)) + return result_list def send_multiple_transactions(self, signer: SolanaAccount, tx_list: [], waiter, skip_preflight: bool, preflight_commitment: str) -> [{}]: diff --git a/proxy/common_neon/solana_tx_list_sender.py b/proxy/common_neon/solana_tx_list_sender.py index 3169c0e38..1141dbad6 100644 --- a/proxy/common_neon/solana_tx_list_sender.py +++ b/proxy/common_neon/solana_tx_list_sender.py @@ -8,6 +8,7 @@ from .costs import update_transaction_cost from .solana_receipt_parser import SolReceiptParser, SolTxError +from .errors import EthereumError from ..environment import WRITE_TRANSACTION_COST_IN_DB, SKIP_PREFLIGHT, RETRY_ON_FAIL @@ -99,7 +100,7 @@ def send(self) -> SolTxListSender: self._on_post_send() if len(self._tx_list): - raise RuntimeError('Run out of attempts to execute transaction') + raise EthereumError(message='No more retries to complete transaction!') return self def update_transaction_cost(self, receipt_list): diff --git a/proxy/environment.py b/proxy/environment.py index ed00dfebe..98af2eba1 100644 --- a/proxy/environment.py +++ b/proxy/environment.py @@ -168,3 +168,5 @@ def read_elf_params(out_dict, *, logger): NEON_TOKEN_MINT: PublicKey = PublicKey(ELF_PARAMS.get("NEON_TOKEN_MINT")) HOLDER_MSG_SIZE = int(ELF_PARAMS.get("NEON_HOLDER_MSG_SIZE")) CHAIN_ID = int(ELF_PARAMS.get('NEON_CHAIN_ID', None)) +NEON_EVM_VERSION = ELF_PARAMS.get("NEON_PKG_VERSION") +NEON_EVM_REVISION = ELF_PARAMS.get('NEON_REVISION') diff --git a/proxy/plugin/solana_rest_api.py b/proxy/plugin/solana_rest_api.py index 35938588f..42c0ece39 100644 --- a/proxy/plugin/solana_rest_api.py +++ b/proxy/plugin/solana_rest_api.py @@ -26,7 +26,6 @@ from ..http.server import HttpWebServerBasePlugin, httpProtocolTypes from typing import List, Tuple -from .solana_rest_api_tools import neon_config_load from ..common_neon.transaction_sender import NeonTxSender from ..common_neon.solana_interactor import SolanaInteractor from ..common_neon.solana_receipt_parser import SolTxError @@ -37,6 +36,7 @@ from ..common_neon.utils import SolanaBlockInfo from ..common_neon.keys_storage import KeyStorage from ..environment import SOLANA_URL, PP_SOLANA_URL, PYTH_MAPPING_ACCOUNT, EVM_STEP_COUNT, CHAIN_ID, ENABLE_PRIVATE_API +from ..environment import NEON_EVM_VERSION, NEON_EVM_REVISION from ..environment import neon_cli from ..memdb.memdb import MemDB from .gas_price_calculator import GasPriceCalculator @@ -46,7 +46,7 @@ modelInstanceLock = threading.Lock() modelInstance = None -NEON_PROXY_PKG_VERSION = '0.7.4' +NEON_PROXY_PKG_VERSION = '0.7.5' NEON_PROXY_REVISION = 'NEON_PROXY_REVISION_TO_BE_REPLACED' @@ -71,27 +71,20 @@ def __init__(self): self.debug(f"Worker id {self.proxy_id}") - neon_config_load(self) - def neon_proxy_version(self): return 'Neon-proxy/v' + NEON_PROXY_PKG_VERSION + '-' + NEON_PROXY_REVISION def web3_clientVersion(self): - neon_config_load(self) - return self.neon_config_dict['web3_clientVersion'] + return 'Neon/v' + NEON_EVM_VERSION + '-' + NEON_EVM_REVISION def eth_chainId(self): - neon_config_load(self) - # NEON_CHAIN_ID is a string in decimal form - return hex(int(self.neon_config_dict['NEON_CHAIN_ID'])) + return hex(int(CHAIN_ID)) def neon_cli_version(self): return neon_cli().version() def net_version(self): - neon_config_load(self) - # NEON_CHAIN_ID is a string in decimal form - return self.neon_config_dict['NEON_CHAIN_ID'] + return str(CHAIN_ID) def eth_gasPrice(self): return hex(int(self.gas_price_calculator.get_suggested_gas_price())) diff --git a/proxy/plugin/solana_rest_api_tools.py b/proxy/plugin/solana_rest_api_tools.py deleted file mode 100644 index bcf8f8bf8..000000000 --- a/proxy/plugin/solana_rest_api_tools.py +++ /dev/null @@ -1,28 +0,0 @@ -from datetime import datetime -from solana.publickey import PublicKey -from logged_groups import logged_group - -from ..environment import read_elf_params, TIMEOUT_TO_RELOAD_NEON_CONFIG - - -@logged_group("neon.Proxy") -def neon_config_load(ethereum_model, *, logger): - try: - ethereum_model.neon_config_dict - except AttributeError: - logger.debug("loading the neon config dict for the first time!") - ethereum_model.neon_config_dict = dict() - else: - elapsed_time = datetime.now().timestamp() - ethereum_model.neon_config_dict['load_time'] - logger.debug('elapsed_time={} proxy_id={}'.format(elapsed_time, ethereum_model.proxy_id)) - if elapsed_time < TIMEOUT_TO_RELOAD_NEON_CONFIG: - return - - read_elf_params(ethereum_model.neon_config_dict) - ethereum_model.neon_config_dict['load_time'] = datetime.now().timestamp() - # 'Neon/v0.3.0-rc0-d1e4ff618457ea9cbc82b38d2d927e8a62168bec - ethereum_model.neon_config_dict['web3_clientVersion'] = 'Neon/v' + \ - ethereum_model.neon_config_dict['NEON_PKG_VERSION'] + \ - '-' \ - + ethereum_model.neon_config_dict['NEON_REVISION'] - logger.debug(ethereum_model.neon_config_dict)