Skip to content

Stop the chain registry crashing the app on opening (4.2.2 fix for master) - #1327

Open
takemiyamakoto wants to merge 5 commits into
masterfrom
hotfix/chain-registry-launch-crash-master
Open

takemiyamakoto wants to merge 5 commits into
masterfrom
hotfix/chain-registry-launch-crash-master

Conversation

@takemiyamakoto

@takemiyamakoto takemiyamakoto commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

iOS 4.2.1 (2026.11.11) can crash right after unlock with Index out of range in ChainRegistry.webSocketDidChangeState, on the socket state queue (TestFlight crash report: iPhone 16 Pro Max, iOS 27.2.1). Since 4.2.0, the socket engine reports state changes on its own queue. The delegate read the chain list without the registry's lock while the startup chain update was replacing it. Master has the same code: the five changed files are byte-identical to 4.2.1's.

This brings the fix from the 4.2.2 line (hotfix/4.2.0-polkaswap-usability) to master, together with #1326's chain update ordering commit. It leaves out the 4.2.2 version bump, so master stays 4.2.0 (2026.11.5).

Changes

  1. fix: stop the chain registry crashing the app on opening (git cherry-pick -x f3cb35961, applied cleanly)
    • ChainRegistry looks the chain up under its lock, on its own serial queue. Its lock is no longer lazy. performHotBoot, the package adapter and chainsTypesMap read under locks.
    • NetworkIssuesCenter and ChainsIssuesCenter keep their listeners and issues under a lock.
    • EthereumConnectionPool and AccountInfoUpdatingService create their locks with the object.
    • It also adds the fix's tests to ChainRegistryTests.swift.
  2. Refresh the Core Data release gate for master (from 20204c732). Master's reviewed list was still the 424-name list from Consolidate native iOS ownership and wallet recovery #1305, which cannot pass. Recomputed for master plus the fix, it is 483 tests: the same set as the 4.2.2 line.
  3. Deliver chain updates in provider order during wallet startup (git cherry-pick -x 2cd1011a4, from Consolidate every open iOS PR: 4.2.1 (2026.11.11), redesign and brand, config, subscription and test fixes #1326)
    • subscribeToChains took chain batches on DispatchQueue.global(qos: .userInitiated), which is concurrent. Two batches could reach the registry's write lock in either order, so an older node update could overwrite a newer one.
    • The fix's stress test exposed it: it hung for 120 s in about 15% of Release -O runs. In 6 of 40 probe runs, an earlier round was applied after the last one.
    • Batches now arrive on the registry's serial queue. The stress test also stops its reader threads when its wait fails.
    • The branch first had a test-only workaround (89503c771). 10ff8e1e3 reverts it in favour of this commit.

Local results

This project has no hosted CI gate. Everything ran locally at 2668d3624 (base dce8790cd), with Xcode 27.2 on iOS 27.0 simulators.

Check Result
ChainRegistryTests, IssuesCenterConcurrencyTests, IssuesCenterListenerRemovalTests, IssuesCenterReachabilityTests, ConnectionPoolTests, FailoverChainConnectionTests, EthereumNodeFailoverTests, RootTests (Debug) 90 passed, 0 failed
The fix's 7 new tests, 40 iterations each (Debug) 280 of 280 passed
Core Data release gate (scripts/ci/run-coredata-release-gate.sh, Release -O) 483 of 483 passed; the executed identities equal the list (see the note on the Ethereum test)
Stress test and testChosenEthereumNode…, 40 iterations each (Release -O) 80 of 80 passed
Source audits and script self-tests, including the gate self-test 58 of 58 pass
The new tests on master's code without the fix 5 of 7 fail, 3 by crashing the test host. The stress test fails because state changes are dropped.

Notes for review

  • processingQueue has no QoS of its own. With 2cd1011a4, chain update batches therefore run at the QoS they inherit from the provider's utility queue; on DispatchQueue.global(qos: .userInitiated) they ran at user-initiated. These runs showed no slowdown. The same applies to Consolidate every open iOS PR: 4.2.1 (2026.11.11), redesign and brand, config, subscription and test fixes #1326.
  • The existing testChosenEthereumNodeTakesEffect…, which this PR doesn't change, has 10 s waits. In Core Data gate runs right after a fresh app build, it took 8 to 11 s: it failed twice here and took 8.07 s once on the 4.2.2 line. Otherwise it took under 1.1 s. At this head, the gate run right after the rebuild failed on it, and the rerun from the same build passed it in 0.16 s.

Merge order

#1326 is still open. It carries the same fix, the same 483-test gate list and the same ordering commit. ChainRegistry.swift and ChainRegistryTests.swift are identical on both branches, and a trial merge of this branch with #1326's head is clean.

🤖 Generated with Claude Code

takemiyamakoto and others added 3 commits November 13, 2026 09:23
4.2.1 (2026.11.11) can crash right after unlock with "Index out of range"
in ChainRegistry.webSocketDidChangeState, on the socket state queue (device
crash report from TestFlight, iPhone 16 Pro Max, iOS 27.2.1). Since 4.2.0
the socket engine reports state changes on its own queue, so each new
chain socket's first change arrives while the registry is still adding the
chain list. The delegate read that list without the registry's lock and
trapped when the update replaced it mid-read. 4.0.4 reported the change on
the updating thread itself, which hid the missing lock.

- ChainRegistry: look the chain up under the lock, on the registry's own
  serial queue, so the shared socket queue never waits for a chain update.
  Create the lock with the registry (a lazy var can be created twice), read
  the list under it in performHotBoot and the package adapter, and guard
  the chain-types map, which the types sync publishes from its own queue.
- NetworkIssuesCenter, ChainsIssuesCenter: keep listeners and issues under
  a lock and call listeners after releasing it. Screens add listeners while
  the event queue publishes the reconnecting events above.
- EthereumConnectionPool, AccountInfoUpdatingService: create their locks
  with the object instead of lazily.

Tests: a deterministic test parks a chain update and shows the state change
waits for it; stress tests race state changes against updates, the types
map and both issue centres; a real-socket test drives ConnectionFactory,
FailoverChainConnection and WebSocketEngine into the registry; plus a
hot-boot test. On 4.2.1 five of them fail (three crash the host) and Thread
Sanitizer reports the race at ChainRegistry.swift:1066 through
ConnectionPool and FailoverChainConnection, the device crash's frames. With
the fix, 149 related tests pass under Thread Sanitizer with no reports, and
the new tests pass 20 times in a row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit f3cb359)
Master's reviewed list for the gate is still the 424-name list that came
in with #1305. Nine of its names do not exist, and the selected classes
have gained tests since, so the gate cannot pass on master. The launch-crash
fix adds five more ChainRegistryTests.

Recompute the list for master with the fix. The selected classes hold the
same tests here as on the 4.2.2 line, so the list is that line's 483
identities and the gate requires exactly 483. A source scan that reproduces
the 4.2.1 (478) and 4.2.2 (483) lists exactly gives the same 483 here.

The three files now match the 4.2.2 line (20204c7), which also brings two
gate self-test fixes from that line. From 8df1106: the fake xcrun no
longer races under pipefail. From 6063ef1: the self-test fails unless it
reaches its end. The self-test passes: 6 positive contracts and 50 negative
or adversarial cases.

Tooling only; nothing here is compiled into the app.

(cherry picked from commit 20204c7)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
testSocketStateChangesRacingChainUpdatesAreEachPublishedFromACompleteList
treated the last round's node on the last chain as its sign that every
round had been applied. StreamableProvider hands each batch to the
registry on the global user-initiated queue, which is concurrent, so the
rounds can reach the registry's write lock in any order. When an earlier
round landed last, the node never matched, and the test waited out its
120 s and failed.

In the Release -O Core Data gate on master it failed the first run, then
2 of 10 repeats. A temporary probe showed the last round was not the last
batch applied in 6 of 40 runs.

Wait instead for one ChainsSetupCompleted per batch (the registry posts
one each time it applies a batch). Then check that each chain ends on a
node from one of the rounds. What the test checks is unchanged: no socket
state change is dropped while chains are replaced.

Test only; nothing here is compiled into the app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@takemiyamakoto
takemiyamakoto requested review from a team as code owners November 13, 2026 11:42
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-13T11:44:27.170718Z 89503c7 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

takemiyamakoto and others added 2 commits November 13, 2026 20:05
This reverts commit 89503c7. The next
commit, 2cd1011 from #1326, fixes the cause in the registry instead:
chain updates are delivered on its serial queue, so they apply in provider
order and the test's wait for the last round's node holds again. Taking
that commit as it is keeps ChainRegistry.swift and ChainRegistryTests.swift
the same on master and in #1326.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant