Summary
Private balance scan on Arbitrum permanently stalls at 50% and cannot recover through any in-app recovery mechanism. The root cause is corrupted LevelDB state in the Electron app data directory, which persists across wallet removal and full app reinstall because the installer preserves %APPDATA%\railway-reactjs\ user data.
This was discovered using Railway Wallet v5.24.21 (which uses the latest @railgun-community/wallet SDK), but the root cause is in the SDK's scan cache management — specifically how LevelDB checkpoints are written and resumed.
Environment
- App: Railway Wallet v5.24.21 (desktop, Windows)
- OS: Windows 11
- Chain: Arbitrum One
- RPC: Alchemy archive endpoint (
https://arb-mainnet.g.alchemy.com/v2/[KEY])
- Transfer type: 0zk-to-0zk private transfer (WETH) between two accounts in the same wallet
- POI state: Sender shows "Missing Internal POI", receiver shows "Missing External POI"
Reproduction
- Initiate a 0zk-to-0zk private transfer on Arbitrum
- Close Railway (possibly during balance scan or POI generation)
- Reopen Railway on Arbitrum — balance scan stalls at 50% indefinitely
- Attempt all in-app recovery options:
- Resync proof on the delayed transaction
- Generate all private POIs
- Rescan private balances
- Switch RPCs (tried 1RPC, drpc, Alchemy with API key, default RPCs)
- Switch chains (Ethereum completes scan quickly, BSC also stalls)
- Remove wallet and re-import from seed phrase — scan still stalls at 50%
- Uninstall and reinstall Railway v5.24.21 — scan still stalls at 50% (wallet data preserved by installer)
- Manually delete
%APPDATA%\railway-reactjs\IndexedDB and %APPDATA%\railway-reactjs\Local Storage from Windows
- Reopen Railway, import wallet — scan progresses past 50% and completes successfully ✅
Root Cause
The wallet SDK stores scan progress and Merkle tree cache in LevelDB databases. When a scan is interrupted mid-write, the LevelDB checkpoint becomes inconsistent:
- The scan cursor persists at the interruption point (50%)
- The accompanying Merkle tree data is incomplete/corrupt
- Every subsequent scan resumes from the corrupted checkpoint and deadlocks at the same point
Why refreshBalances() doesn't recover: The function resumes from the cached checkpoint, not from block 0. A corrupted checkpoint means the scan can never advance past the corruption point.
Why wallet removal doesn't help: Removing the wallet only clears the encrypted keystore entry — the scan cache persists separately in the same LevelDB store.
Why reinstalling the app doesn't help: The installer preserves %APPDATA%\railway-reactjs\ user data, so the corrupted LevelDB files survive.
Why switching to Ethereum works: Ethereum has a separate scan cache and different block range, so it's unaffected by the Arbitrum cache corruption.
Related Issue
#110 (LevelDB Locking Issue) — related LevelDB reliability concern in the SDK.
Suggested Improvements
1. Checkpoint validation on scan resume (deep fix)
Before resuming a scan from a cached checkpoint, validate that the cached Merkle tree state is consistent with the chain's current state. If validation fails, discard the cached checkpoint and start a fresh scan automatically.
2. Stall detection
If a balance scan shows no progress for N seconds (e.g., 60s), the SDK should signal a stall state via onMerkletreeScanCallback so the wallet UI can prompt the user to rebuild the cache.
3. Cache rebuild API
Expose a function (e.g., rebuildScanCache(chain, railgunWalletID)) that clears the Merkle tree scan cache for a specific chain without affecting wallet configuration. This would allow wallet apps to offer a "Rebuild Scan Cache" button without requiring users to manually delete app data directories.
4. Separate scan cache from wallet keystore
Currently, scan progress and wallet configuration share the same LevelDB store, which is why removing a wallet doesn't clear the scan state. Isolating these data stores would make cache rebuilds safer and more predictable.
Additional Context
- RAILGUN docs reference: https://docs.railgun.org/developer-guide/wallet/private-balances/balance-and-sync-callbacks
- The scan callback system (
onMerkletreeScanCallback, onBalanceUpdateCallback) would be the right place to implement stall detection
- The
refreshBalances() function controls the scan loop
- This issue also affects BSC (observed similar stall behavior on BSC during testing)
- Railway Wallet's GitHub repo has issues disabled — they redirect support to t.me/railwaywallet — so posting here as the SDK-level issue
Workaround (for affected users)
On Windows:
- Close Railway completely
- Delete these two folders:
%APPDATA%\railway-reactjs\IndexedDB
%APPDATA%\railway-reactjs\Local Storage
- Reopen Railway and re-import your wallet using your seed phrase
- Reconfigure your RPC settings
- Allow the balance scan to complete from scratch
Summary
Private balance scan on Arbitrum permanently stalls at 50% and cannot recover through any in-app recovery mechanism. The root cause is corrupted LevelDB state in the Electron app data directory, which persists across wallet removal and full app reinstall because the installer preserves
%APPDATA%\railway-reactjs\user data.This was discovered using Railway Wallet v5.24.21 (which uses the latest
@railgun-community/walletSDK), but the root cause is in the SDK's scan cache management — specifically how LevelDB checkpoints are written and resumed.Environment
https://arb-mainnet.g.alchemy.com/v2/[KEY])Reproduction
%APPDATA%\railway-reactjs\IndexedDBand%APPDATA%\railway-reactjs\Local Storagefrom WindowsRoot Cause
The wallet SDK stores scan progress and Merkle tree cache in LevelDB databases. When a scan is interrupted mid-write, the LevelDB checkpoint becomes inconsistent:
Why
refreshBalances()doesn't recover: The function resumes from the cached checkpoint, not from block 0. A corrupted checkpoint means the scan can never advance past the corruption point.Why wallet removal doesn't help: Removing the wallet only clears the encrypted keystore entry — the scan cache persists separately in the same LevelDB store.
Why reinstalling the app doesn't help: The installer preserves
%APPDATA%\railway-reactjs\user data, so the corrupted LevelDB files survive.Why switching to Ethereum works: Ethereum has a separate scan cache and different block range, so it's unaffected by the Arbitrum cache corruption.
Related Issue
#110 (LevelDB Locking Issue) — related LevelDB reliability concern in the SDK.
Suggested Improvements
1. Checkpoint validation on scan resume (deep fix)
Before resuming a scan from a cached checkpoint, validate that the cached Merkle tree state is consistent with the chain's current state. If validation fails, discard the cached checkpoint and start a fresh scan automatically.
2. Stall detection
If a balance scan shows no progress for N seconds (e.g., 60s), the SDK should signal a stall state via
onMerkletreeScanCallbackso the wallet UI can prompt the user to rebuild the cache.3. Cache rebuild API
Expose a function (e.g.,
rebuildScanCache(chain, railgunWalletID)) that clears the Merkle tree scan cache for a specific chain without affecting wallet configuration. This would allow wallet apps to offer a "Rebuild Scan Cache" button without requiring users to manually delete app data directories.4. Separate scan cache from wallet keystore
Currently, scan progress and wallet configuration share the same LevelDB store, which is why removing a wallet doesn't clear the scan state. Isolating these data stores would make cache rebuilds safer and more predictable.
Additional Context
onMerkletreeScanCallback,onBalanceUpdateCallback) would be the right place to implement stall detectionrefreshBalances()function controls the scan loopWorkaround (for affected users)
On Windows:
%APPDATA%\railway-reactjs\IndexedDB%APPDATA%\railway-reactjs\Local Storage