Skip to content

Commit

Permalink
Extend errors from emulator. #678
Browse files Browse the repository at this point in the history
  • Loading branch information
afalaleev committed Mar 30, 2022
1 parent 53c41cf commit bab907b
Show file tree
Hide file tree
Showing 7 changed files with 229 additions and 122 deletions.
287 changes: 210 additions & 77 deletions proxy/common_neon/emulator_interactor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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")
Expand All @@ -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

Expand All @@ -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):
Expand All @@ -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)
3 changes: 0 additions & 3 deletions proxy/common_neon/errors.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
from enum import Enum


class EthereumError(Exception):
def __init__(self, message, code=-32000, data=None):
self.code = code
Expand Down
11 changes: 10 additions & 1 deletion proxy/common_neon/solana_interactor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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) -> [{}]:
Expand Down
3 changes: 2 additions & 1 deletion proxy/common_neon/solana_tx_list_sender.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down
2 changes: 2 additions & 0 deletions proxy/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Loading

0 comments on commit bab907b

Please sign in to comment.