Skip to content

[Bug] WalletNode close-to-peak sync can strand valid CoinState updates in peer-local race_cache, leaving newly received coins temporarily unspendable #20707

@areksaxyz

Description

@areksaxyz

What happened?

WalletNode.add_states_from_peer() stores untrusted CoinStateUpdate data in a per-peer race_cache when fork_height is present, but sync_from_untrusted_close_to_peak() only drains the race_cache of the peer whose new_peak is currently being processed.

Because of this, one synced peer can advance wallet processing while valid wallet updates from another synced peer remain stranded in that other peer’s cache. If the late peer’s new_peak is then processed after the wallet has already moved forward on the close-to-peak sync path, the normal path can return without ever applying the valid cached update from that other peer.

This is a correctness / robustness issue, not a fund-loss report. Recovery exists via wallet restart, peer reconnection, or connection to a new peer. However, in the normal two-synced-peer close-to-peak path there is no automatic recovery, and the effect is broader than stale visibility alone: newly received confirmed coins can remain temporarily unspendable through both internal spend flow and user-facing RPC spend flow until a separate recovery/apply path occurs.

Reproduction

I reproduced this with deterministic tests added to chia/_tests/wallet/test_wallet_node.py:

  • test_cross_peer_race_cache_misses_wallet_update
  • test_cross_peer_race_cache_breaks_spend_path
  • test_cross_peer_race_cache_breaks_rpc_spend_path

From the repository root:

cd /home/areksaxyz/chia/chia-blockchain
timeout 120s ./activated.py pytest -n 0 \
  chia/_tests/wallet/test_wallet_node.py::test_cross_peer_race_cache_misses_wallet_update \
  chia/_tests/wallet/test_wallet_node.py::test_cross_peer_race_cache_breaks_spend_path \
  chia/_tests/wallet/test_wallet_node.py::test_cross_peer_race_cache_breaks_rpc_spend_path -q

Observed result:
3 passed

What the tests show

  • test_cross_peer_race_cache_misses_wallet_update
  • a valid CoinStateUpdate is cached under peer_a
  • later peak processing proceeds via peer_b
  • peer_a’s valid state remains stranded in peer_a’s race_cache
  • the wallet coin record is still missing

test_cross_peer_race_cache_breaks_spend_path

  • before recovery, wallet.select_coins(uint64(1), ...) fails
  • before recovery, wallet.generate_signed_transaction([uint64(1)], ...) fails
  • after applying the same valid CoinState, spendable balance becomes available and the same spend flow succeeds

test_cross_peer_race_cache_breaks_rpc_spend_path

  • before recovery, WalletRpcApi.select_coins() fails
  • before recovery, WalletRpcApi.create_signed_transaction() fails
  • after applying the same valid CoinState, the same RPC flows succeed

Expected behavior

  • valid wallet updates relevant to the processed peak should be applied exactly once regardless of which synced peer supplied the corresponding new_peak
  • newly received confirmed funds should become usable through normal spend flows once the wallet has processed the relevant peak

Actual behavior

  • valid wallet updates can remain stranded in another synced peer’s race_cache
  • the wallet can advance through the relevant peak while the valid state remains unapplied
  • internal spend-path and RPC spend-path operations fail until a later recovery or explicit apply path occurs

Relevant references

  • helper creating the stranded-update condition: chia/_tests/wallet/test_wallet_node.py:55
  • stale-state PoC: chia/_tests/wallet/test_wallet_node.py:734
  • internal spend-path PoC: chia/_tests/wallet/test_wallet_node.py:841
  • RPC spend-path PoC: chia/_tests/wallet/test_wallet_node.py:882
  • per-peer cache creation: chia/wallet/wallet_node.py:206
  • untrusted updates stored in peer-local race_cache: chia/wallet/wallet_node.py:877
  • CoinStateUpdate processing entrypoint: chia/wallet/wallet_node.py:1011
  • only the current peer’s race_cache is drained during close-to-peak sync: chia/wallet/wallet_node.py:1239

This issue is distinct from the public fork-boundary / off-by-one race-cache bug. This reproduction does not rely on fork-boundary behavior. The root cause here is cross-peer race_cache isolation / peak-source mismatch.

Version

What platform are you using?

Linux

What ui mode are you using?

CLI

Relevant log output

timeout 120s ./activated.py pytest -n 0 \
  chia/_tests/wallet/test_wallet_node.py::test_cross_peer_race_cache_misses_wallet_update \
  chia/_tests/wallet/test_wallet_node.py::test_cross_peer_race_cache_breaks_spend_path \
  chia/_tests/wallet/test_wallet_node.py::test_cross_peer_race_cache_breaks_rpc_spend_path -q

==================================================================== test session starts ====================================================================
platform linux -- Python 3.13.11, pytest-8.4.2, pluggy-1.5.0
rootdir: /home/areksaxyz/chia/chia-blockchain
configfile: pytest.ini
plugins: mock-3.15.1, rerunfailures-16.1, anyio-4.12.1, xdist-3.8.0, tach-0.33.4, cov-7.0.0
collected 3 items

chia/_tests/wallet/test_wallet_node.py ...                                                                                                             [3/3]

===================================================================== 3 passed in 2.05s =====================================================================

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions