From 7b553817640840a918a313cdd27adc18d7a70923 Mon Sep 17 00:00:00 2001 From: Adam Tucker Date: Fri, 10 Apr 2026 11:46:19 -0600 Subject: [PATCH 1/6] chore: switch imt-tree dev-dep from git to crates.io v0.1.0 Replace the floating `branch = "main"` git dependency on vote-nullifier-pir with the now-published crates.io version. Eliminates the URL-form mismatch that caused imt-tree to be double-compiled in the iOS SDK build (P4/P6). --- voting-circuits/Cargo.lock | 3 ++- voting-circuits/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/voting-circuits/Cargo.lock b/voting-circuits/Cargo.lock index 7493520d..e75367b3 100644 --- a/voting-circuits/Cargo.lock +++ b/voting-circuits/Cargo.lock @@ -508,7 +508,8 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "imt-tree" version = "0.1.0" -source = "git+https://github.com/valargroup/vote-nullifier-pir.git?branch=main#cb71caf89120e47f9ad07c5bd7de9afab4f3668b" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d10acb863735c6c82c0c79e8752696096a86eb4d1e2fe0005ded4fae066297e" dependencies = [ "anyhow", "ff", diff --git a/voting-circuits/Cargo.toml b/voting-circuits/Cargo.toml index e3f217a2..0e3aea4e 100644 --- a/voting-circuits/Cargo.toml +++ b/voting-circuits/Cargo.toml @@ -19,7 +19,7 @@ lazy_static = "1" sinsemilla = "0.1" [dev-dependencies] -imt-tree = { git = "https://github.com/valargroup/vote-nullifier-pir.git", branch = "main" } +imt-tree = "0.1" criterion = "0.4" rand = "0.8" incrementalmerkletree = "0.8.1" From cd27c21dca288cb1b0fff57970f0bb8dd7b8ffda Mon Sep 17 00:00:00 2001 From: Adam Tucker Date: Fri, 10 Apr 2026 11:29:42 -0600 Subject: [PATCH 2/6] refactor: move MulChip and MulInstruction out of orchard fork into voting-circuits These types are voting-circuits helpers with no consumers inside orchard itself. Moving them shrinks the fork diff vs upstream v0.11.0. --- orchard/src/circuit/gadget.rs | 12 ------------ voting-circuits/src/circuit/mod.rs | 1 + .../src/circuit}/mul_chip.rs | 12 +++++++++++- voting-circuits/src/delegation/circuit.rs | 3 +-- 4 files changed, 13 insertions(+), 15 deletions(-) rename {orchard/src/circuit/gadget => voting-circuits/src/circuit}/mul_chip.rs (86%) diff --git a/orchard/src/circuit/gadget.rs b/orchard/src/circuit/gadget.rs index 1fb920c0..c8bf2af0 100644 --- a/orchard/src/circuit/gadget.rs +++ b/orchard/src/circuit/gadget.rs @@ -25,7 +25,6 @@ use halo2_proofs::{ }; pub mod add_chip; -pub mod mul_chip; impl super::Config { pub(super) fn add_chip(&self) -> add_chip::AddChip { @@ -88,17 +87,6 @@ pub trait AddInstruction: Chip { ) -> Result, plonk::Error>; } -/// An instruction set for multiplying two circuit words (field elements). -pub trait MulInstruction: Chip { - /// Constraints `a * b` and returns the product. - fn mul( - &self, - layouter: impl Layouter, - a: &AssignedCell, - b: &AssignedCell, - ) -> Result, plonk::Error>; -} - /// Witnesses the given value in a standalone region. /// /// Usages of this helper are technically superfluous, as the single-cell region is only diff --git a/voting-circuits/src/circuit/mod.rs b/voting-circuits/src/circuit/mod.rs index ef822787..ba7270b8 100644 --- a/voting-circuits/src/circuit/mod.rs +++ b/voting-circuits/src/circuit/mod.rs @@ -2,6 +2,7 @@ pub mod address_ownership; pub mod elgamal; +pub mod mul_chip; pub mod poseidon_merkle; pub mod van_integrity; pub mod vote_commitment; diff --git a/orchard/src/circuit/gadget/mul_chip.rs b/voting-circuits/src/circuit/mul_chip.rs similarity index 86% rename from orchard/src/circuit/gadget/mul_chip.rs rename to voting-circuits/src/circuit/mul_chip.rs index 5be0fc87..1f80dfeb 100644 --- a/orchard/src/circuit/gadget/mul_chip.rs +++ b/voting-circuits/src/circuit/mul_chip.rs @@ -3,9 +3,19 @@ use halo2_proofs::{ plonk::{self, Advice, Column, ConstraintSystem, Constraints, Selector}, poly::Rotation, }; +use ff::Field; use pasta_curves::pallas; -use super::MulInstruction; +/// An instruction set for multiplying two circuit words (field elements). +pub trait MulInstruction: Chip { + /// Constraints `a * b` and returns the product. + fn mul( + &self, + layouter: impl Layouter, + a: &AssignedCell, + b: &AssignedCell, + ) -> Result, plonk::Error>; +} /// Configuration for the multiplication chip. #[derive(Clone, Debug)] diff --git a/voting-circuits/src/delegation/circuit.rs b/voting-circuits/src/delegation/circuit.rs index f5c1d15d..8630a4f7 100644 --- a/voting-circuits/src/delegation/circuit.rs +++ b/voting-circuits/src/delegation/circuit.rs @@ -27,14 +27,13 @@ use halo2_proofs::{ use pasta_curves::{arithmetic::CurveAffine, pallas, vesta}; use crate::circuit::address_ownership::prove_address_ownership; +use crate::circuit::mul_chip::{MulChip, MulConfig, MulInstruction}; use orchard::{ circuit::{ commit_ivk::{CommitIvkChip, CommitIvkConfig}, gadget::{ add_chip::{AddChip, AddConfig}, - mul_chip::{MulChip, MulConfig}, assign_constant, assign_free_advice, derive_nullifier, note_commit, AddInstruction, - MulInstruction, }, note_commit::{NoteCommitChip, NoteCommitConfig}, }, From eaa4e18c9fc6c89bdccef83669c85d0cd4629fe8 Mon Sep 17 00:00:00 2001 From: Adam Tucker Date: Fri, 10 Apr 2026 11:36:59 -0600 Subject: [PATCH 3/6] refactor: move spend_authority gadget out of orchard fork into voting-circuits Revert orchard/src/circuit.rs back to the upstream inline [alpha] SpendAuthG + ak_P block, and move the extracted prove_spend_authority() function into voting-circuits where its only consumers live (delegation and vote-proof circuits). --- orchard/src/circuit.rs | 35 +++++++++++-------- orchard/src/lib.rs | 2 -- orchard/src/shared_primitives/mod.rs | 9 ----- .../src/circuit/address_ownership.rs | 4 +-- voting-circuits/src/circuit/mod.rs | 1 + .../src/circuit}/spend_authority.rs | 6 ++-- voting-circuits/src/delegation/circuit.rs | 6 ++-- voting-circuits/src/vote_proof/circuit.rs | 4 +-- 8 files changed, 31 insertions(+), 36 deletions(-) delete mode 100644 orchard/src/shared_primitives/mod.rs rename {orchard/src/shared_primitives => voting-circuits/src/circuit}/spend_authority.rs (95%) diff --git a/orchard/src/circuit.rs b/orchard/src/circuit.rs index 8b773dd5..11596551 100644 --- a/orchard/src/circuit.rs +++ b/orchard/src/circuit.rs @@ -26,7 +26,7 @@ use self::{ use crate::{ builder::SpendInfo, constants::{ - OrchardCommitDomains, OrchardFixedBases, OrchardHashDomains, + OrchardCommitDomains, OrchardFixedBases, OrchardFixedBasesFull, OrchardHashDomains, MERKLE_DEPTH_ORCHARD, }, keys::{ @@ -45,7 +45,7 @@ use crate::{ use halo2_gadgets::{ ecc::{ chip::{EccChip, EccConfig}, - NonIdentityPoint, Point, ScalarFixed, ScalarFixedShort, ScalarVar, + FixedPoint, NonIdentityPoint, Point, ScalarFixed, ScalarFixedShort, ScalarVar, }, poseidon::{primitives as poseidon, Pow5Chip as PoseidonChip, Pow5Config as PoseidonConfig}, sinsemilla::{ @@ -552,19 +552,24 @@ impl plonk::Circuit for Circuit { }; // Spend authority (https://p.z.cash/ZKS:action-spend-authority) - // - // Uses the shared gadget from crate::shared_primitives – a 1:1 copy of - // the upstream Orchard spend authority check: - // https://github.com/zcash/orchard/blob/main/src/circuit.rs#L542-L558 - crate::shared_primitives::spend_authority::prove_spend_authority( - ecc_chip.clone(), - layouter.namespace(|| "spend authority"), - self.alpha, - &ak_P.clone().into(), - config.primary, - RK_X, - RK_Y, - )?; + { + let alpha = + ScalarFixed::new(ecc_chip.clone(), layouter.namespace(|| "alpha"), self.alpha)?; + + // alpha_commitment = [alpha] SpendAuthG + let (alpha_commitment, _) = { + let spend_auth_g = OrchardFixedBasesFull::SpendAuthG; + let spend_auth_g = FixedPoint::from_inner(ecc_chip.clone(), spend_auth_g); + spend_auth_g.mul(layouter.namespace(|| "[alpha] SpendAuthG"), alpha)? + }; + + // [alpha] SpendAuthG + ak_P + let rk = alpha_commitment.add(layouter.namespace(|| "rk"), &ak_P)?; + + // Constrain rk to equal public input + layouter.constrain_instance(rk.inner().x().cell(), config.primary, RK_X)?; + layouter.constrain_instance(rk.inner().y().cell(), config.primary, RK_Y)?; + } // Diversified address integrity (https://p.z.cash/ZKS:action-addr-integrity?partial). let pk_d_old = { diff --git a/orchard/src/lib.rs b/orchard/src/lib.rs index 42192456..3e2aac15 100644 --- a/orchard/src/lib.rs +++ b/orchard/src/lib.rs @@ -31,8 +31,6 @@ pub mod builder; pub mod bundle; #[cfg(feature = "circuit")] pub mod circuit; -#[cfg(feature = "circuit")] -pub mod shared_primitives; #[allow(missing_docs)] pub mod constants; pub mod keys; diff --git a/orchard/src/shared_primitives/mod.rs b/orchard/src/shared_primitives/mod.rs deleted file mode 100644 index bc81ca58..00000000 --- a/orchard/src/shared_primitives/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Shared circuit primitives extracted 1:1 from the upstream Orchard action circuit. -//! -//! Each sub-module contains circuit gadgets that are exact copies of the -//! corresponding code in [`zcash/orchard`]. This makes it easy to audit that -//! the voting circuits reuse the same constraint logic as mainline Orchard. -//! -//! [`zcash/orchard`]: https://github.com/zcash/orchard - -pub mod spend_authority; diff --git a/voting-circuits/src/circuit/address_ownership.rs b/voting-circuits/src/circuit/address_ownership.rs index 64658a39..533b2e1e 100644 --- a/voting-circuits/src/circuit/address_ownership.rs +++ b/voting-circuits/src/circuit/address_ownership.rs @@ -33,8 +33,8 @@ use halo2_gadgets::sinsemilla::chip::SinsemillaChip; /// Computes `[scalar] * SpendAuthG` using the Orchard fixed base. /// -/// Thin wrapper around the shared gadget in `orchard::shared_primitives` – -/// see [`orchard::shared_primitives::spend_authority`] for the upstream reference. +/// Thin wrapper around the shared gadget in `crate::circuit::spend_authority` – +/// see [`crate::circuit::spend_authority`] for the upstream reference. /// /// Used by delegation (condition 4: alpha), vote proof (condition 3: vsk). /// Returns the resulting curve point so the caller can e.g. add `ak_P` for rk diff --git a/voting-circuits/src/circuit/mod.rs b/voting-circuits/src/circuit/mod.rs index ba7270b8..3bd1c56f 100644 --- a/voting-circuits/src/circuit/mod.rs +++ b/voting-circuits/src/circuit/mod.rs @@ -4,5 +4,6 @@ pub mod address_ownership; pub mod elgamal; pub mod mul_chip; pub mod poseidon_merkle; +pub mod spend_authority; pub mod van_integrity; pub mod vote_commitment; diff --git a/orchard/src/shared_primitives/spend_authority.rs b/voting-circuits/src/circuit/spend_authority.rs similarity index 95% rename from orchard/src/shared_primitives/spend_authority.rs rename to voting-circuits/src/circuit/spend_authority.rs index d55b7c91..e93db771 100644 --- a/orchard/src/shared_primitives/spend_authority.rs +++ b/voting-circuits/src/circuit/spend_authority.rs @@ -18,8 +18,8 @@ //! (§ 4.15, Spend Authorization Signature) //! //! This module extracts that logic into a reusable gadget so that both the -//! Orchard action circuit and the voting delegation/vote-proof circuits can -//! share the exact same constraint code. +//! voting delegation and vote-proof circuits can share the exact same +//! constraint code as the upstream Orchard action circuit. use halo2_proofs::{ circuit::{Layouter, Value}, @@ -27,7 +27,7 @@ use halo2_proofs::{ }; use pasta_curves::pallas; -use crate::constants::{OrchardFixedBases, OrchardFixedBasesFull}; +use orchard::constants::{OrchardFixedBases, OrchardFixedBasesFull}; use halo2_gadgets::ecc::{ chip::EccChip, FixedPoint, Point, ScalarFixed, }; diff --git a/voting-circuits/src/delegation/circuit.rs b/voting-circuits/src/delegation/circuit.rs index 8630a4f7..6a5c8ce1 100644 --- a/voting-circuits/src/delegation/circuit.rs +++ b/voting-circuits/src/delegation/circuit.rs @@ -763,11 +763,11 @@ impl plonk::Circuit for Circuit { // The out-of-circuit verifier checks that the keystone signature is valid under rk, // so this links the ZKP to the signature without revealing ak. // - // Uses the shared gadget from orchard::shared_primitives – a 1:1 copy of + // Uses the shared gadget from crate::circuit::spend_authority – a 1:1 copy of // the upstream Orchard spend authority check: // https://github.com/zcash/orchard/blob/main/src/circuit.rs#L542-L558 - // Note: RK_X and RK_Y are public inputs.ß - orchard::shared_primitives::spend_authority::prove_spend_authority( + // Note: RK_X and RK_Y are public inputs. + crate::circuit::spend_authority::prove_spend_authority( ecc_chip.clone(), layouter.namespace(|| "cond4 spend authority"), self.alpha, diff --git a/voting-circuits/src/vote_proof/circuit.rs b/voting-circuits/src/vote_proof/circuit.rs index 2da4f5c0..62340c78 100644 --- a/voting-circuits/src/vote_proof/circuit.rs +++ b/voting-circuits/src/vote_proof/circuit.rs @@ -905,10 +905,10 @@ impl plonk::Circuit for Circuit { // The out-of-circuit verifier checks that the vote signature is valid under r_vpk, // so this links the ZKP to the signature without revealing ak. // - // Uses the shared gadget from orchard::shared_primitives – a 1:1 copy of + // Uses the shared gadget from crate::circuit::spend_authority – a 1:1 copy of // the upstream Orchard spend authority check: // https://github.com/zcash/orchard/blob/main/src/circuit.rs#L542-L558 - orchard::shared_primitives::spend_authority::prove_spend_authority( + crate::circuit::spend_authority::prove_spend_authority( ecc_chip.clone(), layouter.namespace(|| "cond4 spend authority"), self.alpha_v, From be072e62ab4c966f6fb7131c2577a402f0336160 Mon Sep 17 00:00:00 2001 From: Adam Tucker Date: Fri, 10 Apr 2026 11:48:45 -0600 Subject: [PATCH 4/6] revert: remove dev profile tweaks from orchard fork Revert [profile.dev] opt-level and [profile.release/bench] debug settings to match upstream zcash/orchard v0.11.0. --- orchard/Cargo.toml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/orchard/Cargo.toml b/orchard/Cargo.toml index c9c7dee6..2a037ed9 100644 --- a/orchard/Cargo.toml +++ b/orchard/Cargo.toml @@ -100,11 +100,8 @@ harness = false name = "circuit" harness = false -[profile.dev] -opt-level = 1 # Faster runtime for ZK circuits without full release optimization cost - [profile.release] -debug = false +debug = true [profile.bench] -debug = false +debug = true From 86345b8a0502640d4698d94d017224b3d5719af1 Mon Sep 17 00:00:00 2001 From: Adam Tucker Date: Fri, 10 Apr 2026 11:54:23 -0600 Subject: [PATCH 5/6] refactor: tighten unused pub widenings in orchard fork back to pub(crate) Revert visibility widenings that voting-circuits does not import: T_Q, T_P, L_ORCHARD_SCALAR, L_VALUE, NullifierDerivingKey::{prf_nf, to_bytes, from_bytes}, CommitIvkRandomness::{to_bytes, from_bytes}, Nullifier::dummy, Anchor::inner, value_commit_orchard, lebs2ip, lebs2ip_field, i2lebsp. Also remove circuit/README.md which documented voting-circuits gadgets, not orchard internals. --- orchard/src/circuit/README.md | 158 ---------------------------------- orchard/src/circuit/gadget.rs | 2 +- orchard/src/constants.rs | 8 +- orchard/src/keys.rs | 10 +-- orchard/src/note/nullifier.rs | 3 +- orchard/src/spec.rs | 6 +- orchard/src/tree.rs | 2 +- 7 files changed, 15 insertions(+), 174 deletions(-) delete mode 100644 orchard/src/circuit/README.md diff --git a/orchard/src/circuit/README.md b/orchard/src/circuit/README.md deleted file mode 100644 index 812321ae..00000000 --- a/orchard/src/circuit/README.md +++ /dev/null @@ -1,158 +0,0 @@ -# Gadgets - -## Address ownership gadget - -Shared circuit gadget that encapsulates **address ownership** and **SpendAuthG fixed-base multiplication** used by both ZKP #1 (delegation) and ZKP #2 (vote proof). Same pattern as the shared VAN integrity gadget (`van_integrity.rs`): chip-agnostic, takes config/chip refs and assigned cells. - -### Why it exists - -Both delegation and vote proof circuits enforce: - -1. **SpendAuthG fixed-base mul** — `[scalar] * SpendAuthG` (delegation: `alpha` then add `ak_P` for `rk`; vote proof: `vsk` then `ExtractP` for `ak`). -2. **Address ownership** — `ivk = CommitIvk(ak, nk, rivk)`, then `pk_d = [ivk] * g_d`, constrained to the claimed address. - -This module provides a single place for that constraint logic so delegation and vote proof don’t duplicate the same CommitIvk → scalar mul → constrain wiring. - -### API - -### `spend_auth_g_mul(ecc_chip, layouter, label, scalar) -> Result` - -Computes `[scalar] * SpendAuthG` using `OrchardFixedBasesFull::SpendAuthG`. - -- **Delegation (condition 4):** `alpha` → `alpha_commitment`; caller adds `ak_P` and constrains to `rk` (public inputs). -- **Vote proof (condition 3):** `vsk` → `vsk_ak_point`; caller does `ak = vsk_ak_point.extract_p().inner().clone()` then passes `ak` into `prove_address_ownership`. - -### `prove_address_ownership(..., ak, nk, rivk, g_d, pk_d_claimed) -> Result, Error>` - -1. Calls `commit_ivk(..., ak, nk, rivk)` to get `ivk`. -2. Converts `ivk` to `ScalarVar`, computes `derived_pk_d = g_d.mul(ivk_scalar)`. -3. Constrains `derived_pk_d == pk_d_claimed`. - -Returns the **ivk cell** so callers (e.g. delegation) can reuse it for per-note diversified address checks (e.g. condition 11). - -- **Delegation (condition 5):** `ak` from `ExtractP(ak_P)`, `nk` from keystone note, `g_d_signed`, `pk_d_signed`. Returned `ivk_cell` is passed into per-note slot synthesis. -- **Vote proof (condition 3):** `ak` from `ExtractP([vsk]*SpendAuthG)`, `vsk_nk`, `rivk_v`, `vpk_g_d_point`, `vpk_pk_d_point`. Return value is unused. - -### Dependencies - -- Reuses existing **CommitIvk** from `commit_ivk.rs` (same `CommitIvkChip`, `SinsemillaChip`, `EccChip`). No new chips; this is a thin wrapper: CommitIvk + scalar mul + constrain_equal. - -### Usage - -- **Delegation:** `orchard/src/delegation/circuit.rs` — condition 4 uses `spend_auth_g_mul`, condition 5 uses `prove_address_ownership`. -- **Vote proof:** `orchard/src/vote_proof/circuit.rs` — condition 3 uses both. - -The main Orchard action circuit could later call `spend_auth_g_mul` and `prove_address_ownership` for consistency (optional follow-up). - -## VAN integrity gadget - -Shared circuit gadget that encapsulates the **VAN (Vote Authority Note) integrity** two-layer Poseidon hash used by ZKP #1 (delegation, condition 7) and ZKP #2 (vote proof, conditions 2 and 6). Same pattern as the address ownership gadget: chip-agnostic, takes config and assigned cells. - -### Why it exists - -Both delegation and vote proof circuits need a shared commitment shape for the governance commitment / Vote Authority Note: - -- **Delegation (condition 7):** when creating the output note, the circuit commits to `van_comm` = two-layer hash(domain_van, g_d, pk_d, value, round, proposal_authority; rand). This becomes the VAN that can later be spent in vote proof. -- **Vote proof (condition 2):** the voter proves the old VAN commitment is correctly formed (same hash structure) so that VANs created by delegation are valid leaves. -- **Vote proof (condition 6):** the new VAN commitment (after decrementing proposal authority) uses the same hash structure. - -A single module provides the two-layer Poseidon (core hash then blind with rand) so both circuits use identical constraints and hashes. - -### API - -**`DOMAIN_VAN`** — `u64` constant `0`. Domain tag for Vote Authority Notes; `DOMAIN_VC = 1` for Vote Commitments. Prepended as the first Poseidon input for domain separation in the shared vote commitment tree. - -**`van_integrity_hash(g_d_x, pk_d_x, value, voting_round_id, proposal_authority, van_comm_rand) -> pallas::Base`** - -Out-of-circuit two-layer hash. Computes: - -- `van_comm_core = Poseidon(DOMAIN_VAN, g_d_x, pk_d_x, value, voting_round_id, proposal_authority)` -- `result = Poseidon(van_comm_core, van_comm_rand)` - -Used by builders and tests to compute the expected VAN / van_comm value. - -**`van_integrity_poseidon(poseidon_config, layouter, label, domain_van, g_d_x, pk_d_x, value, voting_round_id, proposal_authority, van_comm_rand) -> Result, Error>`** - -In-circuit two-layer hash with the same structure. Takes assigned cells and a `PoseidonConfig` (P128Pow5T3, width 3, rate 2). Returns the final hash cell. Callers constrain it to their witnessed commitment (delegation: van_comm; vote proof: vote_authority_note_old or vote_authority_note_new). - -### Dependencies - -- Uses **Poseidon** only: `halo2_gadgets::poseidon::Pow5Chip`, `P128Pow5T3`, `ConstantLength<6>` and `ConstantLength<2>`. No ECC or Sinsemilla; any circuit that already has a compatible Poseidon config can call `van_integrity_poseidon`. - -### Usage - -- **Delegation:** `orchard/src/delegation/circuit.rs` — condition 7 assigns `domain_van` from `DOMAIN_VAN`, then calls `van_integrity_poseidon` and constrains the result to `van_comm` (public input). -- **Vote proof:** `orchard/src/vote_proof/circuit.rs` — condition 2 (old VAN) and condition 6 (new VAN) call `van_integrity_poseidon` and constrain to `vote_authority_note_old` and `vote_authority_note_new` respectively. - -### Constraint flow (conceptual) - -``` -Delegation (condition 7) Vote proof (condition 2) Vote proof (condition 6) -──────────────────────── ──────────────────────── ──────────────────────── -domain_van, g_d_new, domain_van, vpk_g_d, domain_van, vpk_g_d, -pk_d_new, value=0, vpk_pk_d, total_value, vpk_pk_d, total_value, -vote_round_id, vote_round_id, vote_round_id, -MAX_PROPOSAL_AUTHORITY proposal_authority_old proposal_authority_new - ↓ ↓ ↓ -Poseidon(core) Poseidon(core) Poseidon(core) - ↓ ↓ ↓ -Poseidon(core, rand) Poseidon(core, rand) Poseidon(core, rand) - ↓ ↓ ↓ -constrain van_comm constrain vote_authority_ constrain vote_authority_ -(public input) note_old note_new (public input) -``` - -### See also (VAN integrity) - -- `address_ownership.rs` — shared SpendAuthG and CommitIvk-based address ownership (conditions 4/5 delegation, condition 3 vote proof). -- Delegation README: condition 7 (gov commitment integrity). -- Vote proof README: conditions 2 and 6. - -## Constraint flow (conceptual) - -``` -ZKP 1 (delegation) ZKP 2 (vote proof) -──────────────────── ──────────────────── -ak witnessed [vsk]*SpendAuthG → ak - ↓ ↓ -CommitIvk (shared) CommitIvk (shared) - ↓ ↓ -[ivk]*g_d_signed [ivk_v]*vpk_g_d - ↓ ↓ -constrain pk_d_signed constrain vpk_pk_d - -[rk =] [alpha]*SpendAuthG + ak_P (delegation only) -``` - -### See also - -- `address_ownership.rs` — shared SpendAuthG and CommitIvk-based address ownership (conditions 4/5 delegation, condition 3 vote proof). -- `elgamal.rs` — shared El Gamal encryption integrity (vote proof condition 11). -- `van_integrity.rs` — shared VAN integrity Poseidon hash (conditions 2/6/7). -- `commit_ivk.rs` — Sinsemilla-based CommitIvk and canonicity gates. -- Delegation README: conditions 4 and 5. -- Vote proof README: condition 3. - -## El Gamal gadget - -Shared circuit gadget that proves **El Gamal encryption integrity** for the vote proof (ZKP #2, condition 11). For each of the 5 vote shares, it constrains that the witnessed ciphertext x-coordinates are correct: C1_i = [r_i]*G, C2_i = [v_i]*G + [r_i]*ea_pk, with G = SpendAuthG and ea_pk from public inputs. - -### Why it exists - -The vote proof must bind the Poseidon hash of enc_share x-coordinates (condition 10) to the actual ECC computation. Extracting this logic into a dedicated gadget keeps the vote proof synthesis simpler and allows layout/optimization work to be done in one place. - -### API - -**`prove_elgamal_encryptions(ecc_chip, layouter, namespace, ea_pk, ea_pk_loc, advice_col, share_cells, r_cells, enc_c1_cells, enc_c2_cells) -> Result<(), Error>`** - -Per share i: constrains C1_i = [r_i]*G and C2_i = [v_i]*G + [r_i]*ea_pk, and `constrain_equal(ExtractP(C1_i), enc_c1_cells[i])` and similarly for C2. The gadget owns all ea_pk scaffolding: it witnesses the point internally and calls `layouter.constrain_instance` using the `EaPkInstanceLoc` descriptor (instance column + x/y row offsets). The caller supplies the four varying arrays and `ea_pk` as a `Value`. - -**Out-of-circuit (same module):** `spend_auth_g_affine()`, `base_to_scalar(b)`, `elgamal_encrypt(share_value, randomness, ea_pk)` — used by the vote proof builder and tests. - -### Dependencies - -- Uses **EccChip** only (same config as condition 3). No new columns; no change to K. - -### Usage - -- **Vote proof:** `orchard/src/vote_proof/circuit.rs` — condition 11 witnesses r_0..r_4, then calls `prove_elgamal_encryptions` with `EaPkInstanceLoc { instance: config.primary, x_row: EA_PK_X, y_row: EA_PK_Y }` and `config.advices[0]`. The gadget handles ea_pk witnessing, instance-pinning, and sign-cell constant assignment internally. diff --git a/orchard/src/circuit/gadget.rs b/orchard/src/circuit/gadget.rs index c8bf2af0..89872e47 100644 --- a/orchard/src/circuit/gadget.rs +++ b/orchard/src/circuit/gadget.rs @@ -122,7 +122,7 @@ pub fn assign_constant( /// `ValueCommit^Orchard` from [Section 5.4.8.3 Homomorphic Pedersen commitments (Sapling and Orchard)]. /// /// [Section 5.4.8.3 Homomorphic Pedersen commitments (Sapling and Orchard)]: https://zips.z.cash/protocol/protocol.pdf#concretehomomorphiccommit -pub fn value_commit_orchard< +pub(in crate::circuit) fn value_commit_orchard< EccChip: EccInstructions< pallas::Affine, FixedPoints = OrchardFixedBases, diff --git a/orchard/src/constants.rs b/orchard/src/constants.rs index 6ce370c5..1356a0c1 100644 --- a/orchard/src/constants.rs +++ b/orchard/src/constants.rs @@ -16,20 +16,20 @@ pub const MERKLE_DEPTH_ORCHARD: usize = 32; /// The Pallas scalar field modulus is $q = 2^{254} + \mathsf{t_q}$. /// -pub const T_Q: u128 = 45560315531506369815346746415080538113; +pub(crate) const T_Q: u128 = 45560315531506369815346746415080538113; /// The Pallas base field modulus is $p = 2^{254} + \mathsf{t_p}$. /// -pub const T_P: u128 = 45560315531419706090280762371685220353; +pub(crate) const T_P: u128 = 45560315531419706090280762371685220353; /// $\ell^\mathsf{Orchard}_\mathsf{base}$ pub const L_ORCHARD_BASE: usize = 255; /// $\ell^\mathsf{Orchard}_\mathsf{scalar}$ -pub const L_ORCHARD_SCALAR: usize = 255; +pub(crate) const L_ORCHARD_SCALAR: usize = 255; /// $\ell_\mathsf{value}$ -pub const L_VALUE: usize = 64; +pub(crate) const L_VALUE: usize = 64; /// SWU hash-to-curve personalization for the group hash for key diversification pub const KEY_DIVERSIFICATION_PERSONALIZATION: &str = "z.cash:Orchard-gd"; diff --git a/orchard/src/keys.rs b/orchard/src/keys.rs index e587706f..cc6af956 100644 --- a/orchard/src/keys.rs +++ b/orchard/src/keys.rs @@ -243,17 +243,17 @@ impl From<&SpendingKey> for NullifierDerivingKey { impl NullifierDerivingKey { /// Computes PRF^nf on the given rho value. - pub fn prf_nf(&self, rho: pallas::Base) -> pallas::Base { + pub(crate) fn prf_nf(&self, rho: pallas::Base) -> pallas::Base { prf_nf(self.0, rho) } /// Converts this nullifier deriving key to its serialized form. - pub fn to_bytes(self) -> [u8; 32] { + pub(crate) fn to_bytes(self) -> [u8; 32] { <[u8; 32]>::from(self.0) } /// Parses a nullifier deriving key from a byte slice. - pub fn from_bytes(bytes: &[u8]) -> Option { + pub(crate) fn from_bytes(bytes: &[u8]) -> Option { let nk_bytes = <[u8; 32]>::try_from(bytes).ok()?; let nk = pallas::Base::from_repr(nk_bytes).map(NullifierDerivingKey); if nk.is_some().into() { @@ -285,12 +285,12 @@ impl CommitIvkRandomness { } /// Converts this commit-ivk randomness to its serialized form. - pub fn to_bytes(self) -> [u8; 32] { + pub(crate) fn to_bytes(self) -> [u8; 32] { <[u8; 32]>::from(self.0) } /// Parses commit-ivk randomness from a byte slice. - pub fn from_bytes(bytes: &[u8]) -> Option { + pub(crate) fn from_bytes(bytes: &[u8]) -> Option { let rivk_bytes = <[u8; 32]>::try_from(bytes).ok()?; let rivk = pallas::Scalar::from_repr(rivk_bytes).map(CommitIvkRandomness); if rivk.is_some().into() { diff --git a/orchard/src/note/nullifier.rs b/orchard/src/note/nullifier.rs index abae6732..39485161 100644 --- a/orchard/src/note/nullifier.rs +++ b/orchard/src/note/nullifier.rs @@ -30,8 +30,7 @@ impl Nullifier { /// /// Instead of explicitly sampling for a unique nullifier, we rely here on the size of /// the base field to make the chance of sampling a colliding nullifier negligible. - /// Generates a random dummy nullifier. - pub fn dummy(rng: &mut impl RngCore) -> Self { + pub(crate) fn dummy(rng: &mut impl RngCore) -> Self { Nullifier(extract_p(&pallas::Point::random(rng))) } diff --git a/orchard/src/spec.rs b/orchard/src/spec.rs index 577aefca..3f424d69 100644 --- a/orchard/src/spec.rs +++ b/orchard/src/spec.rs @@ -287,7 +287,7 @@ pub(crate) fn extract_p_bottom(point: CtOption) -> CtOption(bits: &[bool; L]) -> F { +pub(crate) fn lebs2ip_field(bits: &[bool; L]) -> F { F::from(lebs2ip::(bits)) } @@ -296,7 +296,7 @@ pub fn lebs2ip_field(bits: &[bool; L]) -> F { /// # Panics /// /// Panics if the bitstring is longer than 64 bits. -pub fn lebs2ip(bits: &[bool; L]) -> u64 { +pub(crate) fn lebs2ip(bits: &[bool; L]) -> u64 { assert!(L <= 64); bits.iter() .enumerate() @@ -309,7 +309,7 @@ pub fn lebs2ip(bits: &[bool; L]) -> u64 { /// /// Panics if the expected length of the sequence `NUM_BITS` exceeds /// 64. -pub fn i2lebsp(int: u64) -> [bool; NUM_BITS] { +pub(crate) fn i2lebsp(int: u64) -> [bool; NUM_BITS] { assert!(NUM_BITS <= 64); gen_const_array(|mask: usize| (int & (1 << mask)) != 0) } diff --git a/orchard/src/tree.rs b/orchard/src/tree.rs index 2827abe3..6ea4d24c 100644 --- a/orchard/src/tree.rs +++ b/orchard/src/tree.rs @@ -69,7 +69,7 @@ impl Anchor { } /// Returns the inner base field element. - pub fn inner(&self) -> pallas::Base { + pub(crate) fn inner(&self) -> pallas::Base { self.0 } From 6f2f32d85e8c698c7bf2148a5819e383d908cfa7 Mon Sep 17 00:00:00 2001 From: Adam Tucker Date: Fri, 10 Apr 2026 13:41:16 -0600 Subject: [PATCH 6/6] docs: add UPSTREAM.md listing changes needed to eliminate orchard fork Comprehensive checklist of visibility widenings and structural additions that need to be upstreamed to zcash/orchard before the fork can be removed entirely. --- UPSTREAM.md | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 UPSTREAM.md diff --git a/UPSTREAM.md b/UPSTREAM.md new file mode 100644 index 00000000..a15d9a14 --- /dev/null +++ b/UPSTREAM.md @@ -0,0 +1,114 @@ +# Orchard fork: upstream checklist + +This document lists every remaining change in `orchard/` (our fork of +[zcash/orchard](https://github.com/zcash/orchard) v0.11.0) that would need to +be accepted upstream before we can delete the fork and depend on the published +crate. + +Once all items below are merged upstream, replace +`orchard = { path = "../orchard" }` in `voting-circuits/Cargo.toml` with +`orchard = ""` and delete the `orchard/` directory. + +## Visibility widenings (`pub(crate)` to `pub`) + +These items are `pub(crate)` in upstream v0.11.0. Our fork widens them to `pub` +because voting-circuits imports them directly. + +### `circuit.rs` / `circuit/gadget.rs` + +- [ ] `pub mod commit_ivk` (circuit.rs) — expose the CommitIvk sub-circuit +- [ ] `pub mod note_commit` (circuit.rs) — expose the NoteCommit sub-circuit +- [ ] `pub mod gadget` (circuit.rs) — expose the gadget module +- [ ] `pub trait AddInstruction` (gadget.rs) — addition trait +- [ ] `pub fn assign_free_advice` (gadget.rs) — cell assignment helper +- [ ] `pub fn derive_nullifier` (gadget.rs) — nullifier derivation gadget +- [ ] `pub use commit_ivk`, `pub use note_commit` (gadget.rs) — re-exports + +### `circuit/gadget/add_chip.rs` + +- [ ] `pub struct AddConfig` / `pub struct AddChip` +- [ ] `pub fn configure` / `pub fn construct` + +### `circuit/commit_ivk.rs` + +- [ ] `pub fn configure` / `pub fn construct` +- [ ] `pub mod gadgets` / `pub fn commit_ivk` + +### `circuit/note_commit.rs` + +- [ ] `pub fn configure` / `pub fn construct` +- [ ] `pub mod gadgets` / `pub fn note_commit` + +### `constants.rs` + +- [ ] `pub const L_ORCHARD_BASE` + +### `constants/fixed_bases.rs` + +- [ ] `pub` re-exports: `OrchardFixedBases`, `OrchardFixedBasesFull` + +### `keys.rs` + +- [ ] `pub struct NullifierDerivingKey` + `pub fn inner()` +- [ ] `pub struct CommitIvkRandomness` + `pub fn inner()` +- [ ] `pub fn SpendingKey::random()` +- [ ] `pub fn SpendAuthorizingKey::derive_inner()` +- [ ] `pub fn FullViewingKey::nk()` / `pub fn FullViewingKey::rivk()` +- [ ] `pub fn DiversifiedTransmissionKey::inner()` / `pub fn DiversifiedTransmissionKey::to_bytes()` + +### `spec.rs` + +- [ ] `pub struct NonIdentityPallasPoint` + `pub fn from_bytes()` + +### `note.rs` / `note/commitment.rs` / `note/nullifier.rs` + +- [ ] `pub mod commitment` / `pub mod nullifier` (note.rs) +- [ ] `pub fn Note::new` / `pub fn Note::dummy` / `pub fn Note::from_nf_old` +- [ ] `pub fn Note::into_inner` / `pub fn Note::psi` / `pub fn Note::rcm` +- [ ] `pub use NoteCommitTrapdoor` re-export (note.rs) +- [ ] `pub struct NoteCommitTrapdoor` + `pub fn inner()` (commitment.rs) +- [ ] `pub fn NoteCommitment::inner()` +- [ ] `pub fn ExtractedNoteCommitment::inner()` +- [ ] `pub` field on `Nullifier` (nullifier.rs) + +### `tree.rs` + +- [ ] `pub fn MerkleHashOrchard::inner()` +- [ ] `pub fn MerklePath::dummy()` + +### `value.rs` + +- [ ] `pub fn NoteValue::zero()` + +### `address.rs` + +- [ ] `pub fn Address::g_d()` +- [ ] `pub fn Address::pk_d()` + +## Structural additions (new code, not just visibility) + +These items don't exist in upstream v0.11.0 at all. They would need to be +proposed as new functionality. + +### `constants/fixed_bases.rs` + +- [ ] `OrchardBaseFieldBases` enum — new enum routing base-field fixed-base + multiplication (variants: `NullifierK`, `SpendAuthGBase`) +- [ ] `OrchardShortScalarBases` enum — new enum routing short-scalar fixed-base + multiplication (variants: `ValueCommitV`, `SpendAuthGShort`) +- [ ] Expanded `OrchardFixedBases` enum — new `Base(OrchardBaseFieldBases)` and + `Short(OrchardShortScalarBases)` variants alongside the existing + `Full(OrchardFixedBasesFull)` +- [ ] `From` trait implementations and `FixedPoint` impls for the new variants +- [ ] Test additions for the new variants + +### `constants/fixed_bases/spend_auth_g.rs` + +- [ ] `Z_SHORT` / `U_SHORT` precomputed tables for `SpendAuthGShort` — enables + short-scalar multiplication on the spend authorization generator + +### `circuit/gadget.rs` + +- [ ] `pub fn assign_constant()` — helper for assigning a constant value + constrained by the verification key (counterpart to the existing + `assign_free_advice`)