Expose PCZT getters and add governance wallet query methods#2
Closed
greg0x wants to merge 2 commits into
Closed
Conversation
67af478 to
c3abb34
Compare
greg0x
added a commit
that referenced
this pull request
Mar 12, 2026
The AsyncThrowingStream closures used bare `try` inside unstructured Tasks with no do/catch. When Rust rejected the empty placeholder witness, the error was silently swallowed and the continuation was never finished — freezing the UI at step 7/8. Wrap both proof stream bodies (ZKP #1 and #2) in do/catch so errors propagate via continuation.finish(throwing:). Also provide non-empty placeholder witness data so the stub accepts it.
greg0x
added a commit
that referenced
this pull request
Mar 12, 2026
The protocol (§3.3.1) requires exactly 4 encrypted shares per vote — shares_hash = H(enc_share_1..4) and ZKP #2 verifies 4 preimage checks. decompose_weight now produces exactly 4 shares: binary decomposition followed by round-robin bucketing when more than 4 bits are set. Previously it returned one share per set bit (up to 64), which hit the ElGamal guard for any balance with >4 bits set — like the simulator's 101768753 (12 bits). Also adds error logging to the vote submission effect so failures are visible in the console instead of silently swallowed.
greg0x
pushed a commit
that referenced
this pull request
Mar 12, 2026
greg0x
pushed a commit
that referenced
this pull request
Mar 12, 2026
* rename gov_comm to van_cmx, add duplicate nullifier check in DelegateVote - Rename proto field gov_comm (field 6) to van_cmx in MsgDelegateVote - Regenerate protobuf and update all Go references - Add duplicate nullifier check before SetNullifier in DelegateVote handler Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix e2e test: rename gov_comm to van_cmx in delegation payload Match the proto field rename (gov_comm → van_cmx) in the JSON payload sent by the Rust e2e tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * remove cmx_new from vote commitment tree Only van_cmx is appended to the commitment tree during delegation. cmx_new is recorded on-chain but not included in the tree — no subsequent proof references it; only the VAN needs a Merkle path for ZKP #2. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix e2e test: tree next_index >= 1 after delegation (only van_cmx) cmx_new is no longer appended to the tree, so next_index after a single delegation is 1, not 2. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix e2e test: remove extra cmx_new arg from build_van_merkle_witness call The merge brought in a call with 3 args but the function now takes 2 (only van_cmx + checkpoint_height since cmx_new was removed from tree). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix e2e test: expect 1 leaf after delegation (only van_cmx) Since cmx_new was removed from the commitment tree, delegation now appends only van_cmx (1 leaf) instead of 2. Update the tree sync assertion accordingly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix merge: rename GovComm to VanCmx in sighash.go The sighash.go file was added on main and references msg.GovComm, but this branch renamed the proto field to van_cmx (VanCmx in Go). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix e2e test: update all leaf counts for single-leaf delegation Delegation now appends only van_cmx (1 leaf) instead of 2, so: - delegation wait: next_index >= 1 (was >= 2) - cast-vote wait: next_index >= 3 (was >= 4) - post-cast assertion: next_index == 3 (was == 4) - tree sync assertion: contains "3" (was "4") Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
greg0x
pushed a commit
that referenced
this pull request
Mar 12, 2026
…cash#69) * Ballot scaling in ZKP #1: convert zatoshi to ballot count Replace the minimum-weight check (condition 8) with ballot scaling that floor-divides v_total by 12,500,000 to produce num_ballots. Condition 7 now hashes num_ballots into the VAN commitment instead of the raw v_total. Circuit constraints for condition 8: - num_ballots * BALLOT_DIVISOR + remainder == v_total - remainder < 2^24 (via shift-by-2^6 into 30-bit lookup check) - 0 < num_ballots <= 2^30 (via nb_minus_one 30-bit range check) Adds MulChip (c = a * b gate) used for the reconstruction constraint and the remainder bit-shift. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Apply ballot scaling to VAN hashes in ZKP #2 and librustvoting ZKP #1 now hashes num_ballots (not raw zatoshi) into VAN commitments. Update all downstream VAN hash callers to match: - vote_proof/builder.rs: convert total_note_value to num_ballots before VAN integrity hashing and share splitting - governance.rs: construct_van now divides total_weight by BALLOT_DIVISOR - Update test values to use weights >= 12,500,000 (one ballot minimum) - Freeze new known-answer VAN test vector Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
greg0x
pushed a commit
that referenced
this pull request
Mar 12, 2026
Wire real ZKP #3 (share reveal) proofs into the E2E voting flow and fix the Go/Rust El Gamal generator mismatch so auto-tally BSGS decryption succeeds end-to-end. - Add orchard/src/share_reveal/ circuit, builder, and prover (K=14) - Expose share_reveal via sdk/circuits FFI for on-chain verification - Update Go PallasGenerator() to use SpendAuthG (matching ZKP #2 circuit) - Add Go ZKP #3 proof verification in sdk/crypto/zkp - E2E test submits 2 of 4 real share reveals, waits for auto-tally, and asserts exact decrypted total_value = 7,500,000 - Wire helper-server share-reveal processing and nullifier tracking - Regenerate FFI bindings (xcframework + Swift) - Remove dead mock reveal_share_payload and unused encrypt_share
greg0x
added a commit
that referenced
this pull request
Mar 12, 2026
The stub returned hardcoded shares_hash (0xDD), vote_decision (0), and tree_position (0). Now threads real values: shares_hash from the vote commitment (Poseidon hash of encrypted share x-coordinates, already computed by the ZKP #2 builder), vote_decision from the caller, and vc_tree_position (known after cast-vote TX confirms on chain). shares_hash added to VoteProofBundle and VoteCommitmentBundle so it flows from builder → librustvoting → FFI → Swift without recomputation.
greg0x
added a commit
that referenced
this pull request
Mar 12, 2026
The existing voting_flow test calls the orchard builder directly. This new test exercises the production library stack: VotingDb persistence, TreeClient HTTP sync from chain, witness generation, and ZKP #2 proof generation — all through the librustvoting and vote-commitment-tree-client APIs. Changes: - Make derive_spending_key public in librustvoting so tests can derive a SpendingKey from the same hotkey seed used in production - Parametrize build_delegation_bundle_for_test to accept an optional SpendingKey (for seed-derived key consistency between ZKP #1 and #2) - Add librustvoting + vote-commitment-tree-client deps to e2e-tests with the necessary [patch.crates-io] entries
greg0x
added a commit
that referenced
this pull request
Mar 12, 2026
Steps 10-12 of the ZKP 2 wiring plan: update the Swift layer to match the new FFI signatures and replace the mock VAN witness with the real tree sync + witness generation pipeline. VotingModels: - VoteCommitmentBundle now carries encShares, anchorHeight, and sharesHash (returned by ZKP #2 builder). voteRoundId changed from Data to String (hex). Removed voteCommTreeAnchorHeight. - SharePayload: removed redundant shareIndex (lives in encShare) - Added VanWitness model (authPath, position, anchorHeight) VotingCryptoClient interface: - buildVoteCommitment takes hotkeySeed, networkId, vanAuthPath, vanPosition, anchorHeight instead of encShares + vanWitness Data - buildSharePayloads takes voteDecision + vcTreePosition - New methods: storeVanPosition, syncVoteTree, generateVanWitness VotingCryptoClientLiveKey: - buildVoteCommitment calls FFI with new signature, maps encShares from the result bundle - buildSharePayloads passes voteDecision + vcTreePosition to FFI, constructs full FFI VoteCommitmentBundle with all 9 fields - constructDelegationAction/buildGovernancePczt pass addressIndex: 1 - New storeVanPosition/syncVoteTree/generateVanWitness wrappers VotingStore (.confirmVote): - Replaced decompose → encrypt → mock-witness → build flow with syncVoteTree → generateVanWitness → buildVoteCommitment flow - Derives hotkeySeed from wallet storage for ZKP #2 - Passes bundle directly to submitVoteCommitment (no re-wrapping) - Builds share payloads from bundle.encShares - TODOs for chainNodeBaseUrl config and vcTreePosition from TX events
greg0x
added a commit
that referenced
this pull request
Mar 12, 2026
The cast-vote TX was missing r_vpk coordinates, sighash, and vote_auth_sig — the chain rejected submissions without these fields. New signCastVote crypto client method calls FFI to compute the canonical sighash (Blake2b-256 matching Go's ComputeCastVoteSighash), decompress r_vpk to (x, y) affine coordinates, and sign with the randomized voting key rsk_v = ask_v.randomize(alpha_v). VoteCommitmentBundle model now carries rVpkBytes and alphaV from ZKP #2. submitVoteCommitment API call includes all required fields: r_vpk_x, r_vpk_y, r_vpk, sighash, vote_auth_sig.
greg0x
pushed a commit
that referenced
this pull request
Mar 12, 2026
…imeout The test hardcoded VAN at position 0 and VC at position 2, but the commitment tree is global across rounds — if prior leaves exist, the Merkle witness targets the wrong position and the ZKP #2 proof fails on-chain verification. Capture pre_delegate_next_index before delegation and compute all positions relative to it. Also compute the Step 12 TALLYING timeout dynamically from vote_end_time instead of a fixed 250s.
greg0x
added a commit
that referenced
this pull request
Mar 12, 2026
The helper server's background tree sync loop would sync past share positions before they were marked for witness retention, causing "no witness for position X" errors for every share. This happened because shares arrive ~40s after the tree has already been synced (the test spends that time generating ZKP #1 and #2). TreeSync now tracks marked positions separately and provides witness_or_resync() which rebuilds the tree from scratch with all marked positions if the fast path fails. The API handler also marks positions eagerly on share arrival. Moved the helper server default port from 9090 to 9091 to avoid conflicting with the chain's gRPC server. Updated e2e test defaults to use 127.0.0.1 (avoids macOS IPv6 resolution issues) and port 1318 (matches init.sh).
greg0x
added a commit
that referenced
this pull request
Mar 12, 2026
Rewrote voting_flow_librustvoting.rs to exercise the complete path: delegation (ZKP #1) → cast-vote (ZKP #2) → helper server (ZKP #3) → tally accumulation → auto-tally finalization → result verification. Previously the test generated ZKP #3 inline and submitted reveal-share directly to the chain. Now it sends share payloads to the helper server, which handles tree sync, witness generation, ZKP #3 proof, and chain submission — matching the production Zashi flow. Key fixes: use the chain's EA public key (from ~/.zallyd/ea.pk) instead of generating a random keypair, which was preventing auto-tally decryption. Reduced the default vote window from 480s to 180s since the helper server processes shares much faster than inline ZKP #3 generation.
greg0x
added a commit
that referenced
this pull request
Mar 12, 2026
The voting_round_id is a Blake2b-256 hash, and ~75% of 32-byte hash values exceed the Pallas field modulus. zkp2.rs was using strict from_repr() which rejects non-canonical values, while zkp1.rs, governance.rs, and the chain verifier all correctly use wide reduction via from_uniform_bytes(). This caused vote commitment building to fail with "voting_round_id is not a valid Pallas field element" depending on the round's hash. The e2e test had the same latent bug (working by luck when the hash happened to be canonical).
greg0x
added a commit
that referenced
this pull request
Mar 12, 2026
confirmVote previously fired the full on-chain pipeline (tree sync, ZKP #2, cast-vote TX, share delegation) as a background effect while immediately advancing to the next proposal after 600ms. This meant errors were silently swallowed and the user had no indication of whether their vote actually landed. Now the UI stays on the proposal detail screen with isSubmittingVote=true until the entire pipeline completes. On success, advanceAfterVote fires. On failure, voteSubmissionFailed removes the optimistic vote and surfaces the error.
greg0x
pushed a commit
that referenced
this pull request
Mar 12, 2026
- Add sentinel injection (k*2^250 for k=0..16) to pir-export to satisfy circuit gap-width constraint (#3) - Change Tier 2 empty-leaf padding from Fp::zero() to -Fp::one() so trailing entries sort after real leaves, fixing binary search (#2) - Make TierServer::answer_query() return Result with input validation (length checks, alignment) instead of panicking on malformed requests; handlers return HTTP 400 on error (#1) - Replace unwrap/assert with fallible returns in pir-client and Tier0Data::from_bytes (#4)
greg0x
pushed a commit
that referenced
this pull request
Mar 12, 2026
Adds a defense-in-depth constraint to ZKP #2 Condition 6 that prevents proposal_id = 0 (the dummy sentinel value) from passing verification. The range check alone does not exclude zero; this gate closes that gap using the standard inverse-witness technique: q_cond5 * (1 - proposal_id * proposal_id_inv) = 0 This is satisfiable iff proposal_id ≠ 0, since a field inverse exists only for non-zero elements. Implementation: - Config: add proposal_id_inv column (advices[2] on row 0 of the cond6 region, which is otherwise unused on that row) - configure(): add "proposal_id != 0" gate gated by q_cond5 - synthesize(): witness proposal_id⁻¹ on row 0 of the cond6 region - Tests: update three tests that used proposal_id = 0 as a placeholder to use valid non-zero ids; add proposal_id_zero_fails test to explicitly verify the rejection Co-authored-by: Cursor <cursoragent@cursor.com>
greg0x
pushed a commit
that referenced
this pull request
Mar 12, 2026
Replace deterministic shares_hash = Poseidon(c1_0_x, c2_0_x, ..., c1_4_x, c2_4_x) with per-share blinded commitments: share_comm_i = Poseidon(blind_i, c1_i_x, c2_i_x) shares_hash = Poseidon(share_comm_0, ..., share_comm_4) This prevents observers from recomputing shares_hash from on-chain encrypted shares and linking them back to a specific vote commitment, fixing a voter privacy leak. Changes span the full stack: - Circuits (orchard): ZKP #2 and ZKP #3 condition 3/10 use ConstantLength<3> per-share hashes + ConstantLength<5> final hash instead of ConstantLength<10> - Rust lib (librustvoting): share_blinds plumbed through types and builders - C FFI (sdk/circuits): share_blinds_ptr/len params on share reveal function - Go helper (sdk): wire types, API validation, store schema, processor, CGo - UniFFI bridge + iOS: shareBlinds/shareBlindFactors through FFI and Swift
greg0x
pushed a commit
that referenced
this pull request
Mar 12, 2026
…#3 Move the two-level Poseidon shares-hash computation (per-share blinded commitments + outer hash) from vote_proof/circuit.rs into a new shared_primitives::shares_hash module, and replace the inlined version in share_reveal/circuit.rs with calls to the same gadget. Co-authored-by: Cursor <cursoragent@cursor.com>
greg0x
pushed a commit
that referenced
this pull request
Mar 12, 2026
greg0x
pushed a commit
that referenced
this pull request
Mar 12, 2026
Bind the vote commitment to its voting round by including
voting_round_id in the Poseidon hash:
vote_commitment = Poseidon(DOMAIN_VC, voting_round_id,
shares_hash, proposal_id, vote_decision)
This prevents cross-round replay where an attacker reuses a ZKP #3
proof from round A against round B, injecting shares encrypted under
the wrong election key. The change updates both ZKP #2 (condition 12)
and ZKP #3 (condition 2) circuits, out-of-circuit helpers, builders,
FFI, tests, and documentation.
greg0x
pushed a commit
that referenced
this pull request
Mar 12, 2026
Moves DOMAIN_VC, vote_commitment_hash (out-of-circuit), and a new vote_commitment_poseidon gadget into circuit/vote_commitment.rs, mirroring the van_integrity pattern. ZKP #2 (cond 12) and ZKP #3 (cond 2) now both call the shared gadget instead of duplicating the 5-input Poseidon block inline. Resolves the TODO in vote_proof/circuit.rs. Made-with: Cursor
greg0x
pushed a commit
that referenced
this pull request
Mar 12, 2026
The vote proof circuit was regenerating IPA params and proving/verifying keys on every call (~30s on device). Add a process-level OnceLock cache matching the pattern ZKP #1 already uses for the delegation circuit. Made-with: Cursor
greg0x
pushed a commit
that referenced
this pull request
Mar 12, 2026
Cache vote proof (ZKP #2) keygen with OnceLock
External governance protocols (shielded voting) need to: - Replace the Orchard bundle in a PCZT after constructing a custom governance action (Pczt::set_orchard) - Read back the spend_auth_sig after a hardware wallet signs the PCZT, so the signature can be threaded into a ZK delegation proof - Serialize an orchard::pczt::Bundle into the PCZT wire format from outside the crate (Bundle::serialize_from) - Construct an ephemeral SqliteShardStore from a raw connection to build Merkle witnesses without going through WalletDb (SqliteShardStore::from_connection) Remove set_orchard and narrow serialize_from visibility (#3) Clean up PCZT APIs that are no longer needed now that governance PCZT construction uses `Creator::build_from_parts` (see valargroup/zcash_voting#1). - Remove `Pczt::set_orchard()` — was only used by librustvoting to manually inject an orchard bundle after creating an empty PCZT shell. No longer needed since `build_from_parts` accepts the bundle directly. - Narrow `orchard::Bundle::serialize_from` from `pub` to `pub(crate)` — only used internally by the creator role, no external callers.
WalletDb gains two new methods (not on traits — governance-specific): - get_orchard_notes_at_snapshot(account, height): returns all Orchard notes received at or before snapshot_height and unspent as of that height. Backward-looking query for voting snapshots, unlike select_unspent_notes which is forward-looking. - generate_orchard_witnesses_at_frontier(positions, frontier, height): copies wallet shard data to ephemeral in-memory DB, inserts the lightwalletd frontier as a checkpoint, and generates Merkle witnesses. Wallet DB is strictly read-only. These replace the wallet DB access previously embedded in librustvoting, completing the clean separation: librustzcash owns wallet domain, librustvoting owns voting domain, SDK wires them together. Fix rustfmt and clippy warnings in generate_orchard_witnesses_at_frontier Applies rustfmt formatting and replaces redundant closures with direct function references (`.map_err(SqliteClientError::DbError)`).
c2bc669 to
0669f6c
Compare
Author
|
Closing to reopen as a clean PR — the timeline on this one picked up noise from monorepo cross-references. |
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.
Adds the minimal librustzcash surface needed for Zcash shielded voting (governance protocol). Targets
maint/zcash_client_sqlite-0.19.x(orchard 0.11); same changes apply cleanly to main (orchard 0.12).PCZT changes (
pcztcrate)Spend::spend_auth_siggetter — read back the hardware wallet signature after signing, so it can be threaded into a ZK delegation proofSigner::shielded_sighash()getter — expose the cached sighash (already on upstream main, backported here)Wallet changes (
zcash_client_sqlitecrate)Two new governance-specific methods on
WalletDb(inherent, not on wallet traits — these don't belong in the general-purpose API):get_orchard_notes_at_snapshot(account, height)— returns all Orchard notes received at or beforesnapshot_heightand unspent as of that height. Backward-looking query for voting snapshots, unlikeselect_unspent_noteswhich is forward-looking (based on tx expiry).generate_orchard_witnesses_at_frontier(positions, frontier, height)— copies wallet shard data to an ephemeral in-memory database, inserts the lightwalletd frontier as a checkpoint, and generates Merkle witnesses at the snapshot anchor. The wallet DB is strictly read-only.Internal visibility changes
SqliteShardStore::from_connectionwidened topub(needed bygenerate_orchard_witnesses_at_frontiercallers who construct ephemeral stores)orchard::Bundle::serialize_fromnarrowed topub(crate)— only used internally by the creator roleContext
Governance protocols need to construct Orchard-only PCZTs for hardware wallet signing, query wallet notes at a historical snapshot, and generate witnesses anchored at that snapshot's frontier (even if the wallet has synced past it). These changes cleanly separate concerns: librustzcash owns the wallet domain, the voting library owns the voting domain, and the SDK wires them together.