Repository: rotki/rotki

Issue:
Unable to delete or modify private RPC nodes
## Problem Definition

I have added two private Ethereum RPC nodes. When I try to delete or modify either of them I get a message "Request failed with status code 500" and the modifications fail.

I haven't tried messing with any of the public endpoints, so the issue may or may not also apply to them.

## Logs

This seems to be the relevant part of the log:

```
[30/08/2022 22:40:13 GMT Summer Time] DEBUG rotkehlchen.api.server Greenlet-6: start rotki api DELETE /api/1/blockchains/ETH/nodes view_args={}, query_string=b''
[30/08/2022 22:40:13 GMT Summer Time] CRITICAL rotkehlchen.api.server Greenlet-6: Unhandled exception when processing endpoint request exc_info=True, exception=[<class 'decimal.DivisionUndefined'>]
[30/08/2022 22:40:13 GMT Summer Time] DEBUG rotkehlchen.api.server Greenlet-6: end rotki api DELETE /api/1/blockchains/ETH/nodes view_args={}, query_string=b'', status_code=500, result={'result': None, 'message': "[<class 'decimal.DivisionUndefined'>]"}
[30/08/2022 22:40:13 GMT Summer Time] INFO geventwebsocket.handler 127.0.0.1 - - [2022-08-30 22:40:13] "DELETE /api/1/blockchains/ETH/nodes HTTP/1.1" 500 275 0.004886
```

### System Description

Operating system:  Windows
Rotki version: 1.25.2



Discussion:
Hey @benjaminion Can you describe the steps you followed to get to this? How many nodes are in rotki, what weights and at what point the edit/delete got you there?

There is definitely a problem in the new logic we introduced there. But I hit something different :sweat_smile: 
Cutting and pasting the node list from the app (can provide images if easier):

```
etherscan Etherescan API used by rotki Node weight 0%
mycrypto https://api.mycryptoapi.com/eth Node weight 0%
blockscout https://mainnet-nethermind.blockscout.com/ Node weight 0%
avado pool https://mainnet.eth.cloud.ava.do/ Node weight 0%
1inch https://web3.1inch.exchange Node weight 0%
myetherwallet https://names.mewapi.io/rpc/eth Node weight 0%
cloudflare https://cloudflare-eth.com/ Node weight 0%
my node https://mainnet.infura.io/v3/[redacted] Private nodes take priority over public nodes
MetalAlbert http://192.168.1.203:8545 Private nodes take priority over public nodes
```

The first 7 are automatically configured and I have not touched them. I did not change their weights myself, although they seem to have become set to zero at some point.

Quite a while ago I added "my node" which is an Infura endpoint.

Yesterday I successfully added "MetalAlbert".

I want to delete "my node", but trying to use the delete button gives the 500 error. Likewise anything I try to modify about the private nodes also gives 500.
> The first 7 are automatically configured and I have not touched them. I did not change their weights myself, although they seem to have become set to zero at some point.

That's really weird.

Let us investigate, there seem to be multiple things a bit broken in 1.25.2 there.

 Scheduled it for our bugfixes branch but we most probably won't make 1.25.3 and it will be included in 1.26.0 directly.

Resolving patch:
diff --git a/rotkehlchen/api/rest.py b/rotkehlchen/api/rest.py
--- a/rotkehlchen/api/rest.py
+++ b/rotkehlchen/api/rest.py
@@ -2534,6 +2534,7 @@ def add_web3_node(self, node: WeightedNode) -> Response:
             self.rotkehlchen.data.db.add_web3_node(node)
         except InputError as e:
             return api_response(wrap_in_fail_result(str(e)), status_code=HTTPStatus.CONFLICT)
+
         # Update the connected nodes
         nodes_to_connect = self.rotkehlchen.data.db.get_web3_nodes(only_active=True)
         self.rotkehlchen.chain_manager.ethereum.connect_to_multiple_nodes(nodes_to_connect)
@@ -2544,6 +2545,7 @@ def update_web3_node(self, node: WeightedNode) -> Response:
             self.rotkehlchen.data.db.update_web3_node(node)
         except InputError as e:
             return api_response(wrap_in_fail_result(str(e)), status_code=HTTPStatus.CONFLICT)
+
         # Update the connected nodes
         nodes_to_connect = self.rotkehlchen.data.db.get_web3_nodes(only_active=True)
         self.rotkehlchen.chain_manager.ethereum.connect_to_multiple_nodes(nodes_to_connect)
@@ -2554,6 +2556,7 @@ def delete_web3_node(self, identifier: int) -> Response:
             self.rotkehlchen.data.db.delete_web3_node(identifier)
         except InputError as e:
             return api_response(wrap_in_fail_result(str(e)), status_code=HTTPStatus.CONFLICT)
+
         # Update the connected nodes
         nodes_to_connect = self.rotkehlchen.data.db.get_web3_nodes(only_active=True)
         self.rotkehlchen.chain_manager.ethereum.connect_to_multiple_nodes(nodes_to_connect)
diff --git a/rotkehlchen/api/v1/resources.py b/rotkehlchen/api/v1/resources.py
--- a/rotkehlchen/api/v1/resources.py
+++ b/rotkehlchen/api/v1/resources.py
@@ -485,7 +485,7 @@ def get(self, async_query: bool) -> Response:
 class EthereumNodesResource(BaseMethodView):
 
     put_schema = EthereumNodeSchema()
-    post_schema = EthereumNodeEditSchema()
+    patch_schema = EthereumNodeEditSchema()
     delete_schema = EthereumNodeListDeleteSchema()
 
     @require_loggedin_user()
@@ -514,8 +514,8 @@ def put(
         return self.rest_api.add_web3_node(node)
 
     @require_loggedin_user()
-    @use_kwargs(post_schema, location='json_and_query')
-    def post(
+    @use_kwargs(patch_schema, location='json_and_query')
+    def patch(
         self,
         identifier: int,
         name: str,
diff --git a/rotkehlchen/db/dbhandler.py b/rotkehlchen/db/dbhandler.py
--- a/rotkehlchen/db/dbhandler.py
+++ b/rotkehlchen/db/dbhandler.py
@@ -3348,23 +3348,29 @@ def _rebalance_web3_nodes_weights(
             write_cursor.execute('SELECT identifier, weight FROM web3_nodes WHERE owned=0')
         else:
             write_cursor.execute(
-                'SELECT identifier, weight FROM web3_nodes WHERE identifier != ? AND owned=0',
+                'SELECT identifier, weight FROM web3_nodes WHERE identifier !=? AND owned=0',
                 (exclude_identifier,),
             )
         new_weights = []
         nodes_weights = write_cursor.fetchall()
         weight_sum = sum(FVal(node[1]) for node in nodes_weights)
         for node_id, weight in nodes_weights:
+
             if exclude_identifier:
-                new_weights.append((str(FVal(weight) / weight_sum * proportion_to_share), node_id))
+                new_weight = FVal(weight) / weight_sum * proportion_to_share if weight_sum != ZERO else ZERO  # noqa: E501
             else:
-                new_weights.append((str(FVal(weight) / weight_sum), node_id))
+                new_weight = FVal(weight) / weight_sum if weight_sum != ZERO else ZERO
+            new_weights.append((str(new_weight), node_id))
+
         write_cursor.executemany(
             'UPDATE web3_nodes SET weight=? WHERE identifier=?',
             new_weights,
         )
 
     def add_web3_node(self, node: WeightedNode) -> None:
+        """
+        Adds a new web3 node.
+        """
         with self.user_write() as cursor:
             try:
                 cursor.execute(
@@ -3380,10 +3386,12 @@ def add_web3_node(self, node: WeightedNode) -> None:
             )
 
     def update_web3_node(self, node: WeightedNode) -> None:
-        with self.conn.read_ctx() as cursor:
-            cursor.execute('SELECT COUNT(*) FROM web3_nodes WHERE name=?', (node.node_info.name,))
-            if cursor.fetchone() != (1,):
-                raise InputError(f'Node with name {node.node_info.name} doesn\'t exist')
+        """
+        Edits an existing web3 node
+
+        May raise:
+        - InputError if no entry with such
+        """
         with self.user_write() as cursor:
             cursor.execute(
                 'UPDATE web3_nodes SET name=?, endpoint=?, owned=?, active=?, weight=? WHERE identifier=?',  # noqa: E501
@@ -3396,6 +3404,10 @@ def update_web3_node(self, node: WeightedNode) -> None:
                     node.identifier,
                 ),
             )
+
+            if cursor.rowcount == 0:
+                raise InputError(f'Node with identifier {node.identifier} doesn\'t exist')
+
             self._rebalance_web3_nodes_weights(
                 write_cursor=cursor,
                 proportion_to_share=ONE - node.weight,


Test patch:
diff --git a/rotkehlchen/tests/api/test_misc.py b/rotkehlchen/tests/api/test_misc.py
--- a/rotkehlchen/tests/api/test_misc.py
+++ b/rotkehlchen/tests/api/test_misc.py
@@ -199,8 +199,26 @@ def test_manage_ethereum_nodes(rotkehlchen_api_server):
         status_code=HTTPStatus.BAD_REQUEST,
     )
 
-    # try to edit a node
-    response = requests.post(
+    # try to edit an unknown node
+    response = requests.patch(
+        api_url_for(rotkehlchen_api_server, 'ethereumnodesresource'),
+        json={
+            'identifier': 666,
+            'name': '1inch',
+            'endpoint': 'ewarwae',
+            'owned': True,
+            'weight': '40',
+            'active': True,
+        },
+    )
+    assert_error_response(
+        response=response,
+        contained_in_msg="Node with identifier 666 doesn't exist",
+        status_code=HTTPStatus.CONFLICT,
+    )
+
+    # try to edit a node's endpoint
+    response = requests.patch(
         api_url_for(rotkehlchen_api_server, 'ethereumnodesresource'),
         json={
             'identifier': 8,
@@ -215,12 +233,38 @@ def test_manage_ethereum_nodes(rotkehlchen_api_server):
     response = requests.get(api_url_for(rotkehlchen_api_server, 'ethereumnodesresource'))
     result = assert_proper_response_with_result(response)
     for node in result:
-        if node['name'] == '1inch':
+        if node['identifier'] == 8:
+            assert FVal(node['weight']) == 40
+            assert node['name'] == '1inch'
+            assert node['active'] is True
+            assert node['endpoint'] == 'ewarwae'
+            assert node['owned'] is True
+            break
+
+    # try to edit a node's name
+    response = requests.patch(
+        api_url_for(rotkehlchen_api_server, 'ethereumnodesresource'),
+        json={
+            'identifier': 8,
+            'name': 'oneinch',
+            'endpoint': 'ewarwae',
+            'owned': True,
+            'weight': '40',
+            'active': True,
+        },
+    )
+    assert_proper_response(response)
+    response = requests.get(api_url_for(rotkehlchen_api_server, 'ethereumnodesresource'))
+    result = assert_proper_response_with_result(response)
+    for node in result:
+        if node['identifier'] == 8:
             assert FVal(node['weight']) == 40
+            assert node['name'] == 'oneinch'
             assert node['active'] is True
             assert node['endpoint'] == 'ewarwae'
             assert node['owned'] is True
             break
+
     result = assert_proper_response_with_result(response)
     response = requests.put(
         api_url_for(rotkehlchen_api_server, 'ethereumnodesresource'),
@@ -233,7 +277,7 @@ def test_manage_ethereum_nodes(rotkehlchen_api_server):
         },
     )
     # set owned to false and see that we have the expected amount of nodes
-    response = requests.post(
+    response = requests.patch(
         api_url_for(rotkehlchen_api_server, 'ethereumnodesresource'),
         json={
             'identifier': 4,
@@ -254,7 +298,7 @@ def test_manage_ethereum_nodes(rotkehlchen_api_server):
             break
 
     # Try to edit etherscan weight
-    response = requests.post(
+    response = requests.patch(
         api_url_for(rotkehlchen_api_server, 'ethereumnodesresource'),
         json={
             'identifier': 1,
@@ -267,6 +311,24 @@ def test_manage_ethereum_nodes(rotkehlchen_api_server):
     )
     assert_proper_response_with_result(response)
 
+    # and now let's replicate https://github.com/rotki/rotki/issues/4769 by
+    # editing all nodes to have 0% weight.
+    response = requests.get(api_url_for(rotkehlchen_api_server, 'ethereumnodesresource'))
+    result = assert_proper_response_with_result(response)
+    for node in result:
+        response = requests.patch(
+            api_url_for(rotkehlchen_api_server, 'ethereumnodesresource'),
+            json={
+                'identifier': node['identifier'],
+                'name': node['name'],
+                'endpoint': node['endpoint'],
+                'owned': node['owned'],
+                'weight': '0',
+                'active': node['active'],
+            },
+        )
+        assert_proper_response(response)
+
 
 @pytest.mark.parametrize('sleep_secs', [60])
 def test_configuration(rotkehlchen_api_server):