Repository: rotki/rotki

Issue:
Invalid ENS names cause Ethereum transaction decoding to halt.
## Problem Definition

While loading my transaction history ('History Events'), the process seems to fail. It is getting stuck when it attempts to load a transaction that minted an ENS name containing an invalid character. In this case, that character was 'ʀ'.

This is the error I receive in the notifications: 'Failed to retrieve missing events from transactions for ethereum chain: Error: The backend query task died unexpectedly: Invalid character: 'ʀ' | codepoint 640 (0x280)'.

Ignoring the NFT or excluding the 'token' when it appears in the 'Newly Detected Tokens' screen has no effect. It continuously runs into the issue.

## Logs

I took the liberty of omitting some of the information that might expose my wallet address. Hopefully, the logs remain useful.
```
[22/02/2024 05:48:07 GMT Standard Time] ERROR rotkehlchen.chain.ethereum.modules.ens.decoder Greenlet-7: Could not decode an ERC721 transfer for an ENS name transfer.
[22/02/2024 05:48:07 GMT Standard Time] DEBUG rotkehlchen.chain.ethereum.graph Greenlet-7: Querying The Graph for query{domains(first:1, where:{id:"id"}){name}}
[22/02/2024 05:48:07 GMT Standard Time] INFO gql.transport.requests >>> {"query": "{\n  domains(\n    first: 1\n    where: {id: \"id\"}\n  ) {\n    name\n  }\n}"}
[22/02/2024 05:48:08 GMT Standard Time] INFO gql.transport.requests <<< {"data":{"domains":[{"name":"ʀ.eth"}]}}
[22/02/2024 05:48:08 GMT Standard Time] DEBUG rotkehlchen.chain.ethereum.graph Greenlet-7: Got result from The Graph query
[22/02/2024 05:48:08 GMT Standard Time] DEBUG rotkehlchen.chain.ethereum.graph Greenlet-7: Querying The Graph for query{domains(first:1, where:{labelhash:"labelhash"}){name}}
[22/02/2024 05:48:08 GMT Standard Time] INFO gql.transport.requests >>> {"query": "{\n  domains(\n    first: 1\n    where: {labelhash: \"labelhash\"}\n  ) {\n    name\n  }\n}"}
[22/02/2024 05:48:08 GMT Standard Time] INFO gql.transport.requests <<< {"data":{"domains":[{"name":"ʀ.eth"}]}}
[22/02/2024 05:48:08 GMT Standard Time] DEBUG rotkehlchen.chain.ethereum.graph Greenlet-7: Got result from The Graph query
[22/02/2024 05:48:08 GMT Standard Time] ERROR rotkehlchen.greenlets.manager Greenlet with id 2583392411200: decode 152 ethereum transactions died with exception: Invalid character: 'ʀ' | codepoint 640 (0x280).
Exception Name: <class 'ens.exceptions.InvalidName'>
Exception Info: Invalid character: 'ʀ' | codepoint 640 (0x280)
Traceback:
   File "src\\gevent\\greenlet.py", line 908, in gevent._gevent_cgreenlet.Greenlet.run
  File "rotkehlchen\chain\evm\decoding\decoder.py", line 516, in get_and_decode_undecoded_transactions
  File "rotkehlchen\chain\evm\decoding\decoder.py", line 573, in decode_transaction_hashes
  File "rotkehlchen\chain\evm\decoding\decoder.py", line 635, in _get_or_decode_transaction_events
  File "rotkehlchen\chain\evm\decoding\decoder.py", line 433, in _decode_transaction
  File "rotkehlchen\chain\evm\decoding\decoder.py", line 345, in decode_by_address_rules
  File "rotkehlchen\chain\ethereum\modules\ens\decoder.py", line 117, in _decode_ens_registrar_event
  File "rotkehlchen\chain\ethereum\modules\ens\decoder.py", line 134, in _decode_name_registered
  File "rotkehlchen\chain\ethereum\modules\ens\decoder.py", line 81, in _save_hash_mappings_get_fullname
  File "ens\base_ens.py", line 60, in namehash
  File "ens\utils.py", line 240, in raw_name_to_hash
  File "ens\utils.py", line 132, in normalize_name
  File "ens\_normalization.py", line 504, in normalize_name_ensip15
```

### System Description

Operating system: Windows 10
Rotki version: v1.32.0



Discussion:
Thanks @jack-richards. We are going to look into this. ~~Can you maybe join our discord and dm us the address that has triggered the issue so we can reproduce it? https://discord.rotki.com/~~ Saw that is in the logs. Thanks!
Actually @jack-richards if you can send me the address that triggers the error would be great. I don't seem to be able to find it just with the ens

Resolving patch:
diff --git a/rotkehlchen/chain/ethereum/modules/ens/decoder.py b/rotkehlchen/chain/ethereum/modules/ens/decoder.py
--- a/rotkehlchen/chain/ethereum/modules/ens/decoder.py
+++ b/rotkehlchen/chain/ethereum/modules/ens/decoder.py
@@ -2,7 +2,7 @@
 from typing import TYPE_CHECKING, Any
 
 import content_hash
-from ens import ENS
+import ens
 
 from rotkehlchen.accounting.structures.balance import Balance
 from rotkehlchen.assets.utils import TokenEncounterInfo, get_or_create_evm_token
@@ -72,20 +72,27 @@ def _save_hash_mappings_get_fullname(name: str) -> str:
     Saves the namehash -> name and labelhash -> name mappings to the global DB
     cache and gets name with.eth suffix.
 
-    The given name has to be without the .eth suffix and is returned with it
+    The given name has to be without the .eth suffix and is returned with it. In the case that
+    the registered name is invalid we return the name but we don't store it in the database cache.
     """
     full_name = f'{name}.eth'
     with GlobalDBHandler().conn.write_ctx() as write_cursor:
-        globaldb_set_unique_cache_value(
-            write_cursor=write_cursor,
-            key_parts=(CacheType.ENS_NAMEHASH, ENS.namehash(full_name).hex()),
-            value=full_name,
-        )
-        globaldb_set_unique_cache_value(
-            write_cursor=write_cursor,
-            key_parts=(CacheType.ENS_LABELHASH, ENS.labelhash(name).hex()),
-            value=name,
-        )
+        try:
+            globaldb_set_unique_cache_value(
+                write_cursor=write_cursor,
+                key_parts=(CacheType.ENS_NAMEHASH, ens.ENS.namehash(full_name).hex()),
+                value=full_name,
+            )
+            globaldb_set_unique_cache_value(
+                write_cursor=write_cursor,
+                key_parts=(CacheType.ENS_LABELHASH, ens.ENS.labelhash(name).hex()),
+                value=name,
+            )
+        except ens.exceptions.InvalidName as e:
+            log.error(
+                f'Got an invalid ENS name {name}. namehash and labelhash not stored '
+                f'in the globaldb cache. {e=}',
+            )
     return full_name
 
 
@@ -332,7 +339,7 @@ def _get_name_to_show(self, node: bytes) -> str | None:
                         log.debug(f'Failed to reverse resolve ENS name ue to {e!s}')
                         return None
 
-        elif queried_graph:  # if we succesfully asked the graph, save the mapping
+        elif queried_graph:  # if we successfully asked the graph, save the mapping
             _save_hash_mappings_get_fullname(name=name_to_show[:-4])
 
         return name_to_show


Test patch:
diff --git a/rotkehlchen/tests/unit/decoders/test_ens.py b/rotkehlchen/tests/unit/decoders/test_ens.py
--- a/rotkehlchen/tests/unit/decoders/test_ens.py
+++ b/rotkehlchen/tests/unit/decoders/test_ens.py
@@ -1,3 +1,4 @@
+from typing import TYPE_CHECKING
 import pytest
 
 from rotkehlchen.accounting.structures.balance import Balance
@@ -10,6 +11,7 @@
     ENS_PUBLIC_RESOLVER_2_ADDRESS,
     ENS_REGISTRAR_CONTROLLER_1,
     ENS_REGISTRAR_CONTROLLER_2,
+    _save_hash_mappings_get_fullname,
 )
 from rotkehlchen.chain.evm.decoding.constants import CPT_GAS
 from rotkehlchen.chain.evm.types import string_to_evm_address
@@ -20,6 +22,7 @@
 from rotkehlchen.history.events.structures.types import HistoryEventSubType, HistoryEventType
 from rotkehlchen.tests.utils.ethereum import get_decoded_events_of_transaction
 from rotkehlchen.types import (
+    CacheType,
     ChainID,
     EvmTokenKind,
     Location,
@@ -28,6 +31,10 @@
     deserialize_evm_tx_hash,
 )
 
+if TYPE_CHECKING:
+    from rotkehlchen.globaldb.handler import GlobalDBHandler
+
+
 ADDY = '0x2B888954421b424C5D3D9Ce9bB67c9bD47537d12'
 
 
@@ -752,3 +759,18 @@ def test_claim_airdrop(database, ethereum_inquirer, ethereum_accounts):
         ),
     ]
     assert events == expected_events
+
+
+def test_invalid_ens_name(globaldb: 'GlobalDBHandler'):
+    """
+    Test that possible exceptions when processing invalid ENS names are handled correctly and
+    nothing gets stored in the database cache for invalid names.
+    """
+    name = 'ʀ'
+    full_name = _save_hash_mappings_get_fullname(name=name)
+    assert full_name == f'{name}.eth'
+    with globaldb.conn.read_ctx() as cursor:
+        for cache_key in (CacheType.ENS_NAMEHASH, CacheType.ENS_LABELHASH):
+            assert cursor.execute(
+                f'SELECT COUNT(*) FROM unique_cache WHERE key LIKE "{cache_key.serialize()}%"',
+            ).fetchone()[0] == 0