Skip to content

Commit

Permalink
Parse revert reason when call fails
Browse files Browse the repository at this point in the history
Solidity allows messages in `require` statements which can help a lot
when debugging. This commit parses the revert reasons from failing
`call`s and raises them as `SolidityError` exceptions.

Closes ethereum#941.
  • Loading branch information
karlb committed Feb 25, 2020
1 parent fc997a3 commit be352bb
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 5 deletions.
1 change: 1 addition & 0 deletions newsfragments/941.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Raise `SolidityError` exceptions that contain the revert reason when a `call` fails.
65 changes: 65 additions & 0 deletions tests/core/utilities/test_method_formatters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import pytest

from web3._utils.method_formatters import (
get_error_formatters,
get_revert_reason,
)
from web3._utils.rpc_abi import (
RPC,
)
from web3.exceptions import (
SolidityError,
)
from web3.types import (
RPCResponse,
)

REVERT_WITH_MSG = RPCResponse({
'jsonrpc': '2.0',
'error': {
'code': -32015,
'message': 'VM execution error.',
'data': (
'Reverted '
'0x08c379a'
'00000000000000000000000000000000000000000000000000000000000000020'
'0000000000000000000000000000000000000000000000000000000000000016'
'6e6f7420616c6c6f77656420746f206d6f6e69746f7200000000000000000000'
),
},
'id': 2987,
})

REVERT_WITHOUT_MSG = RPCResponse({
'jsonrpc': '2.0',
'error': {
'code': -32015,
'message': 'VM execution error.',
'data': 'Reverted 0x',
},
'id': 2987,
})

OTHER_ERROR = RPCResponse({
"jsonrpc": "2.0",
"error": {
"code": -32601,
"message": "Method not found",
},
"id": 1,
})


def test_get_revert_reason() -> None:
assert get_revert_reason(REVERT_WITH_MSG) == 'not allowed to monitor'
assert get_revert_reason(REVERT_WITHOUT_MSG) == ''
assert get_revert_reason(OTHER_ERROR) is None


def test_get_error_formatters() -> None:
formatters = get_error_formatters(RPC.eth_call)
with pytest.raises(SolidityError, match='not allowed to monitor'):
formatters(REVERT_WITH_MSG)
with pytest.raises(SolidityError):
formatters(REVERT_WITHOUT_MSG)
assert formatters(OTHER_ERROR) == OTHER_ERROR
58 changes: 54 additions & 4 deletions web3/_utils/method_formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,12 @@
from web3.datastructures import (
AttributeDict,
)
from web3.exceptions import (
SolidityError,
)
from web3.types import (
RPCEndpoint,
RPCResponse,
TReturn,
)

Expand Down Expand Up @@ -431,7 +435,6 @@ def apply_list_to_array_formatter(formatter: Any) -> Callable[..., Any]:
RPC.net_peerCount: to_integer_if_hex,
}


ATTRDICT_FORMATTER = {
'*': apply_formatter_if(is_dict and not_attrdict, AttributeDict.recursive)
}
Expand All @@ -452,6 +455,54 @@ def apply_list_to_array_formatter(formatter: Any) -> Callable[..., Any]:
ABI_REQUEST_FORMATTERS = abi_request_formatters(STANDARD_NORMALIZERS, RPC_ABIS)


def get_revert_reason(response: RPCResponse) -> str:
"""
Parse revert reason from response, return None if no revert happened.
If a revert happened, but no message has been given, return an empty string.
Reverts contain a `data` attribute with the following layout:
"Reverted "
Function selector for Error(string): 08c379a (4 bytes)
Data offset: 32 (32 bytes)
String length (32 bytes)
Reason strong (padded, use string length from above to get meaningful part)
See also https://solidity.readthedocs.io/en/v0.6.3/control-structures.html#revert
"""
assert 'error' in response
if not isinstance(response['error'], dict):
return None

data = response['error'].get('data', '')

if data == 'Reverted 0x':
return ''

# "Reverted", function selector and offset are always the same for revert errors
prefix = 'Reverted 0x08c379a00000000000000000000000000000000000000000000000000000000000000020'
if not data.startswith(prefix):
return None

reason_length = int(data[len(prefix):len(prefix) + 64], 16)
reason = data[len(prefix) + 64:len(prefix) + 64 + reason_length * 2]
return bytes.fromhex(reason).decode('utf8')


def raise_solidity_error_on_revert(response: RPCResponse) -> RPCResponse:
revert_reason = get_revert_reason(response)
if revert_reason is None:
return response
if revert_reason == '':
raise SolidityError()
raise SolidityError(revert_reason)


ERROR_FORMATTERS: Dict[RPCEndpoint, Callable[..., Any]] = {
RPC.eth_call: raise_solidity_error_on_revert,
}


@to_tuple
def combine_formatters(
formatter_maps: Collection[Dict[RPCEndpoint, Callable[..., TReturn]]], method_name: RPCEndpoint
Expand Down Expand Up @@ -487,10 +538,9 @@ def get_result_formatters(

def get_error_formatters(
method_name: Union[RPCEndpoint, Callable[..., RPCEndpoint]]
) -> Dict[str, Callable[..., Any]]:
) -> Callable[..., Any]:
# Note error formatters work on the full response dict
# TODO - test this function
error_formatter_maps = ()
error_formatter_maps = (ERROR_FORMATTERS, )
formatters = combine_formatters(error_formatter_maps, method_name)

return compose(*formatters)
8 changes: 8 additions & 0 deletions web3/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,11 @@ class InvalidEventABI(ValueError):
Raised when the event ABI is invalid.
"""
pass


class SolidityError(ValueError):
# Inherits from ValueError for backwards compatibility
"""
Raised on a solidity require/revert
"""
pass
2 changes: 1 addition & 1 deletion web3/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ def request_blocking(
response = self._make_request(method, params)

if "error" in response:
apply_error_formatters(error_formatters, response)
response = apply_error_formatters(error_formatters, response)
raise ValueError(response["error"])

return response['result']
Expand Down
1 change: 1 addition & 0 deletions web3/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ class EventData(TypedDict):
class RPCError(TypedDict):
code: int
message: str
data: Optional[str]


class RPCResponse(TypedDict, total=False):
Expand Down

0 comments on commit be352bb

Please sign in to comment.