Repository: ethereum/web3.py

Issue:
raise_contract_logic_error_on_revert fails with UnicodeDecodeError
* Version: 6.4.0
* Python: 3.10
* OS: linux
* `pip freeze` output

```
aiohttp==3.8.4
aiosignal==1.3.1
async-timeout==4.0.2
attrs==23.1.0
bitarray==2.7.3
cached-property==1.5.2
certifi==2023.5.7
charset-normalizer==3.1.0
coverage==7.2.6
cytoolz==0.12.1
eth-abi==4.0.0
eth-account==0.8.0
eth-bloom==2.0.0
eth-hash==0.5.1
eth-keyfile==0.6.1
eth-keys==0.4.0
eth-rlp==0.3.0
eth-typing==3.3.0
eth-utils==2.1.0
frozenlist==1.3.3
hexbytes==0.3.0
idna==3.4
iniconfig==2.0.0
jsonschema==4.17.3
lru-dict==1.2.0
multidict==6.0.4
mypy-extensions==0.4.4
packaging==23.1
parsimonious==0.9.0
pluggy==1.0.0
protobuf==4.23.2
py-ecc==6.0.0
py-evm==0.7.0a2
pycryptodome==3.18.0
pyethash==0.1.27
pyrsistent==0.19.3
pytest==7.3.1
pytest-cov==4.1.0
regex==2023.5.5
requests==2.31.0
rlp==3.0.0
safe-eth-py==5.4.3
safe-pysha3==1.0.4
sortedcontainers==2.4.0
toolz==0.12.0
trie==2.1.0
urllib3==2.0.2
web3==6.4.0
websockets==11.0.3
yarl==1.9.2
```


### What was wrong?

Function `raise_contract_logic_error_on_revert` raises `UnicodeDecodeError` when parsing receipt for the following response
`response = {'error': {'code': -32015, 'data': 'Reverted 0xd27b44a9', 'message': 'VM execution error.'}, 'id': 3, 'jsonrpc': '2.0'}`


* Node: GnosisChain (issue happens for most nodes, https://rpc.gnosischain.com/, https://rpc.ankr.com/gnosis, etc)

```
/opt/hostedtoolcache/Python/3.10.11/x64/lib/python3.10/site-packages/web3/contract/contract.py:461: in call
    return call_contract_function(
/opt/hostedtoolcache/Python/3.10.11/x64/lib/python3.10/site-packages/web3/contract/utils.py:96: in call_contract_function
    return_data = w3.eth.call(
/opt/hostedtoolcache/Python/3.10.11/x64/lib/python3.10/site-packages/web3/eth/eth.py:255: in call
    return self._durin_call(transaction, block_identifier, state_override)
/opt/hostedtoolcache/Python/3.10.11/x64/lib/python3.10/site-packages/web3/eth/eth.py:274: in _durin_call
    return self._call(transaction, block_identifier, state_override)
/opt/hostedtoolcache/Python/3.10.11/x64/lib/python3.10/site-packages/web3/module.py:68: in caller
    result = w3.manager.request_blocking(
/opt/hostedtoolcache/Python/3.10.11/x64/lib/python3.10/site-packages/web3/manager.py:232: in request_blocking
    return self.formatted_response(
/opt/hostedtoolcache/Python/3.10.11/x64/lib/python3.10/site-packages/web3/manager.py:197: in formatted_response
    apply_error_formatters(error_formatters, response)
/opt/hostedtoolcache/Python/3.10.11/x64/lib/python3.10/site-packages/web3/manager.py:73: in apply_error_formatters
    formatted_resp = pipe(response, error_formatters)
cytoolz/functoolz.pyx:666: in cytoolz.functoolz.pipe
    ???
cytoolz/functoolz.pyx:641: in cytoolz.functoolz.c_pipe
    ???
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

response = {'error': {'code': -32015, 'data': 'Reverted 0xd27b44a9', 'message': 'VM execution error.'}, 'id': 3, 'jsonrpc': '2.0'}

    def raise_contract_logic_error_on_revert(response: RPCResponse) -> RPCResponse:
        """
        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 string (padded, use string length from above to get meaningful part)
    
        See also https://solidity.readthedocs.io/en/v0.6.3/control-structures.html#revert
        """
        if not isinstance(response["error"], dict):
            raise ValueError("Error expected to be a dict")
    
        data = response["error"].get("data", "")
    
        # Ganache case:
        if isinstance(data, dict) and response["error"].get("message"):
            raise ContractLogicError(
                f'execution reverted: {response["error"]["message"]}', data=data
            )
    
        # Parity/OpenEthereum case:
        if data.startswith("Reverted "):
            # "Reverted", function selector and offset are always the same for revert errors
            prefix = "Reverted 0x08c379a00000000000000000000000000000000000000000000000000000000000000020"  # noqa: 501
            if not data.startswith(prefix):
                if data.startswith("Reverted 0x"):
                    # Special case for this form: 'Reverted 0x...'
                    receipt = data.split(" ")[1][2:]
>                   revert_reason = bytes.fromhex(receipt).decode("utf-8")
E                   UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd2 in position 0: invalid continuation byte

/opt/hostedtoolcache/Python/3.10.11/x64/lib/python3.10/site-packages/web3/_utils/method_formatters.py:743: UnicodeDecodeError
```



Discussion:
It seems like the message is not able to be decoded. I don't particularly like the current flow of assuming that it can be decoded. Perhaps we can catch the `UnicodeDecodeError` and raise `ContractLogicError` with just the raw `data`. That might be the most constructive way to handle it.

Resolving patch:
diff --git a/web3/_utils/contract_error_handling.py b/web3/_utils/contract_error_handling.py
--- a/web3/_utils/contract_error_handling.py
+++ b/web3/_utils/contract_error_handling.py
@@ -1,3 +1,5 @@
+import warnings
+
 from eth_abi import (
     abi,
 )
@@ -91,8 +93,12 @@ def raise_contract_logic_error_on_revert(response: RPCResponse) -> RPCResponse:
         else:
             raise ContractLogicError("execution reverted", data=data)
 
-        reason_string = bytes.fromhex(reason_as_hex).decode("utf8")
-        raise ContractLogicError(f"execution reverted: {reason_string}", data=data)
+        try:
+            reason_string = bytes.fromhex(reason_as_hex).decode("utf8")
+            raise ContractLogicError(f"execution reverted: {reason_string}", data=data)
+        except UnicodeDecodeError:
+            warnings.warn("Could not decode revert reason as UTF-8", RuntimeWarning)
+            raise ContractLogicError("execution reverted", data=data)
 
     # --- EIP-3668 | CCIP Read --- #
     if data[:10] == OFFCHAIN_LOOKUP_FUNC_SELECTOR:


Test patch: