Repository: Chia-Network/chia-blockchain

Issue:
[Bug] dictionary changed size during iteration 
### What happened?

when switching between wallets 
![wallet_change_error_GUI](https://user-images.githubusercontent.com/116583441/234676534-8ff77ef9-f1ac-4301-b134-2b52bcc1a437.png)
occured.
after clicking [OK] and clicking on the desired wallet, it will load without issue

### Version

1.8.0rc3

### What platform are you using?

Windows

### What ui mode are you using?

GUI

### Relevant log output or stacktrace

_No response_


Discussion:
Interestingly, this is actually a python error message. @bramv-chia any chance you can find that error message in your debug log?
@paninaro 
2023-04-26T07:06:48.114 wallet chia.wallet.wallet_node    : INFO     Sync (trusted: True) duration was: 51.454047441482544
2023-04-26T07:06:48.148 wallet chia.rpc.rpc_server        : WARNING  Error while handling message: Traceback (most recent call last):
  File "chia\rpc\rpc_server.py", line 340, in safe_handle
  File "chia\rpc\rpc_server.py", line 331, in ws_api
  File "chia\rpc\wallet_rpc_api.py", line 317, in log_in
  File "chia\wallet\wallet_node.py", line 388, in _start_with_fingerprint
RuntimeError: dictionary changed size during iteration
Thanks @bramv-chia I'll transfer this to the blockchain repo and assign it.
@trepca @xdustinface the error message shown in the GUI is originating from the backend when logging into a wallet key. Bram provided the following debug.log output:

```
2023-04-26T07:06:48.114 wallet chia.wallet.wallet_node : INFO Sync (trusted: True) duration was: 51.454047441482544
2023-04-26T07:06:48.148 wallet chia.rpc.rpc_server : WARNING Error while handling message: Traceback (most recent call last):
File "chia\rpc\rpc_server.py", line 340, in safe_handle
File "chia\rpc\rpc_server.py", line 331, in ws_api
File "chia\rpc\wallet_rpc_api.py", line 317, in log_in
File "chia\wallet\wallet_node.py", line 388, in _start_with_fingerprint
RuntimeError: dictionary changed size during iteration
```

Looks to me like it the collection of wallets is updating while iterating over it:

```python
        # Populate the balance caches for all wallets
        for wallet_id in self.wallet_state_manager.wallets:
            await self.get_balance(wallet_id)
```

Resolving patch:
diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py
--- a/chia/wallet/wallet_node.py
+++ b/chia/wallet/wallet_node.py
@@ -385,8 +385,9 @@ async def _start_with_fingerprint(
         self.wallet_state_manager.state_changed("sync_changed")
 
         # Populate the balance caches for all wallets
-        for wallet_id in self.wallet_state_manager.wallets:
-            await self.get_balance(wallet_id)
+        async with self.wallet_state_manager.lock:
+            for wallet_id in self.wallet_state_manager.wallets:
+                await self._update_balance_cache(wallet_id)
 
         async with self.wallet_state_manager.puzzle_store.lock:
             index = await self.wallet_state_manager.puzzle_store.get_last_derivation_path()
@@ -1604,29 +1605,33 @@ async def push_tx(self, spend_bundle: SpendBundle) -> None:
         for peer in full_nodes:
             await peer.send_message(msg)
 
+    async def _update_balance_cache(self, wallet_id: uint32) -> None:
+        assert self.wallet_state_manager.lock.locked(), "WalletStateManager.lock required"
+        wallet = self.wallet_state_manager.wallets[wallet_id]
+        unspent_records = await self.wallet_state_manager.coin_store.get_unspent_coins_for_wallet(wallet_id)
+        balance = await wallet.get_confirmed_balance(unspent_records)
+        pending_balance = await wallet.get_unconfirmed_balance(unspent_records)
+        spendable_balance = await wallet.get_spendable_balance(unspent_records)
+        pending_change = await wallet.get_pending_change_balance()
+        max_send_amount = await wallet.get_max_send_amount(unspent_records)
+
+        unconfirmed_removals: Dict[bytes32, Coin] = await wallet.wallet_state_manager.unconfirmed_removals_for_wallet(
+            wallet_id
+        )
+        self._balance_cache[wallet_id] = Balance(
+            confirmed_wallet_balance=balance,
+            unconfirmed_wallet_balance=pending_balance,
+            spendable_balance=spendable_balance,
+            pending_change=pending_change,
+            max_send_amount=max_send_amount,
+            unspent_coin_count=uint32(len(unspent_records)),
+            pending_coin_removal_count=uint32(len(unconfirmed_removals)),
+        )
+
     async def get_balance(self, wallet_id: uint32) -> Balance:
         self.log.debug(f"get_balance - wallet_id: {wallet_id}")
         if not self.wallet_state_manager.sync_mode:
             self.log.debug(f"get_balance - Updating cache for {wallet_id}")
             async with self.wallet_state_manager.lock:
-                wallet = self.wallet_state_manager.wallets[wallet_id]
-                unspent_records = await self.wallet_state_manager.coin_store.get_unspent_coins_for_wallet(wallet_id)
-                balance = await wallet.get_confirmed_balance(unspent_records)
-                pending_balance = await wallet.get_unconfirmed_balance(unspent_records)
-                spendable_balance = await wallet.get_spendable_balance(unspent_records)
-                pending_change = await wallet.get_pending_change_balance()
-                max_send_amount = await wallet.get_max_send_amount(unspent_records)
-
-                unconfirmed_removals: Dict[
-                    bytes32, Coin
-                ] = await wallet.wallet_state_manager.unconfirmed_removals_for_wallet(wallet_id)
-                self._balance_cache[wallet_id] = Balance(
-                    confirmed_wallet_balance=balance,
-                    unconfirmed_wallet_balance=pending_balance,
-                    spendable_balance=spendable_balance,
-                    pending_change=pending_change,
-                    max_send_amount=max_send_amount,
-                    unspent_coin_count=uint32(len(unspent_records)),
-                    pending_coin_removal_count=uint32(len(unconfirmed_removals)),
-                )
+                await self._update_balance_cache(wallet_id)
         return self._balance_cache.get(wallet_id, Balance())


Test patch: