Skip to content

Enable Tor on mobile - #473

Open
piatoss3612 wants to merge 10 commits into
mainfrom
rowan/mobile-tor
Open

Enable Tor on mobile#473
piatoss3612 wants to merge 10 commits into
mainfrom
rowan/mobile-tor

Conversation

@piatoss3612

@piatoss3612 piatoss3612 commented Aug 6, 2026

Copy link
Copy Markdown
Member

Note

Draft. Based on #472 — review the four commits after rowan/tor-followup-fixes; merge #472 first, then retarget this to main.

Runs on the iOS simulator: the app builds with arti linked, launches, and Tor bootstraps from the app's own support directory. No device run yet, and the simulator cannot settle the filesystem-permission question either way — see "What is not verified".

Problem

Arti already ships inside every iOS and Android binary. zcash_client_backend's tor feature and Rust's network_privacy module are both unconditional, and a merged commit already patched the iOS podspec to link liblzma for it. Mobile pays the dependency and binary-size cost today and gets nothing: the only Dart entry point is gated on kAppFormFactor == AppFormFactor.desktop.

Every transport mobile uses is already Tor-aware — lightwalletd gRPC (sync, broadcast, migration preparation), the shared HTTP client (price, swap, Sapling params, wallet link), and even the iOS background paths, which reach lightwalletd through Rust rather than URLSession. What was missing was the entry point and a place to turn it on.

Issue #455's specific ask — a unique Tor circuit per Ironwood migration transaction — is implemented in shared Rust, so mobile inherits it for every foreground broadcast. The iOS background outbox reached lightwalletd through a C entry point that opened the shared route instead; that entry point now isolates too, so the property holds on both paths.

Solution

Apply the route on mobile, and give mobile settings its own control. The desktop control is not reused: six of its eight copy branches are about desktop software updates, which the app stores own here, and its toggle geometry belongs to a settings page rather than a grouped card. The state machine behind both is the same provider.

Two things the desktop route never needed, both recovered from the Tor integration this repo shipped and removed in April 2026 (b1cb18bb6):

  • Arti checks that its data directory is not readable by other users. On mobile the app sandbox is the boundary and those checks reject the directory outright — which would fail the bootstrap from inside fail-closed mode, blocking every request while Tor never becomes ready. Mobile disables them the way both ECC mobile SDKs do; desktop keeps them.
  • An idle client keeps guard connections and directory tasks alive, so it now sleeps with the app — but only where the process actually stops working. Desktop keeps polling with its windows hidden, so firing on every hide made it pay a circuit rebuild for no saving.

Background work no longer ignores the route. Rust's route policy is process-local and starts Direct, and only Dart sets it. iOS kills the app often, so a background task can launch a cold process where Dart never ran: route_decision returns Direct and the background lightwalletd calls go over clearnet even though the user chose Tor. The confirmation tracker makes this the normal path — it deliberately holds its queries while the app is active and releases them once it is not.

A background pass may now use Tor on external power over an unmetered link, and defers to the foreground otherwise; each signal fails closed while it is still unknown. A cold background process has no Dart, so it brings Tor up itself under a deadline sized for a background window rather than the foreground's, and an expiry keeps the route Tor-desired and not ready rather than falling back to the clear. The native layer declares the persisted route before its first network call, so a path that slips the gate is refused rather than sent in the clear.

Guard state stays off device backups. Arti's directory records which guards this wallet chose, and on iOS it lands in Application Support, which iCloud backs up. A restored install should pick fresh guards; that costs one bootstrap. Android needs nothing here — allowBackup="false" already covers it.

Why the constraint is power and metering

Measured with a standalone probe against arti 0.35.0 using the feature set zcash_client_backend enables:

Cold bootstrap 8.45 MB down / 744 KB up / 23.7 s
Warm bootstrap 0.64 s, zero bytes
On-disk cache 46.3 MB
Idle traffic, awake ~500 B/s
Idle traffic, dormant ~27 B/s

The recurring cost is channel padding, not CPU — and it runs even with zero wallet traffic, because the forced onion-service-client feature keeps a 3+1 circuit pool alive. Dormancy removes it; it does not reduce CPU, because GuardMgr's periodic loop is dormancy-unaware.

So the expensive case is a cold bootstrap, not Tor itself: a warm client is free to reach and its steady-state cost is small. Gating background work on external power and an unmetered link is what those numbers support — it keeps the 8.45 MB case off cellular and off battery without giving up background progress entirely.

Validation

  • fvm flutter test — 30 failures, byte-identical to the same run on origin/main (pre-existing Ironwood and figma_compare). No new failures.
  • fvm flutter test --tags mobile --run-skipped --dart-define=VIZOR_FORM_FACTOR=mobile — 17 failures, likewise identical to the origin/main baseline.
  • cargo test --lib — 606 passed.
  • fvm flutter analyze — no errors; the 23 pre-existing Ironwood warnings are unchanged.
  • Rust cross-compiles with these changes: cargo check --target aarch64-apple-ios and --target aarch64-linux-android (NDK 28.2), both clean.
  • Swift type-checked against the real iOS SDK at arm64-apple-ios15.0 and arm64-apple-ios26.0.
  • Each behavioural change was checked by reverting it and confirming its regression test fails.

On a booted iPhone 17 Pro Max simulator:

  • fvm flutter build ios --simulator succeeds with arti linked into the app target, and the app launches and renders onboarding — mobile startup is not blocked by applying the route.
  • integration_test/mobile_tor_bootstrap_probe_test.dart reaches NetworkPrivacyStatus.ready against the app's own support directory, which structurally requires the arti bootstrap to have succeeded.

What is not verified

  • The filesystem-permission exemption is unproven. The probe was also run with the exemption compiled out, and it still passed — a simulator container is an ordinary host directory, so arti's checks do not reject it there. The change is kept because both ECC mobile SDKs and this repo's own removed integration do the same, and because disabling a check the sandbox already enforces fails safe. It needs a device to confirm, and a device is also the only place a regression here would show up.
  • No device run, and no Android emulator run.
  • The background policy is verified by reading and type-checking, not against real BGContinuedProcessingTask / BGProcessingTask lifecycles.
  • The iCloud backup exclusion is unverified end to end; it needs a device and a backup inspection.
  • Android is argued from code and cross-compilation: this branch has no Android native network code (three Kotlin files, no WorkManager, no android_jni.rs), so the iOS background work has no Android counterpart to need.

Follow-ups worth raising upstream

librustzcash does not surface arti's PaddingLevel::Reduced, which arti documents as being for mobile, and its tor feature forces onion-service-client on, which is what creates the always-on circuit pool that makes padding run at all.

https://claude.ai/code/session_01R9nkJCPTCahMjGiDgqFNrV

@piatoss3612 piatoss3612 self-assigned this Aug 6, 2026
@piatoss3612
piatoss3612 changed the base branch from rowan/tor-followup-fixes to main August 6, 2026 04:56
Arti already shipped inside every mobile binary — the tor feature and
the network_privacy module are both unconditional — but nothing had
taught it to run there.

Its filesystem permission checks are written for a shared desktop
filesystem. On a mobile app sandbox they add nothing the sandbox does
not already enforce, and a rejection would surface from inside
fail-closed mode, blocking every request while Tor never becomes ready.

An idle client keeps guard connections and directory tasks alive, so it
needs to sleep with the app. The intent is held process-wide and applied
when a client is installed, because the request usually arrives while
the first bootstrap is still running.

A background process has no Dart, so it gets a C entry point that
declares the route and one that brings Tor up under a deadline sized
for a background window. Both fail closed: the route stays Tor-desired
and not ready rather than falling back to the clear.

Claude-Session: https://claude.ai/code/session_01R9nkJCPTCahMjGiDgqFNrV
Every transport mobile uses was already Tor-aware — lightwalletd gRPC,
the shared HTTP client, and the iOS background paths that reach
lightwalletd through Rust rather than URLSession. Only the entry point
was gated to desktop, so mobile paid arti's cost and got nothing.

Tor sleeps when this process really stops working. Desktop keeps polling
with its windows hidden, and waking costs a circuit build, so firing on
every hide would make it pay that repeatedly for nothing. Waking is
asked only by the process that asked for the sleep.

Claude-Session: https://claude.ai/code/session_01R9nkJCPTCahMjGiDgqFNrV
The desktop control is not reusable here: most of its copy is about
desktop software updates, which the app stores own on mobile, and its
toggle geometry belongs to a settings page rather than a grouped card.
The state machine behind both is the same provider.

The two failure directions read differently, because a failed disable
leaves Tor connected and carrying traffic, and the recovery action names
the route it will actually retry.

Claude-Session: https://claude.ai/code/session_01R9nkJCPTCahMjGiDgqFNrV
Arti's directory records which guards this wallet chose. Carrying it to
a second device would carry that choice along; a restored install should
pick fresh ones, at the cost of one bootstrap.

Rust creates the directory before it bootstraps and arti writes guard
state into it immediately, so the exclusion has to run whether or not
the bootstrap succeeds — the failure case is exactly the one that would
otherwise be left exposed. Failing to mark it never fails the route, but
it is no longer silent.

On Android `allowBackup="false"` stops cloud backup and, from target SDK
31, says nothing about device transfer, which needs its own rules.

Claude-Session: https://claude.ai/code/session_01R9nkJCPTCahMjGiDgqFNrV
Rust's route policy is process-local and starts Direct, and only Dart
sets it. iOS kills the app often, so a background task can launch a cold
process where Dart never ran and the background lightwalletd calls go
over clearnet even though the user chose Tor. The confirmation tracker
makes this the normal path — it holds its queries while the app is
active and releases them once it is not.

What that traffic costs decides the rule. A warm client is reached in
0.64 s and zero bytes, and its idle padding is ~500 B/s awake against
~27 B/s dormant; a cold bootstrap is 8.45 MB and 23.7 s. Those are fine
on a charger over an unmetered link and are not fine on battery or
cellular, so a background pass proceeds there and defers otherwise. Each
signal fails closed while it is still unknown, and the route is declared
before the first network call so a path that slips the gate is refused
rather than sent in the clear.

A run that is merely mid-wave is no longer parked with the runs that
genuinely need the foreground, which used to leave background tracking
parked after the user turned Tor back off. The copy follows the rule,
and a device whose OS has no background lane at all still reads
differently from a route that only constrains one.

Claude-Session: https://claude.ai/code/session_01R9nkJCPTCahMjGiDgqFNrV
A failed Tor bootstrap surfaces from inside fail-closed mode, so a host
test cannot reach it. This drives the real bootstrap against the app's
own support directory on a booted simulator or device, without going
through onboarding first.

Claude-Session: https://claude.ai/code/session_01R9nkJCPTCahMjGiDgqFNrV
@piatoss3612
piatoss3612 marked this pull request as ready for review August 6, 2026 07:00

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a2b1be1fdf

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread rust/src/ffi.rs
Comment thread ios/Runner/BackgroundMigrationManager.swift Outdated
Each C entry point built its own current-thread runtime and dropped it
on return. That is fine for a request that completes inside the call,
but a background pass brings Tor up and leaves the client in a
process-wide slot: arti's directory, guard and circuit tasks run on
whatever runtime created it, so the client outlived its own machinery.
The caller saw a successful bring-up and every later call reused a
client that could not carry traffic.

One runtime for the lifetime of the process, multi-threaded because
separate native threads block on it.

The outbox wake no longer bakes the route into its request either. The
route is a setting the user can change after the request is submitted,
and a wake that turned direct would keep waiting for a charger it no
longer needs, holding signed work past the height it was scheduled for.

Claude-Session: https://claude.ai/code/session_01R9nkJCPTCahMjGiDgqFNrV

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b06cacc393

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread ios/Runner/BackgroundMigrationManager.swift
Comment thread ios/Runner/BackgroundMigrationManager.swift Outdated
Comment thread ios/Runner/BackgroundMigrationPreparationManager.swift Outdated
…lined wakes

Three things the background lanes lost by being allowed onto Tor at all.

A migration broadcast from the outbox went out on the shared route,
while every foreground broadcast takes its own circuit. One
transaction per circuit is the property the isolated path exists for,
and a background send that shares with wallet queries hands a single
exit the linkage it buys. The send entry point now isolates; the two
query entry points deliberately do not, because chain queries are
linkable to each other whatever circuit carries them.

A wake that declined the route completed without arming a replacement,
and starting it had already consumed the pending request. A device that
reached a charger an hour later got no execution opportunity at all, so
signed work waited for the user to open the app, possibly past the
height it was scheduled for. The wake now arms its replacement first,
and stops only when there is no work left — battery, metering and a
failed bring-up all change on their own, so none of them is evidence
about the next wake.

Losing power or gaining a metered link mid-wave ended confirmation
tracking the way a failure would. It is an interruption, not an
outcome: it now pauses and re-arms, like an expiry.

Claude-Session: https://claude.ai/code/session_01R9nkJCPTCahMjGiDgqFNrV

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5e66ae1757

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// than on the notification-gate callback. A client that does not come up
// ready ends the wake the same way an unaffordable one does: fail-closed,
// no network work, the run left to the foreground.
guard BackgroundNetworkRoute.allowsBackgroundNetworkWork() else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Honor expiration while bootstrapping Tor

