Skip to content

Pipelined sync as the standard driver + Ironwood-first wallet UX + setup-reliability fixes - #7

Merged
USCMig merged 17 commits into
mainfrom
feat/sync-optimizations
Aug 16, 2026
Merged

Pipelined sync as the standard driver + Ironwood-first wallet UX + setup-reliability fixes#7
USCMig merged 17 commits into
mainfrom
feat/sync-optimizations

Conversation

@USCMig

@USCMig USCMig commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Summary

Makes the pipelined sync driver the standard (and only) sync path, hardens Zcash
wallet setup so it can't silently hang, adds Ironwood/NU6.3 robustness (including
syncing against pre-Ironwood testnet servers), refreshes the wallet UX to be
Ironwood-first and less noisy, and bumps the Ironwood crate cohort to the latest
release candidates.

Net diff: ~14 files, mostly src-tauri/core/src/wallet.rs and the wallet/groups
screens. Backend + tsc build clean; 50 wallet-feature tests pass; clippy
unchanged at its 7-warning baseline.

What's in this PR

Sync

  • Pipelined driver is now the standard, only sync path. It overlaps block
    download with CPU trial-decryption and streams blocks straight from the network
    to the scanner in memory.
  • Removed the opt-in machinery: the "Experimental pipelined sync" setting +
    Wallet-Settings toggle + command, and SyncOptions.pipelined.
  • Removed the now-dead stock path — the zcash_client_backend::sync::run
    call and the on-disk FsBlockDb/FsCache block cache it required (~110 lines).
  • Serialized syncs behind the app-wide gate so a restart can't race a cancelled
    sync's still-open db connection ("database is locked").
  • Download/scan timing instrumentation for diagnosing slow syncs.

Ironwood / NU6.3

  • Graceful pre-Ironwood tolerance: a lightwalletd that predates Ironwood
    rejects the Ironwood subtree-roots request; the driver now skips just that pool
    and keeps syncing Sapling/Orchard instead of aborting. Lets testnet sync work
    against pre-Ironwood servers (there are no Ironwood notes to miss on them).
  • Friendly diagnostic when a server genuinely can't serve Ironwood, pointing at
    the fix (switch to an Ironwood-capable server).
  • Bumped the RC cohort: zcash_client_backend rc.6→rc.7, zcash_client_sqlite
    rc.6→rc.8, pulling zcash_protocol 0.10.4, pczt 0.9.3, orchard 0.15.5, and
    zcash_pool_migration rc.7.

Wallet setup reliability

  • Instrumented init_group_account with per-step logs and bounded its two
    unary RPCs with a timeout
    , so "Setting up the view-only wallet…" can no longer
    spin forever with nothing in the logs.
  • Surface a failed wallet-status read instead of masking it as a permanent
    "Setting up…" spinner (init only runs after status returns), with a Retry.

UX

  • Ironwood-first balances: the wallet total is strictly the Ironwood amount;
    the always-on Orchard/Ironwood pool split is gone. Any legacy Orchard balance is
    surfaced only when it actually exists, with a one-tap "Move to Ironwood" sweep —
    so no funds are stranded.
  • Dropped user-facing "Orchard" wording (labels, copy) in favor of Ironwood /
    neutral terms; internal identifiers that name the real key type are unchanged.
  • Calmer mainnet UX: removed the passive "you are on mainnet" danger banners
    and the switch-to-mainnet modal; the transaction page header now shows a small
    Mainnet/Testnet pill. Kept one slimmed send-time confirmation before
    broadcasting real ZEC, plus the address/network-mismatch and branch-mismatch
    validators (those are functional, not passive nags).
  • Trimmed the wordiest explanations a level down for a more intuitive feel.
  • DKG wizard: section titles are now bold so they read as headings, not blending
    into the helper text.

Docs

  • Updated docs/SYNC_OPTIMIZATION.md (pipelined is now standard) and
    docs/SYNC_PIPELINE_UAT.md (toggle/stock-baseline sections marked historical).
  • Scoped ValarGroup Shielded Vote in TODO.md as the real voting target.

Behavior changes worth a close look

  • No in-app stock-sync fallback anymore. If a future crate bump breaks the
    pipelined driver, the stock sync::run reference is only in git history.
  • The headline total excludes legacy Orchard (surfaced separately when
    present). Identical for new Ironwood-only groups.
  • Still on release-candidate Zcash crates (no stable 0.24.0/0.22.0 yet).
  • The pre-Ironwood graceful skip means Ironwood value isn't tracked on a server
    that can't serve it — but such a server has none to track.

Testing

  • Backend builds clean; tsc + vite build clean.
  • 50 wallet-feature tests pass (cargo test -p frost-app-core --lib --features wallet).
  • clippy unchanged at the 7-warning baseline.
  • See docs/SYNC_PIPELINE_UAT.md for the testnet acceptance checklist.

Not in this PR / follow-ups

  • Move the RC pins to stable 0.24.0/0.22.0 once published.
  • A second verbosity pass on the remaining flows (NGINX/tunnel explainers, DKG steps).
  • The full ValarGroup Shielded Vote rebuild (scoped in TODO.md).

🤖 Generated with Claude Code

USCMig and others added 17 commits August 2, 2026 22:18
…er hook

Establishes the next major update (sync latency) on its own branch.

- docs/SYNC_OPTIMIZATION.md: the concrete roadmap — why we stay on the ECC
  (Ironwood-capable) stack rather than swap to Warp/ZKool; what we already have
  (tip birthday, subtree roots, spend-before-sync, configurable batch); the three
  remaining levers (pipelining, adaptive batch, parallel decryption); the custom
  pipelined-driver approach; and the safety gate + testnet validation before it
  becomes default.
- SyncOptions { batch_size, pipelined } replaces the bare batch_size arg to
  sync_group; command layer builds it from settings.
- Settings.experimental_pipelined_sync (off by default) reserves the opt-in.
  Until the pipelined driver lands and is validated against the stock driver on
  testnet, the flag safely falls back to zcash_client_backend::sync::run — no
  half-built sync loop ever touches fund detection.

Next on this branch: implement the pipelined driver (prefetch download while
scanning) behind the flag, validate against the stock driver, then flip default.
34 core tests + full backend build + tsc green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire the custom pipelined sync driver that the previous commit left as a
tracing-only fallback. `run_pipelined`/`running_pipelined` in wallet.rs
faithfully reproduce the upstream `zcash_client_backend::sync::run` control
flow (subtree roots -> chain tip -> verify pass -> historic ranges, with
reorg/continuity rewind and higher-priority-range restart), changing only how
batches are fed: a producer task downloads each batch's compact blocks into
memory plus the chain-state anchor and hands them over a bounded channel
(capacity 2), so download runs up to two batches ahead of the CPU-bound
scan. Same wallet state as the stock driver; only network/CPU overlap is new.

Design choices that keep validation a clean equality check:
- In-memory `MemBlockSource` per batch instead of the on-disk `FsCache`, so
  the pipelined path never contends the cache mutex or writes files, and a
  reorg rewinds only the db (nothing cached to truncate).
- Fixed batch units identical to the stock driver (adaptive batch sizing is
  deferred to a follow-up); `split_scan_range` is unit-tested against the
  upstream step-7 splitter semantics.
- Transparent-UTXO refresh omitted, matching the stock driver: our
  zcash_client_backend build does not enable `transparent-inputs` (group
  accounts are Orchard-only view keys), so neither driver performs it.

Still gated by `Settings.experimental_pipelined_sync` (default false) and
raced against the sync cancellation token; scanning is transactional per
batch, so cancellation leaves the db consistent at a batch boundary. Stays
off by default until testnet-validated against the stock driver per
docs/SYNC_OPTIMIZATION.md.

Uses tonic's native `stream.message()` (no futures-util dep) and lets the
subtree-root types infer from the `put_*_subtree_roots` calls (no sapling
dep). 47 core tests pass; full backend builds; no new clippy warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A concrete testnet acceptance checklist for experimental_pipelined_sync:
stock-driver baseline vs pipelined clean-state equality (balance, notes,
history, height), incremental sync, cancellation/resume, reorg tolerance,
send-after-sync, and the flag-off regression. Sign-off maps to the validation
gate in docs/SYNC_OPTIMIZATION.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the memo-v1 "automated poll source" follow-up with the actual
governance target: a full rebuild around ValarGroup Shielded Vote (live,
ZK-based, on-chain vote chain). Records the user's confirmations — infra is
live, FROST-compatible via governance PCZT into ZKP1, Ironwood-supported
snapshots, pin production zcash_voting/pir-client at build time — and notes
that voting.rs (memo v1) is expected to be scrapped. Includes the wallet-side
flow and a de-risking scoping spike as the first step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A lightwalletd that predates Ironwood (NU6.3) rejects the subtree-roots
request for the Ironwood pool with "invalid shielded protocol value", which
aborts the whole sync at its first step. The raw gRPC error gave no hint that
the cause is a server-capability gap, not a wallet bug.

`annotate_sync_error` now post-processes both sync drivers' results: when the
failure matches that signature it returns an actionable message ("This
lightwalletd server doesn't support Ironwood (NU6.3)… switch to an
Ironwood-capable server in the wallet's network settings"), keeping the raw
server text after an em-dash for debugging. The Groups sync box renders it via
SyncErrorView, showing the actionable headline in bold with the raw detail
dimmed beneath — the friendly message on top of the existing log output.

Unit tests cover the flagged case and pass-through of unrelated errors and
cancellation. Backend builds, tsc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
wallet_sync cancelled the previous sync's token and immediately opened a new
db connection, but cancellation is cooperative: the old sync keeps its
connection (and, mid-batch, the SQLite write lock) until it returns at the
next batch boundary. The new sync's first writes — put_*_subtree_roots — then
raced the old writer and, on a large mainnet batch that held the lock past the
30s busy timeout, failed with "database is locked". The pipelined driver
surfaces it more because it keeps the old sync writing more continuously.

Add a per-group async lock (AppState.sync_locks) held across the whole
sync_group call. A restarting sync cancels the old token, then *waits* on this
lock until the cancelled sync fully exits and drops its connection before
opening its own — so at most one writer touches a group's db at a time.
Different groups still sync in parallel. Status reads already use a read-only
(shared-lock) connection, so they don't contend. Driver-agnostic; helps the
stock path too.

55 core tests pass; backend builds; no new clippy warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Log per-batch download time (debug) and per-batch scan time + cumulative
blocks/s (info) in the pipelined driver, so the download-vs-scan split is
visible when diagnosing slow syncs. No behavior change. Confirms whether a slow
sync is network-bound (pipelining helps) or CPU-bound in trial decryption +
note-commitment tree updates (needs parallel decryption, which no published
crate in the current Ironwood cohort provides).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Nothing was subscribing to the app's `tracing` output, so diagnostics
(including the new sync timing logs) went nowhere. Add a global subscriber that
formats every event to stdout AND an in-memory ring buffer (logbuf.rs,
bounded at 3000 lines, honouring RUST_LOG; default info + our crates at debug).
The buffer is process-only and never written to disk — wallet logs can contain
addresses/amounts.

New commands get_logs / clear_logs expose the buffer. Wallet settings gains a
"Diagnostics log" card: live auto-refresh (2s), copy-all, refresh, clear, and a
scroll-pinned monospace view — so a user can grab and share logs (e.g. the
`pipelined scan: … blocks/s` lines) without a terminal.

logbuf unit tests cover ring-buffer capping/order and line splitting. Backend
builds, tsc clean, no new clippy warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Brings the branch up to current main (active-wallet PR #4, log-viewer PR #5)
so it can open a clean PR. Two conflicts resolved:

- state.rs / wallet.rs: main added a global `sync_gate` (one wallet syncs at a
  time) via the active-wallet model; this branch had added a per-group
  `sync_locks` map to fix the "database is locked" race. The global gate is
  strictly stronger — only one sync ever runs — so it subsumes the per-group
  lock. Kept `sync_gate`, dropped `sync_locks` (and its now-unused Arc import).
  The db-lock fix is preserved: holding the gate across the whole sync_group
  call means a restarting sync can't race a cancelled one's open db connection.
- Kept this branch's `SyncOptions { batch_size, pipelined }` and the
  experimental pipelined-sync path (the point of the branch); only the locking
  mechanism changed.

The log-viewer commit (cherry-picked to main as PR #5) merges as identical
content — no duplicate get_logs/clear_logs. Backend + tsc build clean; clippy
unchanged at the 7-warning baseline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The experimental pipelined sync driver was only togglable by hand-editing
settings.json. Add a "Sync" card in Wallet Settings with an "Experimental
pipelined sync" checkbox:

- New set_experimental_pipelined_sync command (persists the flag; read at the
  start of each wallet_sync, so it takes effect on the next sync — no restart).
- Registered in lib.rs; Settings interface + setExperimentalPipelinedSync IPC
  binding added.
- SyncCard in Wallet.tsx reads the flag from settings and toggles it, with a
  note that it's off by default and applies on the next sync.
- SYNC_PIPELINE_UAT.md: point the toggle steps at the checkbox (settings.json
  still works) and add A0a to test the checkbox wiring itself.

Backend + tsc build clean; clippy unchanged at the 7-warning baseline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"Setting up the group's view-only wallet…" could spin forever with nothing in
the log viewer: init_group_account emitted no tracing output, and its two unary
RPCs (get_latest_block, get_tree_state) had no timeout — a server that accepts
the TCP connection but never answers (misconfigured proxy, stalled tree-state
for a deep birthday) left setup hung with no error and no clue.

- Add tracing::info at each step (connecting → chain tip → fetching tree state →
  account imported), so the in-app log viewer shows setup progress. Routes
  through the frost_app_core=debug filter already used by the log buffer.
- Bound get_latest_block and get_tree_state with a 30s timeout so a hang surfaces
  as a "did not respond" connection error (→ the UI's "Couldn't set up the
  wallet — check the lightwalletd endpoint … Retry" path) instead of an infinite
  spinner. These are single unary calls, not the long block stream, so a timeout
  is safe here (the stream deliberately has none).

Note: a FROST group wallet is view-only by design — the app holds the group's
UFVK, so it receives, shows balance, and builds unsigned txs; spending is via a
FROST signing ceremony, not a local spend key.

Backend builds clean; clippy unchanged at the 7-warning baseline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"Setting up the group's view-only wallet…" could sit forever with nothing in the
logs even after the init_group_account timeout/logging fix — because init only
runs *after* the wallet-status query returns. If that status read errors
(keystore locked, key derivation for the group, or a db problem), status.data
stays undefined, the init effect never fires (its guard requires status.data),
and the UI's catch-all `!s` branch shows the setup spinner indefinitely with no
error and no log line.

- Handle status.isError: show the actual error with a Retry (refetch) button.
  The status read is local (no network), so the message notes it's the keystore
  or db, not lightwalletd.
- Distinguish "Loading wallet…" (status still loading) from "Setting up…"
  (init actually running).
- Show the real init error text in the "Couldn't set up the wallet" branch (it
  previously hid it behind generic endpoint advice), so the new get_tree_state /
  get_latest_block timeout messages are visible.

tsc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On testnet the configured lightwalletd often predates Ironwood (NU6.3) and
rejects the Ironwood subtree-roots request with "invalid shielded protocol
value", which aborted the entire sync at the first step — even though a server
that doesn't know the pool has no Ironwood notes to report.

The pipelined driver now treats *that specific* rejection on the Ironwood pool
as "no Ironwood roots yet": it logs a warning and continues syncing Sapling and
Orchard, instead of failing. Any other error still fails the sync, and the
Sapling/Orchard root fetches are unchanged. A server that does support Ironwood
returns the roots normally.

- Extract is_invalid_shielded_protocol() and reuse it in annotate_sync_error
  (the friendly message for the stock driver, which can't skip) and in the new
  graceful-skip branch.
- Unit test for the detector; existing annotate tests still pass (50 wallet-
  feature tests green).

Note: the graceful skip only applies with experimental pipelined sync ON — the
stock zcash_client_backend::sync::run requests Ironwood roots itself and can't be
patched, so it still needs an Ironwood-capable server (and shows the friendly
message). On such a testnet server there are no Ironwood funds to miss.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Ironwood (NU6.3) support still lives in the zcash_client_backend 0.24 /
zcash_client_sqlite 0.22 release-candidate line (no stable 0.24.0/0.22.0 yet),
so move the two `=` pins to the newest RCs:

- zcash_client_backend =0.24.0-rc.6 -> =0.24.0-rc.7
- zcash_client_sqlite  =0.22.0-rc.6 -> =0.22.0-rc.8

Cargo resolved the rest of the cohort via existing caret ranges:
- zcash_protocol 0.10.3 -> 0.10.4 (required by sqlite rc.8)
- pczt 0.9.1 -> 0.9.3
- orchard 0.15.4 -> 0.15.5
- zcash_pool_migration 0.1.0-rc.5 -> 0.1.0-rc.7 (Orchard->Ironwood turnstile)

No source changes needed — the app + core compile against rc.7/rc.8 unchanged;
50 wallet-feature tests pass; clippy unchanged at the 7-warning baseline.

Still an RC line: swap these for the stable 0.24.0/0.22.0 (and loosen the `=`
pins) once they publish. Re-run Part A of the sync UAT after this bump — a
backend/tree dependency change is exactly what that equality test guards.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New DKG groups are Ironwood-only (Orchard is exit-only), so present the wallet
as an Ironwood wallet and stop nagging about mainnet.

1. Orchard -> Ironwood in the UI:
   - Balance summary now shows the Ironwood spendable/pending/total (the total
     is strictly the Ironwood amount), dropping the always-on Orchard/Ironwood
     pool-split line.
   - Any legacy Orchard balance is surfaced only when it actually exists, via a
     "Move to Ironwood" prompt (auto-hidden for new groups) — so no funds get
     stranded.
   - Reworded the visible labels/copy: "Group unified address", "Unified
     address", "shielded pool" (was "shielded Orchard pool"), "RedPallas"
     (dropped "(Orchard)"), DkgWizard "shielded spend authority". Internal
     identifiers (isOrchard, groupOrchardKeys, the orchard balance field) are
     unchanged — they name the real key type, not user-facing text.

2. Less verbosity: trimmed the wallet intro, transport-security note, welcome
   callout, receive-address blurb, viewing-key callout, and the unshield /
   migrate explanations down a level.

3. Calmer mainnet UX: removed the passive "you are on mainnet" danger banners
   (Wallet page, send form) and the switch-to-mainnet confirm modal. The
   transaction page header now carries a small Mainnet/Testnet pill near the
   top. Kept: one slimmed send-time confirmation before broadcasting real ZEC,
   the address/network mismatch validators, and the branch-mismatch warning
   (those are functional, not passive nags).

tsc + vite build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In the DKG wizard the section titles (Role, Group name, Ciphersuite, Threshold,
Server, …) are `<label>`s styled the same dim color/weight as the descriptive
text beneath them, so the two blur together. Scope a rule to the wizard form
(`.dkg-form`) that renders those titles in the bright text color at weight 600,
so each selection reads as a heading. Excludes the participant checkbox rows
(`.multiselect-option`), which are options, not section titles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The pipelined driver is validated and now the default and only sync path, so
remove the opt-in machinery:

- Removed the "Experimental pipelined sync" checkbox (Wallet Settings), the
  set_experimental_pipelined_sync command + its registration, the
  experimental_pipelined_sync setting, and the IPC bindings.
- SyncOptions loses its `pipelined` field; sync_group always runs run_pipelined.
- Deleted the now-dead stock path: the zcash_client_backend::sync::run call and
  the on-disk FsBlockDb/FsCache block cache it required (~110 lines) plus their
  now-unused imports. The pipelined driver streams blocks straight from the
  network to the scanner in memory, so no disk cache is needed.
- The Ironwood pre-NU6.3 graceful-skip (which only ever lived in the pipelined
  path) is now always in effect — testnet syncs against pre-Ironwood servers
  without any flag.
- Docs: SYNC_OPTIMIZATION.md status updated; SYNC_PIPELINE_UAT.md's on/off
  toggle and stock-baseline sections marked historical (no in-app comparison
  is possible now that the stock driver is gone).

Backend + tsc build clean; 50 wallet-feature tests pass; clippy unchanged at 7.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@USCMig
USCMig merged commit c464021 into main Aug 16, 2026
1 check passed
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