fix(nym): make large syncs over the mixnet reliable - #1219
Open
rachyandco wants to merge 66 commits into
Open
Conversation
Replace the global CURRENT_MEMO buffer with a Memo value passed as a process_memo(memo) argument. Fixes the cross-thread race that made the rhai plugin tests flaky under parallel test execution.
The OA1 parser trimmed pair and value whitespace before decoding backslash-escaped spaces, so a value ending in '\ ' lost the escaped space and kept a stray trailing backslash (spec: spaces are trimmed unless escaped). Decode escapes before trimming and preserve spaces encoded by a trailing '\ ' run, matching the openalias-rs grammar.
Settings form edits no longer persist on every keystroke; the save chain now runs once in SettingsPageState.didPop (matching the sub-page pattern) via AppSettingsNotifier.save(). VotingConfigNotifier.build() returns only the cached config so reading the provider never fetches; resolve() takes an optional source so the settings button uses the typed URL.
VotingConfigNotifier.build() returns only the cached config (no network), so reading the provider never triggers a fetch and resolve() is the single fetch path. _resolve logs the real error, serves cache only when present, and rethrows on total failure; voting polls init/refresh show the error via showException.
Ballot intents FK-reference the voting round, which only exists after delegation prepare, so writing intents at proposal-selection time failed with a FOREIGN KEY error. Move intent writes to the submission job before the cast loop (mirroring vizor), sourced from the persisted drafts; the proposal page now persists drafts only. Also parse proposal option ids from 'index' (vote-sdk format, omitted for the first option) so Support/Oppose radios no longer collide on id 0, and surface persistence errors via showException.
The fork's tree-sync HTTP path was the only blocking transport (Runtime::block_on inside an existing tokio runtime panicked during votingSyncTree). Bump to 54e6c72 which converts the transport chain to async and drops the blocking runtime.
Add a bundled-sapling-params cargo feature that embeds the Sapling proving parameters in the binary (via zcash_proofs/bundled-prover). When enabled, get_sapling_prover returns LocalTxProver::bundled() and the download APIs become no-ops; when disabled, behavior is unchanged. Enabled in CI for desktop (mac/linux/windows) and zkool_graphql server builds; mobile builds are unaffected.
A fresh round (Join -> ballot -> Confirm & submit) was a no-op: the submission job is driven by the fork's resume_plan, which only emits steps from existing DB state, so a never-joined round had no steps and finished instantly with 'All steps already confirmed' while recording nothing — the round stayed joinable after restart. Mirror vizor's sequencing: - pass draft proposal ids into votingPlan so needs_draft_setup (the fresh-round trigger) actually computes - _runDelegation: treat needs_draft_setup as delegation work for the first un-confirmed bundle (prepare/prove/broadcast/confirm), and fill missing PIR URL/layout from the resolved voting config - _runVotes: write ballot intents from drafts before reading the plan, reload the session, defer votes for bundles whose delegation is still pending, and sanitize drafts for the fork DraftVote deserializer (vc_tree_position/single_share, dropped skipped choices) - _runDelegation/_runVotes/_submitShares report whether they did work; the done label is honest: 'Delegation confirmed' / 'Votes submitted' / 'Shares submitted' / 'All steps already confirmed' Also: the confirmation poll now requires a positive block height (proof of inclusion, not just HTTP 200), and the status screen shows the tx hash + block height as verifiable evidence.
The fresh voting path (Join -> ballot -> Confirm & submit) never passes a lightwalletd URL, so delegation_prepare got '' and the fork's gather_delegation_lwd_inputs failed with 'invalid lightwalletd URL'. Fall back to the app-configured settings.lwd (same source the sync path uses) when the status-page param is empty; the resume path is untouched (its URL comes from the config saved by the first prepare).
unspent_ironwood_notes selected every unspent Ironwood note without a height bound, so a note created after the round snapshot was passed to the prepare path; rewinding its witness to the snapshot anchor edge failed with 'note position is after anchor edge position'. Apply the same a.height <= snapshot_height filter as eligible_voting_weight, so the prepared bundle matches the eligibility shown on the ballot page.
Delegation keys embed an app-owned voting hotkey, but nothing in the app ever created one, so delegation prepare failed with 'no voting hotkey; create one first' (voting_hotkey_load). Mirror vizor's _ensureHotkey: before preparing, create the hotkey when missing and the round is not yet hotkey-bound — a bound round without the stored key keeps failing instead of silently generating a mismatched key.
The fresh voting flow could show 'Delegation confirmed' without an actual on-chain confirmation, and stale local setup state made the build step permanently refuse to proceed. Fixes: - delegation_build_submission and voting_commit_with_progress: the FRB stream binding drops the returned future (unawaited), so Rust errors surfaced as uncatchable 'Unhandled Exception'. Deliver errors through the sink (decoded as AnyhowException) instead of the dropped future. - _buildDelegation: run the build with empty pczt_bytes (the fork's software path — the build re-runs setup internally with fresh PCZT randomness, so a separate delegationSetup call always produced a conflicting sighash). Retry once after resetting unsigned setup state when the stored sighash is stale from a previous run. - _runDelegation: after delegationConfirm, read back the bundle phase from the session and require 'confirmed' with a tx hash before returning true — the done label cannot be claimed on un-backed state. - voting_status: the done label no longer falls back to 'Delegation confirmed' (that claimed success for stale done states); it renders the honest doneLabel. - new FRB voting_reset_session_state (fork reset_voting_session_state) for the stale-setup recovery. - Cargo.toml: patch zcash_voting to the local clone carrying the SQL alias fixes (voting_votes referenced as 'votes' in clear_stale_share_delegations and record_vote_submission).
PirClientBlocking owns a tokio runtime and block_ons on every call, which
panics inside the FRB async runtime ('cannot start a runtime from within
a runtime'). Connect via the async connect_pir and let the fork's prove
path (now generic over PirProofSource) fetch IMT proofs without a
blocking detour.
- new votingRoundTitle provider fetches the chain round-status title (the only friendly-name source; config and local DB carry none) - polls page: Join tiles and round tiles show the title instead of the bare hex round id - ballot page: the round-name lookup now checks 'title' first, so the friendly name flows through review -> status -> confirmation - status page: shows the round name above the stage label - confirmation page: 'Your vote for <title> has been submitted.' - review page: fetches the chain options and lists the chosen option LABEL (e.g. 'Smooth issuance curve') instead of 'Option N', falling back to indices when the fetch fails - error text on the status page is selectable for copy/paste
The hyper client was created on the FRB runtime and its connections were pooled there; the prove thread's own runtime then reused them, but the FRB runtime is parked on the thread join, so the pooled connections' I/O could never progress and PIR requests stalled until the 60s transport timeout. Connect inside the thread so all PIR traffic lives on the thread's runtime (matches vizor's shape). A probe against the stage server confirmed the cross-runtime pattern was the wedge: single- and thread-runtime PIR both complete in ~6s. Also patch pir-client from a local copy adding the missing public circuit_root getter on the async client (needed by the fork's PirProofSource impl).
…ts 0.10.0
The vote chain reports tx confirmation heights as JSON strings
("7163319"), so parseVoteChainTxConfirmation rejected every confirmed
tx and the confirmation poll timed out even though the delegation was
accepted on-chain (leaf_index recorded). Accept numeric strings.
Also point the voting-circuits patch at the local 0.10.0 clone with the
ZSA-orchard shim (matching the chain's verifier), replacing the stale
0.9.0-rc.3 git rev that made the chain reject every delegation proof.
Add rust/examples/vk_probe.rs to compare circuit/proof fingerprints
against the fork's standalone build.
The vote circuit derives each cast-vote's VAN from a proposal-authority
mask that clears one bit per submission (load_zkp2_inputs drops proposals
whose vote has a recorded tx_hash). The port committed ALL drafts in one
batch, so every proposal used the bundle's VAN and proposals after the
first were rejected by the chain ('nullifier already spent').
Restructure _runVotes to mirror vizor's per-draft build loop: cast one
proposal, submit + confirm it, then cast the next — each subsequent
commitment sees the previous submission and derives the chained VAN.
Extract the submit/confirm sequence into _submitVote, reused by the
cast path and the resume submit_vote path.
The voting flow never passes shareServerUrls, so _submitShares planned against zero servers and the round stayed on 'Resume' forever with pending submit_shares steps. The vote chain servers double as helper servers — mirror vizor's context.config.voteServers: when no explicit share server list is provided, use the resolved config's vote server URLs.
A run that performs no work can still have pending plan steps — the helper shares are scheduled near the vote window, so 'All steps already confirmed' was a misnomer while submit_shares steps remained. Derive the label from the plan: pending share steps -> 'Waiting for the share window', other pending steps -> 'Waiting for the next step', empty plan -> 'All steps already confirmed'.
…reens The evidence block surfaced only the delegation tx; the confirmed votes were verifiable in the DB and on the chain but invisible in the UI. Show each confirmed vote's tx hash and vote-tree position (from the session recovery) on the status screen's done state and the confirmation page, selectable for copy/paste.
Replace the machine-local path patches with the pushed branches so the build is reproducible anywhere: - zcash_voting: dependency rev 54e6c72 -> aa5338f1 (feat/sqlx-storage: SQL alias fixes, voting-circuits 0.10.0 pin, PirProofSource, witness pruning, prove stacks); the patch section is removed — a patch keyed by its own git URL must point at a different source, so the rev lives in the dependency spec itself - voting-circuits: git rev 4403369 (feat/zsa-orchard-0.10: 0.10.0 release source + ZSA-orchard note API shim) - pir-client: git rev b704640 (feat/async-circuit-root-getter on hhanh00/vote-nullifier-pir: public circuit_root accessor for the async client)
The done and confirmation screens listed 'Proposal N: <tx> · tree N'. Add a votingRoundProposals provider (chain round status: id, title, option id -> label) and render each confirmed vote as '<title> — <option label>' above its tx hash and tree position. The confirmation page now receives chainUrl to fetch the proposals.
The ballot evidence fell back to 'Option ${choice + 1}', which showed
choice 1 (Oppose, option id 1) as 'Option 2' — off by one against the
vote-sdk's 0-based option ids. Fall back to the option id itself; the
proposal label ('Oppose') still renders once the round proposals fetch
resolves.
- voting_share_payloads now excludes shares already recorded in voting_share_delegations, so a resume no longer re-submits the same shares (it was duplicating submissions to the helpers). - _submitShares arms the background tracker even when all shares are already recorded, so confirmations still get polled after a restart. - _trackShares refreshes the voting session after each tick, so the round tile flips from 'Resume' to 'View results' once every share confirms instead of showing stale status. - the remaining label distinguishes pending share submissions from pending share confirmations.
…ess closed rounds Round tiles now derive their affordance from the chain round status plus the resume plan: tallying/closed rounds show "View results" (a tally exists), active rounds with pending recovery (incl. unconfirmed helper shares) show "Resume" to the status page, active rounds with the wallet done show "Review" to the vote receipt, and the "Open rounds" section only offers Join for rounds the chain reports as active. The results page renders a closed round with no recorded votes as a zero-filled ballot instead of an empty screen.
…are wallets (hhanh00#1208) * fix(ledger): skip Sapling FVK fetch for transparent-only transactions sign_transaction unconditionally fetched the Sapling full viewing key from sapling_accounts via fetch_one. Transparent-only accounts (e.g. BIP-44 Ledger accounts) have no sapling_accounts row, so this returned RowNotFound and aborted signing before the Ledger device was ever contacted, surfacing as an opaque "no rows returned" error. The FVK and derived OVK are only used inside the Sapling spend/output loops, which only iterate when the PCZT has Sapling components. Fetch the key lazily (only when stin > 0 || stout > 0) as an Option, and unwrap by reference at each use site. SpendValidatingKey is not Copy, so the proof-generation block uses ak.clone(). * fix(ledger): skip Sapling anchor fetch for transparent-only transactions After proving, sign_transaction unconditionally called pczt.sapling().anchor().expect("a Sapling bundle with spends must have an anchor"). A transparent-only transaction has no Sapling bundle, so anchor() returns None and the expect panicked: thread 'tokio-rt-worker' panicked at rust/src/ledger/builder.rs:494: a Sapling bundle with spends must have an anchor The anchor is only read inside the Sapling spends serialization loop, which never iterates for a transparent-only PCZT. Fetch it lazily (only when stin > 0) as an Option and unwrap by reference inside the loop. * fix(ledger): force v5 tx for hardware signing on NU6.3 NU6.3 (Ironwood) is active, so the Builder produces v6 transactions. Hardware wallets using the Zondax "Zcash Shielded" Ledger app can only sign v5 (ZIP-244) transactions; the app predates NU6.3 and computes a pre-NU6 sighash for a v6 tx, so the PCZT signer rejects the device signature with TransparentSign(InvalidExternalSignature). Force the Builder to emit a v5 tx for hardware accounts (hw != 0) via propose_version(TxVersion::V5), while keeping consensus_branch_id as BranchId::for_height (Nu6_3 on the current network). V5 is valid in Nu6_3 per TxVersion::valid_in_branch, and a v5 tx carrying the current Nu6_3 branch id is consensus-valid, so the tx is both signable by the device and acceptable to the network. Software accounts are unaffected and still build v6. This is expected to remain viable for the foreseeable future: the only planned transaction-version phase-out is ZIP 2003 (Draft, proposed for NU7), which disallows v4 (Sprout) transactions and explicitly keeps v5 valid. NU7 is not yet scheduled (activation height TBD), and no ZIP proposes removing v5. The workaround depends on the Zondax Ledger app remaining installed (it is unmaintained but functional); the proper long-term fix is migrating the Ledger signer to the LedgerHQ PCZT-v2 app, which supports v6/Ironwood. --------- Co-authored-by: hhanh00 <hanh425@gmail.com>
…imalDigits flutter_localizations in Flutter 3.47 requires intl ^0.20.3. fixed <6.2.0 pins intl <=0.20.2, so pub resolution failed entirely. Bump fixed to ^6.2.0 (first version supporting intl 0.20.3) and update the four call sites that used the renamed scale: parameter (decimalDigits: has identical semantics).
… voting page
Every voting read call acquired get_connection() for the wallet-id lookup and
then let VotingDb acquire again internally for migrations and queries —
2 connections held per call, so the 4 concurrent per-tile session loads
oversubscribed the 5-connection pool and self-stalled: holders blocked on
their 2nd acquire wait out the 30s acquire timeout, leaving the round tiles
spinning for minutes.
zcash_voting (rev faa06af4) now threads a caller-provided &mut SqliteConnection
through the read path (rounds, ballot_intents, resume_plan, round_snapshot and
all their helpers, including the previously-missed delegation_statuses,
recovered_*_work_from_steps, vote_has_recovery_bundle, get_commitment_bundle,
get_unconfirmed_delegations, share::{list,unconfirmed}). zkool's wrappers
acquire once and pass it down — one pool connection per FRB call (verified
with a standalone probe: full 4-round session load in ~13ms vs 30s timeouts).
The page already fetches the round title from the chain round status for the submission flow; show it in the AppBar (falling back to the round id) instead of always displaying the raw round id hash.
… free during delegation - voting_sessions: new FRB that loads every round's plan/recovery/intents in one Rust call holding a single connection (per-round loads needed one connection per round and stalled the 5-slot pool with many rounds). The round list reads sessions from the shared batch provider. - delegation prepare/build/setup/sign: scope the wallet connection around the short DB phases instead of holding it across the lightwalletd fetch, PCZT build, and PIR proving (a later internal acquire queued behind the held connection and timed out at 30s). - get_connection logs slow acquires with pool size/idle counts. - the round list invalidates when a submission job finishes, so a round leaves the Join list and shows its real status without a manual refresh.
Co-authored-by: macintoshhelper <6757532+macintoshhelper@users.noreply.github.com>
Done on the vote receipt used go('/voting'), which reset the stack and
left the polls page as the root with no way back to the account page.
Chain proposal -> review -> status via pushReplacement and pop the
confirmation page instead, so the polls page keeps its back arrow.
…corrupt stored addresses
…ailures Design review found six gaps against resumability: a server crash lost an accepted tx from the in-memory mempool with no re-broadcast path; a client crash between the broadcast response and persisting the tx hash could dead-end on a duplicate-nullifier 422; no failover across configured vote servers; transient blips killed the run with manual-only retry; share tracking was session-scoped and helper failures failed the whole job; the status page trapped the user during long polls. - votechain client: every completed HTTP response is an answer (status + body + Retry-After pass through); only transport failures are Err - VoteChainFailover service: rotates across configured servers, honors 503 Retry-After, accepts 502-with-hash envelopes, remembers the last-working server per round - bounded auto-retry with 30/60/120s backoff in the submission job; the plan-driven body is idempotent, so re-running is always safe - poll-with-rebroadcast fallback: a recorded hash that never confirms is re-broadcast with byte-identical persisted wire (the vote wire is now persisted like the delegation wire), keeping the recorded hash valid - duplicate-nullifier 422s reconcile confirmations from the commitment tree (cast-vote appends the VAN output immediately before the vote commitment, so one scan recovers both positions) - share tracking extracted into a session-independent provider re-armed on wallet open and voting-page open; helper outages no longer fail the run - voting pages: retrying stage, leave-anytime PopScope, tree-verified receipts without a tx hash - pin rustc 1.95.0 via rust-toolchain.toml (1.88 cannot compile the pinned libcrux-psq) - fork rev bumped to zkool-recovery (commitment-tree recovery APIs + standalone build repair)
Large syncs looped forever on "stalled (no reply for 120s)": a single GetBlockRange stream for the whole range starves the nym-rpc session of reply SURBs, and nym-sdk's MessageBuffer flushes messages that waited over 6s even when earlier ids are missing, corrupting the h2 stream (GOAWAY FRAME_SIZE_ERROR) under mixnet retransmission delays. Over the mixnet, split the shielded sync into 1000-block chunks that each run a complete pass and commit, so a dropped session only costs the chunk in flight; clearnet/Tor keep the single-stream behavior. Replace the decay-based buffer with one that writes strictly consecutive message ids, drops duplicate retransmissions, and honors the server's Close message. A gap left unfilled for 120s trips the stall watchdog instead of hanging the session forever.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Large syncs looped forever on "stalled (no reply for 120s)": a single GetBlockRange stream for the whole range starves the nym-rpc session of reply SURBs, and nym-sdk's MessageBuffer flushes messages that waited over 6s even when earlier ids are missing, corrupting the h2 stream (GOAWAY FRAME_SIZE_ERROR) under mixnet retransmission delays.
Over the mixnet, split the shielded sync into 1000-block chunks that each run a complete pass and commit, so a dropped session only costs the chunk in flight; clearnet/Tor keep the single-stream behavior. Replace the decay-based buffer with one that writes strictly consecutive message ids, drops duplicate retransmissions, and honors the server's Close message. A gap left unfilled for 120s trips the stall watchdog instead of hanging the session forever.