Repository: rotki/rotki

Issue:
Handle binance clock skew error more gracefully
## Abstract

Many users have seen this behavior from binance due to not completely synced local time clocks. And due to the way rotki handles it, each time we had to spend some time debugging to realize what is wrong. We should handle it more gracefully and show an actual warning to the user.

Definitely not this:

```
[16/09/2020 17:41:40 Mitteleuropäische Sommerzeit] rotkehlchen.exchanges.binance: Binance API request request_url=https://api.binance.com/api/v3/account?recvWindow=10000&timestamp=1600270900071&signature=[REDACTED]
[16/09/2020 17:41:40 Mitteleuropäische Sommerzeit] rotkehlchen.api.server: Exception on /api/1/exchanges [PUT]
Traceback (most recent call last):
  File "site-packages\flask\app.py", line 1949, in full_dispatch_request
  File "site-packages\flask\app.py", line 1935, in dispatch_request
  File "site-packages\flask_restful\__init__.py", line 458, in wrapper
  File "site-packages\flask\views.py", line 89, in view
  File "site-packages\flask_restful\__init__.py", line 573, in dispatch_request
  File "site-packages\webargs\core.py", line 366, in wrapper
  File "rotkehlchen\api\v1\resources.py", line 197, in put
  File "rotkehlchen\api\rest.py", line 125, in wrapper
  File "rotkehlchen\api\rest.py", line 393, in setup_exchange
  File "rotkehlchen\rotkehlchen.py", line 810, in setup_exchange
  File "rotkehlchen\exchanges\manager.py", line 94, in setup_exchange
  File "rotkehlchen\exchanges\binance.py", line 204, in validate_api_key
  File "rotkehlchen\exchanges\binance.py", line 295, in api_query_dict
  File "rotkehlchen\exchanges\binance.py", line 273, in api_query
rotkehlchen.errors.RemoteError: Binance API request https://api.binance.com/api/v3/account?recvWindow=10000&timestamp=1600270900071&signature=[SIGNATURE] for account failed with HTTP status code: 400, error code: -1021 and error message: Timestamp for this request was 1000ms ahead of the server's time.
[16/09/2020 17:41:40 Mitteleuropäische Sommerzeit] rotkehlchen.api.server.pywsgi: 127.0.0.1 - - "PUT /api/1/exchanges HTTP/1.1" 500 216 0.373099
[16/09/2020 17:41:43 Mitteleuropäische Sommerzeit] rotkehlchen.api.rest: Request successful response={"result": {"warnings": [], "errors": []}, "message": ""}, status_code=200
```

