Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
b6d2641
feat(sync): sync-optimization branch foundation — design + gated driv…
USCMig Aug 2, 2026
62bbbee
feat(sync): implement pipelined sync driver behind experimental flag
USCMig Aug 3, 2026
4de574c
docs(sync): add UAT checklist for the pipelined sync driver
USCMig Aug 3, 2026
c2ab490
docs(todo): scope ValarGroup Shielded Vote as the real voting target
USCMig Aug 4, 2026
fcc8b25
feat(sync): friendly diagnostic for non-Ironwood lightwalletd servers
USCMig Aug 5, 2026
3d20756
fix(sync): serialize per-group syncs to prevent "database is locked"
USCMig Aug 5, 2026
4875ce2
chore(sync): instrument pipelined driver with download/scan timing
USCMig Aug 5, 2026
494e258
feat(diagnostics): in-app log viewer in Wallet settings
USCMig Aug 5, 2026
b8a09a6
Merge origin/main into feat/sync-optimizations
USCMig Aug 15, 2026
16a829d
feat(sync): toggle pipelined sync from Wallet Settings
USCMig Aug 16, 2026
adde233
fix(wallet): log + time-bound the view-only account setup
USCMig Aug 16, 2026
db7a8a4
fix(wallet): surface a failed status read instead of a stuck spinner
USCMig Aug 16, 2026
dbb6f8c
fix(sync): tolerate pre-Ironwood lightwalletd in the pipelined driver
USCMig Aug 16, 2026
edea246
chore(deps): bump Ironwood RC cohort to the latest release candidates
USCMig Aug 16, 2026
df680db
ux(wallet): Ironwood-first balances, slimmer copy, calmer mainnet cues
USCMig Aug 16, 2026
e1f5039
ux(dkg): bold section titles so they stand out from helper text
USCMig Aug 16, 2026
f82f157
feat(sync): make pipelined sync the standard driver; remove the toggle
USCMig Aug 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 58 additions & 16 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,22 +88,64 @@ current build depends on them.
still authenticates end-to-end, so the transport only provides
reachability).

## Voting (coinholder polling)

- [ ] **Automated poll source / discovery** — voting currently uses **manual
ballot entry**: the user pastes the poll's published ballot-definition JSON
and its reception address, answers, and casts (a shielded memo via the FROST
send path; weight is set by the poll's off-chain balance snapshot). Replace
the manual paste with a programmatic source once one is available:
- Fetch active polls from a configurable **poll-source URL** (Zodl exposes a
"custom poll sources" config; confirm the feed format), render the ballot
automatically, and show live/closed status + results.
- The definitive protocol design is ValarDragon/Valar's; get the poll-feed +
registration/snapshot spec from there (or the Zodl integration docs) to
match eligibility exactly. Keep manual entry as the fallback/offline path.
- The casting core (`voting.rs`: memo v1 encode/validate/poll-hash,
`prepare_vote`) is source-agnostic and already done — this is only about
*where the ballot comes from* and surfacing results.
## Voting (protocol / coinholder governance)

- [ ] **Migrate to ValarGroup Shielded Vote (full rebuild; supersedes memo v1)** —
the real coinholder-vote protocol we should target is **ValarGroup Shielded
Vote**: https://valargroup.gitbook.io/shielded-vote-docs . It is a live,
cryptographically-private, on-chain voting system on a **dedicated vote
chain**, used infrequently to gauge protocol-upgrade sentiment *before*
committing engineering resources — exactly Cyze's governance use case.

**This is not a change to the current memo format — it is a different
system.** Our shipped `core/src/voting.rs` implements the informal
**zec-coin-polling "Vote Cast Memo v1"** (a JSON memo cast as a shielded send
to a reception address, tallied off-chain from a *transparent*-balance
snapshot). Shielded Vote has no vote memo: a vote is a ZK-proven **Vote
Commitment** (VAN consumed → new VAN + `H(DOMAIN_VC, round_id, shares_hash,
proposal_id, vote_decision)`) plus 16 ElGamal share ciphertexts, submitted to
REST endpoints on the vote chain. **Expect to scrap `voting.rs` and its UI**
(`VoteTab`/`parseBallot` in `src/screens/Groups.tsx`, `wallet_prepare_vote`,
`VoteEntry`/`BallotDefinition`) and rebuild around the SDK below. Keep memo v1
only if a lightweight, no-infra sentiment poll is still wanted; otherwise
remove it so the two are never confused.

**Confirmed (2026-08-04, from the user):**
1. **Infrastructure is live** — vote chain + election authority + PIR fleet
are running; there is a real network to build/test against.
2. **FROST-compatible** — the delegation step (ZKP1) takes an externally
produced re-randomized spend-auth signature via a governance PCZT
(`(rk, sighash, spend_auth_sig)`), which maps onto Cyze's existing FROST
re-randomized Orchard signing ceremony. Confirm the exact PCZT hand-off
when building.
3. **Ironwood supported going forward** — vote weight snapshots the group's
shielded note holdings, and Ironwood is covered, so a post-NU6.3 shielded
treasury can vote (this also fixes memo v1's flaw that only *transparent*
balances counted — a shielded FROST treasury effectively couldn't vote).
4. **Crate versions: pin to whatever is in production at build time.** Both
SDK crates are published and moving fast — snapshot the then-current
production versions rather than an early rc:
- `zcash_voting` — client lib (ZKP1/2/3 via Halo2, ElGamal, governance
PCZT, Merkle witnesses, SQLite round state). Repo:
https://github.com/valargroup/zcash_voting
- `pir-client` — nullifier non-membership PIR queries.
(Swift SDK exists too, but Cyze is Rust — use the crates directly.)

**Rough shape of the wallet-side flow** (see the Integration Guide): discover
+ validate vote config → `GET /shielded-vote/v1/rounds/active` → PIR
nullifier proofs → build+prove **ZKP1 delegation** (governance PCZT, FROST
spend-auth) → `POST /delegate-vote` → sync the vote-commitment tree → per
proposal, build **ZKP2** and `POST /cast-vote` → split into 16 ElGamal shares
and `POST /shares` with staggered anti-censorship `submit_at` timing → read
`GET /tally-results/{round_id}` once the round is `FINALIZED`. Note
`vote_round_id` encoding is context-sensitive (hex in config/URLs/shares,
base64 in delegate/cast bodies).

**Suggested first step — a scoping spike** before any UI: add the production
`zcash_voting` + `pir-client` crates, hit a live round's `/rounds/active`, and
prove the FROST-produced re-randomized spend-auth sig feeds ZKP1 end-to-end.
That de-risks the one genuinely novel part (threshold signing into their
prover) cheaply. Own branch, own PR; larger effort than any current item.

## Wallet (Zcash)

Expand Down
148 changes: 148 additions & 0 deletions docs/SYNC_OPTIMIZATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Sync optimization — design & roadmap

Status: **driver implemented, pending testnet validation** (branch
`feat/sync-optimizations`). The pipelined driver is built and off by default; it
becomes the default only after the validation gate below passes. This is the next
major targeted update. Its goal is to cut the wall-clock latency of wallet
sync — especially the recovery / large-range case — without leaving the
Ironwood-capable `zcash_client_backend` (ECC) stack.

## Why we stay on the ECC stack

We surveyed the open-source Zcash wallets (YWallet/ZKool, Zingo, Cake, ZODL,
Vizor). All but ZODL are light wallets on lightwalletd compact blocks; the
differentiation is entirely in the scan engine.

- **YWallet/ZKool** use **Warp Sync** — the fastest engine — but it is
Sapling/Orchard-oriented, keeps its own DB schema, and **has no Ironwood
support**. Adopting it wholesale would fork us off the maintained NU6.3 stack
for a rewrite: wrong trade for a funds wallet on Ironwood.
- **ZODL** offers a full/hybrid node mode — a privacy feature, not a general
latency win, and a large architectural add.

So we **port Warp's ideas onto our existing stack** rather than switch engines.

## What we already have (main)

- **Tip-height birthday for new wallets** — a new group starts at the chain tip,
so it never scans pre-creation history. (The single biggest first-sync win; done.)
- **Subtree-root tree init** — `zcash_client_backend::sync::run` calls
`update_subtree_roots` (GetSubtreeRoots), so the note-commitment tree is
initialized without replaying all history.
- **Spend-before-sync ordering** — `run` scans `suggest_scan_ranges()` in
priority order (ChainTip/Verify first), so the balance surfaces before a full
catch-up finishes, and the UI polls it every ~5s.
- **Configurable batch size** — `sync_group(batch_size)`, clamped
`[MIN,MAX]_SYNC_BATCH_SIZE`, persisted via `Settings.sync_batch_size`.

## What's missing (this update)

The upstream `sync::run` is explicit that "block batches are not downloaded in
parallel with scanning." Two levers remain, and both require driving the sync
loop ourselves instead of calling `sync::run`:

1. **Pipelining** — overlap network download with CPU trial-decryption. This is
Warp's core advantage and the single biggest safe win left. **Implemented and
now the standard (only) sync driver** (`run_pipelined` in `wallet.rs`). The
stock `sync::run` path and its on-disk block cache were removed once the
pipelined driver was validated.
2. **Adaptive batch size** — grow the batch over empty ranges (cheap to scan),
shrink over dense ranges (expensive), instead of one fixed size for the run.
**Deliberately deferred**: the pipelined driver keeps the *same* fixed batch
units as the stock driver so its output is byte-identical and the validation
gate below is a clean equality check. Adaptive sizing changes the scan units,
so it lands as a separate follow-up once pipelining is validated and default.

A third lever is crate-gated:

3. **Parallel trial decryption** — the pinned `zcash_note_encryption` 0.4.2 does
batch decryption single-threaded. Getting multi-core decryption needs either a
`zcash_note_encryption` with the `multicore` feature or wiring the
`zcash_client_backend` `sync-decryptor` (rayon) pipeline. Deferred to the next
crate-cohort bump; tracked here so it isn't forgotten.

## Approach: a custom pipelined driver, alongside `sync::run`

Pipelining needs control of the loop, so we add a **custom driver**
(`run_pipelined` / `running_pipelined` in `wallet.rs`) that faithfully reproduces
the upstream `run`/`running` control flow (subtree roots → chain tip → verify
pass → historic ranges), changing only how batches are fed:

- A **producer** task downloads each batch's compact blocks **into memory** and
the chain-state anchor, then hands `(ScanRange, Vec<CompactBlock>, ChainState)`
over a **bounded channel** (capacity 2) so download runs up to two batches
ahead. A cloned tonic client shares the underlying HTTP/2 connection, so this
adds no new socket.
- The **consumer** (main task) receives a ready batch, wraps its blocks in an
in-memory `BlockSource` (`MemBlockSource`), and runs `scan_cached_blocks` on it
(CPU-bound). Scanning is transactional per batch via `put_blocks`, so an
interrupted or cancelled batch leaves the db consistent at a batch boundary —
the same guarantee the stock driver gives.
- On a **reorg / continuity error** or a newly-added higher-priority range, the
consumer breaks, the producer is aborted, and the pass restarts from
`suggest_scan_ranges` — exactly the upstream `return Ok(true)` → outer-loop
behavior.

Because each batch is downloaded fresh into memory and never persisted, the
pipelined path **never touches the on-disk `FsCache`** — no file writes, no cache
mutex contended between producer and consumer, and nothing to truncate on a
reorg rewind (only the db is rewound).

**Transparent UTXO refresh is intentionally omitted.** Upstream `running` performs
it only under the `transparent-inputs` feature, which our `zcash_client_backend`
build does not enable (group accounts are Orchard-only view keys). The stock
driver we run today therefore does not perform it either, so omitting it keeps
the two byte-identical.

Correctness-critical logic (reorg rewind, verify ranges, subtree roots,
chain-tip update, batch splitting) is **ported faithfully** from the upstream
`sync.rs` we depend on; only the download/scan overlap is new. The batch splitter
(`split_scan_range`) has a unit test asserting it produces the exact same units as
the upstream step-7 splitter.

### A note on overlap and the runtime

`scan_cached_blocks` is synchronous and CPU-bound; the consumer calls it directly
on the async task. On the multi-threaded Tokio runtime the app uses, the producer
keeps downloading the next batches on other worker threads while the consumer
thread scans — which is where the latency win comes from. On a single-threaded
runtime the code is still correct (no overlap, identical result). Moving the scan
onto `spawn_blocking` to guarantee overlap regardless of runtime is a possible
future refinement; it is not needed for correctness.

### Status: standard driver

The pipelined driver was validated on testnet (byte-identical wallet state vs the
stock driver, faster on high-latency links) and is now the **standard, only** sync
path. `sync_group` always calls `run_pipelined`; the `experimental_pipelined_sync`
setting, the stock `zcash_client_backend::sync::run` call, and the on-disk
`FsBlockDb`/`FsCache` block cache it required have been removed. The pipelined
driver streams blocks straight from the network to the scanner in memory.

### Validation gate before default

Before flipping the default to the pipelined driver:

1. A testnet recovery sync (large range) produces a **byte-identical** wallet
state to the stock driver (same balance, notes, witnesses, scanned height).
2. A reorg is exercised (or simulated) and recovers correctly.
3. A shielded send after a pipelined sync builds, signs, and broadcasts.

## Sequencing

1. **[done] Settings gate + custom pipelined driver** (prefetch download while
scanning), off by default. ← core of the work; `run_pipelined` in `wallet.rs`.
2. **[next] Testnet validation** against the stock driver; flip default if it
passes (see the validation gate above).
3. **[follow-up] Adaptive batch size** — grow/shrink the batch by range density,
once pipelining is the validated default.
4. **[next crate bump] Parallel trial decryption** via note-encryption
`multicore` / the `sync-decryptor` pipeline.
5. **[optional, infra] Zaino indexer** — evaluate a Rust indexer (Zingo's path)
in place of stock lightwalletd for richer per-request data. Composes with the
above; not a wallet rewrite.

## Explicit non-goals

- No wholesale swap to Warp/ZKool's engine (no Ironwood support; own DB).
- No full/hybrid-node mode (privacy feature, not latency; large surface).
93 changes: 93 additions & 0 deletions docs/SYNC_PIPELINE_UAT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# UAT — pipelined sync (now the standard driver)

Acceptance checklist for the pipelined sync driver (`feat/sync-optimizations`).
Run on **testnet** first.

> **Note:** the pipelined driver is now the **standard, only** sync path — there
> is no `experimental_pipelined_sync` toggle anymore, and the stock
> `sync::run` path was removed. Sections **A0a** and **A** (the on/off
> checkbox test and the stock-driver baseline) are therefore **historical** — you
> can no longer switch to the stock driver in-app to compare. If you still want an
> equality baseline, capture it from an older build; otherwise start at **B** and
> validate the single driver's correctness, speed, incremental behavior,
> cancellation, and post-sync send.

See `docs/SYNC_OPTIMIZATION.md` for the design.

## Setup

- [ ] Build the current branch: `npm run tauri build` (or `cargo build` for a dev
backend), and launch the freshly built binary — not a previously installed
bundle.
- [ ] Use a **testnet** group with a known, non-trivial history (funded a few
times, at least one send), so scanning actually finds notes.

## A0a. The toggle itself (checkbox wiring) — HISTORICAL

- [ ] *(No longer applies — the toggle was removed and pipelined is the only path.)*

## A. Baseline with the stock driver (control) — HISTORICAL

- [ ] *(No longer runnable in-app — the stock driver was removed. Kept for
reference; use an older build if you need a stock baseline to compare.)*

## B. Pipelined driver — clean-state correctness (the core test)

- [ ] Delete the wallet db (force a full rescan from birthday) and sync to the tip.
- [ ] **Balance is correct** — total (Ironwood), plus any legacy Orchard, matches
the group's known funds and what block explorers show.
- [ ] Received-note count is correct.
- [ ] Transaction history is complete (txids, amounts, memos).
- [ ] Scanned-to height reaches the chain tip.
- [ ] Wall-clock sync time is reasonable (faster on a high-latency link is the
whole point). If you kept a stock baseline from an older build, it should be
**≤** that.

## C. Incremental sync

- [ ] With the pipelined wallet already at the tip, wait for / cause a new inbound
testnet payment, then "Sync Now".
- [ ] Only the new blocks are scanned (fast), the new note appears, and the balance
increases by the expected amount.
- [ ] Sync a second time with no new activity → completes quickly, balance
unchanged (no double-count, no drift).

## D. Cancellation / resume

- [ ] Start a full rescan (delete db) with the pipelined driver, then hit "Sync
Now" (or switch away) mid-sync to cancel it.
- [ ] App stays responsive; no panic; no error toast beyond an expected
"cancelled".
- [ ] Start sync again → it resumes and completes, ending at the same
balance/height as B (cancellation left the db consistent at a batch
boundary, not corrupted).

## E. Reorg tolerance (best-effort)

- [ ] If a testnet reorg happens to occur during a sync, confirm it recovers: the
log shows a "chain reorg detected … rewinding" line and the sync finishes at
the correct tip with the correct balance. (Hard to force on demand; watch for
it opportunistically during A–D.)

## F. Send after a pipelined sync (funds path)

- [ ] After a pipelined sync, build + FROST-sign + broadcast a small testnet send.
- [ ] Transaction is accepted by the node (no branch-id / MissingSpendAuthSig /
note-selection errors).
- [ ] After it confirms, a re-sync shows the spend and the reduced balance
correctly.

## G. Regression — HISTORICAL

- [ ] *(No longer applies — there is no flag to turn off; the pipelined driver is
the only path.)*

## Sign-off

- [ ] B balances/notes/history/height are correct (against known funds / an
explorer).
- [ ] C, D, F pass on testnet.
- [ ] No panics, no stuck syncs, UI responsive throughout.

Only after this passes on testnet: repeat B/F once on **mainnet** with a small
balance before relying on it broadly.
25 changes: 17 additions & 8 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading