Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 6 additions & 3 deletions zcash_voting/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ readme = "README.md"
default = []
# PIR client surface for fetching IMT non-membership proofs over direct
# Hyper/Rustls HTTP.
client-pir = [
pir = [
"dep:imt-tree",
"dep:pir-client",
"dep:pir-types",
Expand All @@ -25,7 +25,7 @@ client-pir = [
"dep:hyper-util",
]
# Vote commitment tree HTTP sync surface for VAN witness generation.
client-tree-sync = [
tree-sync = [
"dep:imt-tree",
"dep:vote-commitment-tree",
"dep:vote-commitment-tree-client",
Expand All @@ -37,8 +37,11 @@ client-tree-sync = [
"dep:hyper-util",
"dep:tokio",
]
# Legacy feature names kept as aliases during the wallet migration window.
client-pir = ["pir"]
client-tree-sync = ["tree-sync"]
# Backwards-compatible aggregate for consumers that already enable `client`.
client = ["client-pir", "client-tree-sync"]
client = ["pir", "tree-sync"]
# Downstream test-only helpers for setting up persisted voting state without
# depending on SQLite schema details.
test-fixtures = []
Expand Down
54 changes: 44 additions & 10 deletions zcash_voting/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,43 @@ Client-side library for integrating [Zcash shielded voting](https://github.com/v

## Usage

Wallets typically consume this through a language bridge:

- **Rust wallets**: add `zcash_voting = "0.9"` to `Cargo.toml`.
- **Mobile wallets**: expose the needed Rust APIs through the wallet SDK's FFI
layer and keep platform-specific work, such as CSPRNG byte generation and
HTTP submission, at the SDK boundary.

See the [wallet integration guide](https://github.com/valargroup/vote-sdk/blob/main/docs/wallet-integration.md) for the full flow.
Wallets should import `zcash_voting::prelude::*` and follow the stable setup →
precompute → delegate lifecycle:

1. Open a `VotingDb`, set the wallet id, and call `create_round`.
2. Convert eligible Orchard notes into `NoteInfo` with
`NoteInfo::from_orchard_note`, then call `ensure_bundles`.
3. Build the governance PCZT with `setup_delegation`.
4. Precompute delegation inputs with `note_witnesses` and, with the `pir`
feature, `delegation_pir`.
5. Prove with `delegate::prove`, assemble submission fields with
`delegation_submission`, and record chain recovery data with
`record_submission` and `record_van_position`.

## Crate layout

| Crate | Purpose |
|---|---|
| **`zcash_voting`** (this crate) | Top-level API: proof generation, hotkey derivation, share construction, PCZT assembly, round-state storage. |
| **`zcash_voting`** (this crate) | Stable wallet API: round setup, note bundles, delegation precompute/proving, hotkey derivation, and round-state storage. |
| [`vote-commitment-tree`](../vote-commitment-tree) | Append-only Poseidon Merkle tree for VANs and vote commitments. |
| [`vote-commitment-tree-client`](../vote-commitment-tree-client) | HTTP client + CLI for syncing the vote commitment tree from a running chain node. |

## Public modules

| Module | Purpose |
|---|---|
| `prelude` | Recommended imports for wallet SDKs. |
| `round` | `VotingDb`, `RoundParams`, `RoundInfo`, and idempotent `ensure_bundles`. |
| `precompute` | Orchard note witness generation and PIR precompute wrappers. |
| `delegate` | PCZT setup, proof generation, submission assembly, and chain recovery writes. |
| `phases` | Per-bundle `DelegationPhase` derived from persisted artifacts. |
| `pir` | PIR endpoint selection helpers and client re-exports. |
| `hotkey` | Primitive hotkey derivation from caller-supplied seed bytes. |
| `governance` | Low-level governance derivations and `BALLOT_DIVISOR`. |

Lower-level modules from previous releases remain available during the 0.11
migration window, but new wallet code should prefer the lifecycle modules above.

## Shared wallet policy helpers

The `share_policy` module contains pure helpers for wallet-side voting behavior
Expand All @@ -41,12 +61,26 @@ crate own the sampling and ordering policy.

- **`orchard 0.13.1`** from crates.io, with the
`unstable-voting-circuits` feature enabled for the governance proof paths.
- **`voting-circuits 0.5.0`** for the delegation and vote proof circuits.
- **`voting-circuits 0.6.0`** for the delegation and vote proof circuits.
- **`vote-commitment-tree 0.3`** and **`vote-commitment-tree-client 0.5`** for
vote commitment tree state and optional HTTP sync.
- **`pczt`, `zcash_keys`, `zcash_primitives`, and `zcash_protocol`** from the
published upstream Zcash crate line used by this release.

## Migrating from 0.10

- Enable `pir` and `tree-sync` instead of `client-pir` and
`client-tree-sync`. The old feature names remain aliases for existing
consumers during migration.
- Prefer `VotingDb::create_round`, `VotingDb::ensure_bundles`, and
`VotingDb::delegation_phases` over direct `storage::queries` calls.
- Use `precompute::note_witnesses` instead of hand-validating cached
`TreeState` bytes and manually constructing `WitnessData`.
- Use `delegate::submission` with `DelegationSigner::Seed` or
`DelegationSigner::Keystone` instead of separate submission methods.
- Treat contextual hotkey mixing as wallet policy. The library intentionally
keeps `generate_hotkey(seed)` primitive.

## License

Dual-licensed under MIT or Apache-2.0. See [LICENSE-MIT](../LICENSE-MIT) and [LICENSE-APACHE](../LICENSE-APACHE).
249 changes: 249 additions & 0 deletions zcash_voting/src/delegate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
//! Delegation lifecycle API.
//!
//! The functions in this module are the stable wallet-facing delegation flow:
//! build a governance PCZT, precompute proof inputs, prove delegation, assemble
//! chain submission data, and record chain recovery data.

pub use crate::phases::DelegationPhase;

use crate::{
round::VotingDb,
types::{Network, NoteInfo, ProgressReporter, VotingError},
};

/// Wallet-derived keys and chain parameters needed to build a delegation PCZT.
#[derive(Clone, Debug)]
pub struct DelegationKeys {
pub fvk_bytes: Vec<u8>,
pub hotkey_raw_address: [u8; 43],
pub seed_fingerprint: [u8; 32],
pub account_index: u32,
pub address_index: u32,
pub consensus_branch_id: u32,
pub coin_type: u32,
pub round_name: String,
}

/// PCZT setup output that callers hand to a signer or QR encoder.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DelegationSetup {
pub pczt_bytes: Vec<u8>,
pub pczt_sighash: [u8; 32],
pub rk: [u8; 32],
pub action_index: usize,
pub action_bytes: Vec<u8>,
}

/// Generated delegation proof and public submission fields for one bundle.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DelegationProof {
pub bytes: Vec<u8>,
pub rk: [u8; 32],
pub nf_signed: [u8; 32],
pub cmx_new: [u8; 32],
pub van_comm: [u8; 32],
pub gov_nullifiers: [[u8; 32]; 5],
}

/// Signature source used when assembling a delegation transaction payload.
pub enum DelegationSigner<'a> {
Seed {
seed: &'a [u8],
network: Network,
account_index: u32,
},
Keystone {
sig: [u8; 64],
sighash: [u8; 32],
},
}

/// Chain-ready delegation transaction fields for one bundle.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DelegationSubmission {
pub proof: Vec<u8>,
pub rk: [u8; 32],
pub nf_signed: [u8; 32],
pub cmx_new: [u8; 32],
pub gov_comm: [u8; 32],
pub gov_nullifiers: [[u8; 32]; 5],
pub alpha: [u8; 32],
pub vote_round_id: String,
pub spend_auth_sig: [u8; 64],
pub sighash: [u8; 32],
}

/// Builds and persists a governance PCZT for one bundle.
///
/// The bundle must already exist via [`VotingDb::ensure_bundles`]. The returned
/// sighash is the exact message that Keystone or the seed signer must sign.
pub fn setup(
db: &VotingDb,
round_id: &str,
bundle_index: u32,
notes: &[NoteInfo],
keys: &DelegationKeys,
) -> Result<DelegationSetup, VotingError> {
let pczt = db.build_governance_pczt(
round_id,
bundle_index,
notes,
&keys.fvk_bytes,
&keys.hotkey_raw_address,
keys.consensus_branch_id,
keys.coin_type,
&keys.seed_fingerprint,
keys.account_index,
&keys.round_name,
keys.address_index,
)?;

Ok(DelegationSetup {
pczt_bytes: pczt.pczt_bytes,
pczt_sighash: array32("pczt_sighash", pczt.pczt_sighash)?,
rk: array32("rk", pczt.rk)?,
action_index: pczt.action_index,
action_bytes: pczt.action_bytes,
})
}

/// Generates and persists the delegation proof for one bundle.
///
/// Witnesses and PIR proof precompute data must already be present. The proof
/// result is checked against PCZT-derived public fields before persistence.
#[cfg(feature = "pir")]
pub fn prove(
db: &VotingDb,
round_id: &str,
bundle_index: u32,
notes: &[NoteInfo],
hotkey_raw_address: &[u8; 43],
pir_client: &pir_client::PirClientBlocking,
network: Network,
progress: &dyn ProgressReporter,
) -> Result<DelegationProof, VotingError> {
let proof = db.build_and_prove_delegation(
round_id,
bundle_index,
notes,
hotkey_raw_address,
pir_client,
network.id(),
progress,
)?;

Ok(DelegationProof {
bytes: proof.proof,
rk: array32("rk", proof.rk)?,
nf_signed: array32("nf_signed", proof.nf_signed)?,
cmx_new: array32("cmx_new", proof.cmx_new)?,
van_comm: array32("van_comm", proof.van_comm)?,
gov_nullifiers: array32x5("gov_nullifiers", proof.gov_nullifiers)?,
})
}

/// Assembles chain-ready delegation submission fields for one bundle.
///
/// Seed signers derive the spend authorization key internally. Keystone signers
/// must provide the signature over the stored PCZT sighash.
pub fn submission(
db: &VotingDb,
round_id: &str,
bundle_index: u32,
signer: DelegationSigner<'_>,
) -> Result<DelegationSubmission, VotingError> {
let data = match signer {
DelegationSigner::Seed {
seed,
network,
account_index,
} => {
db.get_delegation_submission(round_id, bundle_index, seed, network.id(), account_index)
}
DelegationSigner::Keystone { sig, sighash } => {
db.get_delegation_submission_with_keystone_sig(round_id, bundle_index, &sig, &sighash)
}
}?;

Ok(DelegationSubmission {
proof: data.proof,
rk: array32("rk", data.rk)?,
nf_signed: array32("nf_signed", data.nf_signed)?,
cmx_new: array32("cmx_new", data.cmx_new)?,
gov_comm: array32("gov_comm", data.gov_comm)?,
gov_nullifiers: array32x5("gov_nullifiers", data.gov_nullifiers)?,
alpha: array32("alpha", data.alpha)?,
vote_round_id: data.vote_round_id,
spend_auth_sig: array64("spend_auth_sig", data.spend_auth_sig)?,
sighash: array32("sighash", data.sighash)?,
})
}

/// Records the submitted delegation transaction hash for recovery.
pub fn record_submission(
db: &VotingDb,
round_id: &str,
bundle_index: u32,
tx_hash: &str,
) -> Result<(), VotingError> {
db.store_delegation_tx_hash(round_id, bundle_index, tx_hash)
}

/// Records the confirmed VAN leaf position for a delegated bundle.
pub fn record_van_position(
db: &VotingDb,
round_id: &str,
bundle_index: u32,
position: u32,
) -> Result<(), VotingError> {
db.store_van_position(round_id, bundle_index, position)
}

/// Extracts the ZIP-244 shielded sighash from a serialized voting PCZT.
pub fn pczt_sighash(pczt_bytes: &[u8]) -> Result<[u8; 32], VotingError> {
crate::action::extract_pczt_sighash(pczt_bytes)
}

/// Extracts one SpendAuth signature from a Keystone-signed voting PCZT.
pub fn spend_auth_signature(
signed_pczt_bytes: &[u8],
action_index: usize,
) -> Result<[u8; 64], VotingError> {
crate::action::extract_spend_auth_sig(signed_pczt_bytes, action_index)
}

fn array32(label: &str, value: Vec<u8>) -> Result<[u8; 32], VotingError> {
value
.try_into()
.map_err(|value: Vec<u8>| VotingError::Internal {
message: format!("{label} must be 32 bytes, got {}", value.len()),
})
}

fn array64(label: &str, value: Vec<u8>) -> Result<[u8; 64], VotingError> {
value
.try_into()
.map_err(|value: Vec<u8>| VotingError::Internal {
message: format!("{label} must be 64 bytes, got {}", value.len()),
})
}

fn array32x5(label: &str, values: Vec<Vec<u8>>) -> Result<[[u8; 32]; 5], VotingError> {
if values.len() != 5 {
return Err(VotingError::Internal {
message: format!("{label} must contain 5 entries, got {}", values.len()),
});
}

let arrays = values
.into_iter()
.enumerate()
.map(|(idx, value)| array32(&format!("{label}[{idx}]"), value))
.collect::<Result<Vec<_>, _>>()?;

arrays
.try_into()
.map_err(|arrays: Vec<[u8; 32]>| VotingError::Internal {
message: format!("{label} must contain 5 entries, got {}", arrays.len()),
})
}
7 changes: 7 additions & 0 deletions zcash_voting/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//! Public error surface for wallet integrations.
//!
//! The voting API uses one error type across setup, precompute, delegation, and
//! persistence operations. Re-exporting it from this module gives downstream
//! SDKs a stable import path even as internal modules move.

pub use crate::types::VotingError;
Loading
Loading