When a cold Tor bootstrap overlaps task expiration, expire() cannot interrupt it because activeCancellation is not created until after this blocking guard returns. A successful bootstrap then creates a fresh, uncancelled token and starts tip queries and broadcasts after the task has expired; if iOS suspends the process first, the consumed wake is neither completed nor re-armed, potentially leaving signed outbox items past their scheduled height. Make Tor bring-up cancellable or check the expired state and re-arm before entering runOutbox.

AGENTS.md reference: AGENTS.md:L539-L541

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid. Fixed in 8bb5330, though only partly — worth being precise about which part.

activeCancellation is now published before the bring-up, so expiry, foreground handoff and wallet mutation all have something to cancel during it. But the bring-up is a synchronous FFI call with no cancel handle, so cancelling does not interrupt it; it makes the outcome observable. The second half is what closes the hole: after the gate returns, the wake re-reads its state and, if it was reclaimed, re-arms and completes instead of entering runOutbox. So an expired wake no longer starts tip queries or broadcasts, and no longer disappears without leaving a replacement.

Making the bring-up itself interruptible needs a cancellation token threaded through the Rust entry point; that is a larger change than this branch should carry, and the bound above holds without it.

Comment on lines +1786 to +1788
BGTaskScheduler.shared.cancel(
taskRequestWithIdentifier: Self.taskIdentifier
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Re-arm launch-time Tor deferrals

When a Tor-enabled continued-processing task is delivered while the device is on battery or a metered link, or when initial Tor bootstrap fails, both launch guards end in this function. Cancelling the identifier and completing without submitting a replacement consumes the only tracking request, so later connecting to power and an unmetered network cannot resume confirmation tracking; the wave instead waits for the user to reopen Vizor despite the surrounding copy promising automatic resumption. Re-arm before completing, as the mid-wave routeDeferred path does.

AGENTS.md reference: AGENTS.md:L523-L530

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid. Fixed in 8bb5330 by routing both launch-time stand-downs through the same re-arm decision the mid-wave path already uses, rather than adding a second mechanism. The re-arming branch no longer cancels the identifier, and it keeps the mid-wave submit-then-complete ordering and 10s latch.

One honest limit: startAuthorized refuses to submit while the route is unaffordable — deliberately, so a user-visible activity does not spin declining every query. So the re-arm is effectively a no-op for the unaffordable gate, and takes effect for the bring-up-failure gate and for the race between them. Guaranteeing "plugs in an hour later and tracking resumes" would need the silent processing task to re-arm confirmation tracking, which the already-shipped mid-wave path does not do either, so I did not widen it here.

Both files' task exits were enumerated while fixing this, and the remaining ones each have an owner (mutation-quiesce resume, foreground handoff, revoked notifications, nothing left to do, watchdog + notification).

…n launch

Starting a background wake consumes its pending request, so every path
that ends one has to either finish the work or leave a replacement. Two
did neither.

The outbox wake installed its expiration handler and then blocked for
tens of seconds bringing Tor up, but the token expiry cancels was only
created after that. An expiry during bring-up had nothing to cancel, and
a bring-up that then succeeded started tip queries and broadcasts on an
already-expired task; if iOS suspended the process first, the wake was
neither completed nor re-armed and signed items waited past the height
they were scheduled for. The token is now published before the bring-up,
and a wake that finds itself reclaimed re-arms instead of starting
network work. The bring-up itself is a synchronous call with no cancel
handle, so this bounds the damage rather than removing it.

Confirmation tracking already re-armed when it stood down mid-wave, but
the two launch-time stand-downs — an unaffordable device, and a bring-up
that failed — cancelled the identifier and completed, consuming the only
tracking request while the screen promised the wave would resume on its
own. They now take the same re-arm decision as the mid-wave path.

Claude-Session: https://claude.ai/code/session_01R9nkJCPTCahMjGiDgqFNrV

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8bb5330fac

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1272 to +1275
guard BackgroundNetworkRoute.allowsBackgroundNetworkPass() else {
BGTaskScheduler.shared.cancel(
taskRequestWithIdentifier: Self.taskIdentifier
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Let deferred tracking submit its replacement

When a Tor tracking task is interrupted because the charger is removed or the network becomes metered, both finishTorDeferredTask and finishConfirmationTrackingTask call start() to re-arm it, but this guard immediately tests the same still-unaffordable conditions, cancels the request, and returns false. Fresh evidence after the earlier re-arm fix is that the replacement now reaches this scheduling-time gate, so no future task exists to notice when charging or networking becomes eligible again; the wave remains paused until Vizor is reopened. Allow the replacement request to be submitted and enforce affordability when that task actually launches.

AGENTS.md reference: AGENTS.md:L589-L593

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid — this is the limit I flagged in the previous round, and it should not have been left there. Fixed in 890d645, but not the way the comment suggests, because the SDK rules that out.

BGTaskRequest.h (iPhoneOS26.2) on BGContinuedProcessingTaskRequest: "earliestBeginDate will be outright ignored by the scheduler in favor of NSDate.now", and the type is documented as "a request to begin a workload immediately, or shortly after submission". So a continued-processing request cannot hold a wait. Submitting it while the device is ineligible produces start → stand down → resubmit with no floor, and every turn is a user-visible activity — measured at 719 in an hour against 0 today.

So the wait moved to a request type that can hold one. An unaffordable route now schedules the silent BGProcessingTask, which does honour a start floor, and that wake re-arms tracking once it has established the device can carry the route. Same hour on an ineligible device: 5 wakes, 0 visible activities, and something is still watching at the end.

Fixing it at the scheduling gate rather than at the caller covers every caller — including the mid-wave stand-down, which reached the same dead end and which I had believed was already fixed.

Arm-now keeps its gate: with no usable delay, dropping it would show an activity that vanishes the instant a user starts a migration, and the only argument for dropping it was the gap the handoff now closes.

Device-dependent and stated as such: BGProcessingTask delivery is best-effort within ~2 days, so the 15-minute watchdog and the foreground remain the backstops; and whether a background-submitted continued-processing request is accepted at all (the header describes the initialiser as acting on behalf of the foregrounded app) is now on the list to check on a device.

Confirmation tracking stood down when the device could not afford the
Tor route, and re-armed by asking to be scheduled again — but scheduling
began with the same affordability check, so the replacement was never
submitted and nothing was left to notice the device becoming eligible.
The wave waited for the app to be reopened, which is what the re-arm
existed to avoid.

Submitting anyway is not the answer: the SDK documents that a
continued-processing request ignores earliestBeginDate in favour of now,
so a device that stays ineligible would start, stand down and resubmit
in a tight loop, and each turn of it is an activity the user can see.

The wait moves to a request type that can hold one. An unaffordable
route now schedules the silent processing wake instead, and that wake
arms tracking again once it has established the device can carry the
route. Fixing it at the scheduling gate covers every caller, including
the mid-wave stand-down that reached the same dead end.

Arming from the foreground keeps its check: without a usable delay,
dropping it would show an activity that vanishes the moment a migration
starts, and the case for dropping it was the gap the handoff now fills.

Claude-Session: https://claude.ai/code/session_01R9nkJCPTCahMjGiDgqFNrV

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 890d6451f3

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +773 to +775
guard BackgroundNetworkRoute.allowsBackgroundNetworkPass() else {
finishTorDeferredWake(task)
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reset the wake state before deferring the Tor route

When the app has previously entered the foreground, handoffToForeground() leaves foregroundHandoffRequested set even if no wake is active. If a later Tor-enabled wake arrives on battery or a metered link, this guard exits before prepareForBackgroundWake() clears that flag; finishWakeWithoutNetworkWork() consequently sees .finishForegroundOnly and does not submit a replacement, consuming the request and potentially leaving signed outbox items past their scheduled height. Fresh evidence after the earlier re-arm fix is this ordering: the new route gate still precedes the only reset of the foreground-handoff state.

AGENTS.md reference: AGENTS.md:L539-L541

Useful? React with 👍 / 👎.

Comment on lines +197 to +200
torStateLock.lock()
let alreadyUp = torIsUpForBackgroundWork
torStateLock.unlock()
if alreadyUp { return true }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Revalidate cached Tor readiness against the Rust client

After one successful background Tor pass sets torIsUpForBackgroundWork, a foreground switch to direct drops the Rust TorClient but never clears this Swift flag. If the user subsequently enables Tor and that foreground bootstrap fails after persisting the preference, an affordable background wake returns true here without retrying the FFI bootstrap; every lightwalletd call then fails closed because Rust has no client, so confirmation tracking and outbox broadcasts cannot recover in the background until a process restart or foreground retry.

AGENTS.md reference: AGENTS.md:L539-L541

Useful? React with 👍 / 👎.

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