![7qvJzcDY](https://user-images.githubusercontent.com/1658405/93365548-36454880-f84a-11ea-9fd5-48bfe685784a.jpg)

## Task

Do it.


Discussion:
I took a shoot at this one, but I dont really know how to test it (I dont have a binance key)

https://github.com/rudygt/rotki

any suggestions how I can verify that this works would be appreciated, 

Thanks
Hey @rudygt thanks a lot for putting the time to try and address it.

It's probably my bad but this is not what I really meant. I wanted to simply handle the error better too. A 500 Error should never happen and is always a bug on our implementation. And then warn the user to sync their system clock as it may have skewed.

You tried to handle the actual underlying error from what I see. Your approach is not bad. It does 1 extra API call at the beginning and then uses the offer in each API call. Can you open a PR for this though? It's hard to check it from your branch. I did have a look regardless and it does not work as it is right now since you put time in the `V3` endpoints which for us means the private ones, and it tries to authenticate it. Put it in the `V1` endpoints and then it works.

Can you open a PR and add a changelog entry too?
Great, I did created the pull 

https://github.com/rotki/rotki/pull/1485

Thanks!

Resolving patch:
diff --git a/rotkehlchen/chain/ethereum/graph.py b/rotkehlchen/chain/ethereum/graph.py
--- a/rotkehlchen/chain/ethereum/graph.py
+++ b/rotkehlchen/chain/ethereum/graph.py
@@ -35,8 +35,8 @@ def query(
         log.debug(f'Querying The Graph for {querystr}')
         try:
             result = self.client.execute(gql(prefix + querystr), variable_values=param_values)
-        except requests.exceptions.RequestException:
-            raise RemoteError(f'Failed to query the graph for {querystr}')
+        except (requests.exceptions.RequestException, Exception) as e:
+            raise RemoteError(f'Failed to query the graph for {querystr} due to {str(e)}')
 
         log.debug('Got result from The Graph query')
         return result
diff --git a/rotkehlchen/exchanges/binance.py b/rotkehlchen/exchanges/binance.py
--- a/rotkehlchen/exchanges/binance.py
+++ b/rotkehlchen/exchanges/binance.py
@@ -215,6 +215,11 @@ def validate_api_key(self) -> Tuple[bool, str]:
                 return False, 'Provided API Secret is malformed'
             elif 'Invalid API-key, IP, or permissions for action' in error:
                 return False, 'API Key does not match the given secret'
+            elif 'Timestamp for this request was' in error:
+                return False, (
+                    "Local system clock is not in sync with binance server. "
+                    "Try syncing your system's clock"
+                )
             else:
                 raise
         return True, ''


Test patch:
diff --git a/rotkehlchen/tests/fixtures/blockchain.py b/rotkehlchen/tests/fixtures/blockchain.py
--- a/rotkehlchen/tests/fixtures/blockchain.py
+++ b/rotkehlchen/tests/fixtures/blockchain.py
@@ -13,6 +13,7 @@
 from rotkehlchen.externalapis.etherscan import Etherscan
 from rotkehlchen.premium.premium import Premium
 from rotkehlchen.tests.utils.blockchain import geth_create_blockchain
+from rotkehlchen.tests.utils.ethereum import wait_until_all_nodes_connected
 from rotkehlchen.tests.utils.tests import cleanup_tasks
 from rotkehlchen.tests.utils.zerion import create_zerion_patch, wait_until_zerion_is_initialized
 from rotkehlchen.typing import BTCAddress, ChecksumEthAddress, EthTokenInfo
@@ -115,7 +116,7 @@ def ethereum_manager(
     else:
         endpoint = ethrpc_endpoint
 
-    return EthereumManager(
+    manager = EthereumManager(
         ethrpc_endpoint=endpoint,
         etherscan=etherscan,
         database=database,
@@ -123,6 +124,12 @@ def ethereum_manager(
         greenlet_manager=greenlet_manager,
         connect_at_start=ethereum_manager_connect_at_start,
     )
+    wait_until_all_nodes_connected(
+        ethereum_manager_connect_at_start=ethereum_manager_connect_at_start,
+        ethereum=manager,
+    )
+
+    return manager
 
 
 def _geth_blockchain(
diff --git a/rotkehlchen/tests/fixtures/rotkehlchen.py b/rotkehlchen/tests/fixtures/rotkehlchen.py
--- a/rotkehlchen/tests/fixtures/rotkehlchen.py
+++ b/rotkehlchen/tests/fixtures/rotkehlchen.py
@@ -18,6 +18,7 @@
     maybe_include_cryptocompare_key,
     maybe_include_etherscan_key,
 )
+from rotkehlchen.tests.utils.ethereum import wait_until_all_nodes_connected
 from rotkehlchen.tests.utils.factories import make_random_b64bytes
 from rotkehlchen.tests.utils.history import maybe_mock_historical_price_queries
 from rotkehlchen.tests.utils.zerion import create_zerion_patch, wait_until_zerion_is_initialized
@@ -132,6 +133,11 @@ def mock_get_settings() -> DBSettings:
         default_mock_value=default_mock_price_value,
     )
 
+    wait_until_all_nodes_connected(
+        ethereum_manager_connect_at_start=ethereum_manager_connect_at_start,
+        ethereum=rotki.chain_manager.ethereum,
+    )
+
 
 @pytest.fixture()
 def uninitialized_rotkehlchen(cli_args, inquirer, asset_resolver):  # pylint: disable=unused-argument  # noqa: E501
diff --git a/rotkehlchen/tests/utils/ethereum.py b/rotkehlchen/tests/utils/ethereum.py
--- a/rotkehlchen/tests/utils/ethereum.py
+++ b/rotkehlchen/tests/utils/ethereum.py
@@ -1,5 +1,13 @@
+import logging
+
+import gevent
+
 from rotkehlchen.chain.ethereum.manager import NodeName
 
+NODE_CONNECTION_TIMEOUT = 10
+
+log = logging.getLogger(__name__)
+
 ETHEREUM_TEST_PARAMETERS = ['ethrpc_endpoint,ethereum_manager_connect_at_start, call_order', [
     # Query etherscan only
     ('', (), (NodeName.ETHERSCAN,)),
@@ -10,3 +18,28 @@
         (NodeName.OWN,),
     ),
 ]]
+
+
+def wait_until_all_nodes_connected(
+        ethereum_manager_connect_at_start,
+        ethereum,
+        timeout: int = NODE_CONNECTION_TIMEOUT,
+):
+    """Wait until all ethereum nodes are connected or until a timeout is hit"""
+    connected = [False] * len(ethereum_manager_connect_at_start)
+    try:
+        with gevent.Timeout(timeout):
+            while not all(connected):
+                for idx, node_name in enumerate(ethereum_manager_connect_at_start):
+                    if node_name in ethereum.web3_mapping:
+                        connected[idx] = True
+
+                gevent.sleep(0.1)
+    except gevent.Timeout:
+        names = [
+            str(x) for idx, x in enumerate(ethereum_manager_connect_at_start) if not connected[idx]
+        ]
+        log.info(
+            f'Did not connect to nodes: {",".join(names)} due to '
+            f'timeout of {NODE_CONNECTION_TIMEOUT}',
+        )