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
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,17 @@ All notable changes to this workspace will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this workspace adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased
## v3.1.0-rc.7

### Added
- Added `prepare_commit`, `prepare_commit_batch`, `persist_prepared_commit`,
and `persist_prepared_commit_batch` so wallets can perform expensive ZKP #2
proving outside SQLite transactions, then atomically persist the prepared
result only if its vote-authority, ballot-intent, and current-vote state are
still unchanged. `prepare_commit_batch` takes a `VoteCommitBatch` for the
round, drafts, witness, and stage reporter.
- Added `warm_zkp2_proving_cache` for callers that want to initialize the vote
proving parameters independently of the other proving caches.

## v3.1.0-rc.6

Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

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

2 changes: 1 addition & 1 deletion zcash_voting/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "zcash_voting"
version = "3.1.0-rc.6"
version = "3.1.0-rc.7"
edition = "2021"
rust-version = "1.88"
description = "Client-side library for Zcash shielded voting: ZKP delegation and vote-commitment proofs (Halo 2), ElGamal encryption, governance PCZT construction, Merkle witness generation, and SQLite round-state persistence."
Expand Down
25 changes: 25 additions & 0 deletions zcash_voting/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,31 @@ pub use types::{
MAX_PROPOSAL_ID, MAX_VOTE_OPTIONS, MIN_PROPOSAL_ID, MIN_VOTE_OPTIONS,
};

/// Warms the process-lifetime ZKP #2 proving-key cache.
///
/// The warm-up runs on a large-stack thread and is safe to call repeatedly.
pub fn warm_zkp2_proving_cache() -> Result<(), VotingError> {
const KEYGEN_STACK_BYTES: usize = 64 * 1024 * 1024;

std::thread::Builder::new()
.name("voting-vote-proof-cache-warmup".to_string())
.stack_size(KEYGEN_STACK_BYTES)
.spawn(|| {
voting_circuits::vote_proof::warm_vote_proof_keys().map_err(|e| {
VotingError::ProofFailed {
message: format!("ZKP2 proving cache warm-up failed: {e}"),
}
})
})
.map_err(|e| VotingError::Internal {
message: format!("failed to spawn ZKP2 proving cache warm-up thread: {e}"),
})?
.join()
.map_err(|_| VotingError::Internal {
message: "ZKP2 proving cache warm-up thread panicked".to_string(),
})?
}

/// Warm process-lifetime proving-key caches used by on-device voting proofs.
///
/// This is intentionally best-effort at the cache layer: callers should invoke
Expand Down
10 changes: 6 additions & 4 deletions zcash_voting/src/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,17 +81,19 @@ pub use crate::types::{
MAX_VOTE_OPTIONS, MIN_PROPOSAL_ID, MIN_VOTE_OPTIONS,
};
pub use crate::vote::{
commit as commit_vote, commit_batch, parse_recovery,
commit as commit_vote, commit_batch, parse_recovery, persist_prepared_commit,
persist_prepared_commit_batch, prepare_commit, prepare_commit_batch,
record_submission as record_vote_submission, record_vc_position,
recover_commit as recover_vote_commit, recover_signed_commitments, recovery_bundle,
serialize_recovery, submission as vote_submission, validate_draft_vote, validate_draft_votes,
CommittedVote, DraftVote, SignedVoteCommitment, SignedVoteCommitments, VanWitness, VoteCommit,
VoteCommitStage, VoteRecoveryBundle, VoteSigner, VoteSubmission,
CommittedVote, DraftVote, PreparedVoteCommit, PreparedVoteCommitments, SignedVoteCommitment,
SignedVoteCommitments, VanWitness, VoteCommit, VoteCommitBatch, VoteCommitStage,
VoteRecoveryBundle, VoteSigner, VoteSubmission,
};
pub use crate::warm_proving_caches;
pub use crate::wire::{
DelegationSubmissionWire, VoteCommitmentWire, VoteShareWire, VotingHotkeyTargetV1,
};
pub use crate::{warm_proving_caches, warm_zkp2_proving_cache};

pub use crate::precompute::delegation_pir;

Expand Down
45 changes: 44 additions & 1 deletion zcash_voting/src/storage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ mod migrations;
pub mod operations;
pub mod queries;

use std::sync::Mutex;
use std::{sync::Mutex, time::Duration};

use rusqlite::Connection;

use crate::types::{Network, VotingError};

const SQLITE_BUSY_TIMEOUT: Duration = Duration::from_secs(5);

/// Current phase of a voting round.
///
/// Discriminants are ordered lifecycle ranks; `advance_round_phase` compares
Expand Down Expand Up @@ -85,6 +87,10 @@ impl VotingDb {
message: format!("failed to open database: {}", e),
})?;

conn.busy_timeout(SQLITE_BUSY_TIMEOUT)
.map_err(|e| VotingError::Internal {
message: format!("failed to configure database busy timeout: {}", e),
})?;
Comment on lines +90 to +93

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: this is a drive-by from code reviews, stems from adding concurrency

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was a bit worried about this change because it applies globally. But in manual tests, did not observe any issues.

Mentioning for reviewer visibility

conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")
.map_err(|e| VotingError::Internal {
message: format!("failed to set pragmas: {}", e),
Expand Down Expand Up @@ -154,6 +160,43 @@ mod tests {
assert_eq!(version, 14);
}

#[test]
fn writes_wait_for_a_transient_external_writer() {
let unique = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let path = std::env::temp_dir().join(format!(
"zcash-voting-busy-timeout-{}-{unique}.sqlite",
std::process::id()
));
let path_string = path.to_string_lossy().into_owned();
let db = VotingDb::open(&path_string).unwrap();
db.conn()
.execute_batch("CREATE TABLE busy_timeout_probe (value INTEGER NOT NULL)")
.unwrap();

let lock = Connection::open(&path).unwrap();
lock.busy_timeout(SQLITE_BUSY_TIMEOUT).unwrap();
lock.execute_batch("BEGIN IMMEDIATE").unwrap();
let release = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(400));
lock.execute_batch("ROLLBACK").unwrap();
});

let started = std::time::Instant::now();
db.conn()
.execute("INSERT INTO busy_timeout_probe (value) VALUES (1)", [])
.unwrap();
assert!(started.elapsed() >= Duration::from_millis(300));

release.join().unwrap();
drop(db);
let _ = std::fs::remove_file(&path);
let _ = std::fs::remove_file(format!("{path_string}-shm"));
let _ = std::fs::remove_file(format!("{path_string}-wal"));
}

#[test]
fn test_round_lifecycle() {
let db = test_db();
Expand Down
102 changes: 63 additions & 39 deletions zcash_voting/src/storage/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ use crate::types::{
VoteCommitmentBundle, VotingError, VotingRoundParams, WireEncryptedShare, WitnessData,
};

pub(crate) struct PreparedVoteProof {
pub wallet_id: String,
pub bundle: VoteCommitmentBundle,
pub state: queries::VotePreparationState,
}

fn nullifier_bytes_to_base(bytes: &[u8], label: &str) -> Result<pallas::Base, VotingError> {
let nf_bytes: [u8; 32] = bytes.try_into().map_err(|_| VotingError::Internal {
message: format!("{label} nullifier must be 32 bytes, got {}", bytes.len()),
Expand Down Expand Up @@ -1087,7 +1093,7 @@ impl VotingDb {

// --- Phase 3: Voting ---

/// Build vote commitment + ZKP #2 for a proposal. Stores vote in db.
/// Capture vote state, release SQLite, then build vote commitment + ZKP #2.
///
/// Loads ZKP #2 inputs (gov_comm_rand, total_note_value, address_index, ea_pk,
/// voting_round_id) from the DB, derives the SpendingKey from hotkey_seed
Expand All @@ -1096,7 +1102,7 @@ impl VotingDb {
///
/// The builder handles share decomposition and El Gamal encryption internally.
/// The returned bundle includes the encrypted shares for reveal-share payloads.
pub(crate) fn build_vote_commitment(
pub(crate) fn prepare_vote_commitment(
&self,
round_id: &str,
bundle_index: u32,
Expand All @@ -1110,63 +1116,80 @@ impl VotingDb {
anchor_height: u32,
single_share: bool,
progress: &dyn ProgressReporter,
) -> Result<VoteCommitmentBundle, VotingError> {
let conn = self.conn();
) -> Result<PreparedVoteProof, VotingError> {
let mut conn = self.conn();
let wallet_id = self.wallet_id();
let stored_network = queries::load_round_network(&conn, round_id, &wallet_id)?;
let tx = conn.transaction().map_err(|e| VotingError::Internal {
message: format!("failed to begin vote preparation transaction: {e}"),
})?;
// Check the signer's network before loading the rest of the state. Capturing
// state first makes a mismatched network surface as a missing-row error from
// the ZKP2 lookup, which hides the real cause from the caller.
let stored_network = queries::load_round_network(&tx, round_id, &wallet_id)?;
validate_network_matches_round(stored_network, signer_network, "vote signer")?;
let zkp2_data = queries::load_zkp2_inputs(&conn, round_id, &wallet_id, bundle_index)?;
let state = queries::load_vote_preparation_state(
&tx,
round_id,
&wallet_id,
bundle_index,
proposal_id,
)?;
tx.commit().map_err(|e| VotingError::Internal {
message: format!("failed to finish vote preparation transaction: {e}"),
})?;
drop(conn);

if van_position != state.van_position {
return Err(VotingError::InvalidInput {
message: format!(
"VAN witness position {van_position} does not match current bundle position {} for round={round_id}, bundle={bundle_index}",
state.van_position
),
});
}
if let Some((skipped, intent_choice)) = state.ballot_intent {
if skipped || intent_choice != Some(choice) {
return Err(VotingError::InvalidInput {
message: format!(
"vote draft conflicts with current ballot intent for round={round_id}, bundle={bundle_index}, proposal={proposal_id}"
),
});
}
}

// Decode voting_round_id from hex string to 32 bytes
let voting_round_id_bytes =
hex::decode(&zkp2_data.voting_round_id).map_err(|e| VotingError::Internal {
hex::decode(&state.zkp2.voting_round_id).map_err(|e| VotingError::Internal {
message: format!(
"invalid voting_round_id hex '{}': {e}",
zkp2_data.voting_round_id
state.zkp2.voting_round_id
),
})?;

let bundle = crate::zkp2::build_vote_commitment(
hotkey_seed,
stored_network,
zkp2_data.address_index,
zkp2_data.total_note_value,
&zkp2_data.gov_comm_rand,
state.network,
state.zkp2.address_index,
state.zkp2.total_note_value,
&state.zkp2.gov_comm_rand,
&voting_round_id_bytes,
&zkp2_data.ea_pk,
&state.zkp2.ea_pk,
proposal_id,
choice,
num_options,
van_auth_path,
van_position,
anchor_height,
zkp2_data.proposal_authority,
state.zkp2.proposal_authority,
single_share,
progress,
)?;

// Store the vote commitment as serialized bytes
let commitment_bytes = serde_json::to_vec(&serde_json::json!({
"van_nullifier": hex::encode(&bundle.van_nullifier),
"vote_authority_note_new": hex::encode(&bundle.vote_authority_note_new),
"vote_commitment": hex::encode(&bundle.vote_commitment),
"proof": hex::encode(&bundle.proof),
}))
.map_err(|e| VotingError::Internal {
message: format!("failed to serialize vote commitment: {}", e),
})?;

queries::store_vote(
&conn,
round_id,
&wallet_id,
bundle_index,
proposal_id,
choice,
&commitment_bytes,
)?;
queries::advance_round_phase(&conn, round_id, &wallet_id, RoundPhase::VoteReady)?;
Ok(bundle)
Ok(PreparedVoteProof {
wallet_id,
bundle,
state,
})
}

/// Build share payloads for helper server delegation.
Expand Down Expand Up @@ -2403,13 +2426,13 @@ mod tests {
}

#[test]
fn test_build_vote_commitment_rejects_network_mismatch_before_zkp2_inputs() {
fn test_prepare_vote_commitment_rejects_network_mismatch_before_zkp2_inputs() {
let db = test_db();
db.init_round(Network::Testnet, &test_params(), None)
.unwrap();

let err = db
.build_vote_commitment(
.prepare_vote_commitment(
ROUND_ID,
0,
&[0x99; 64],
Expand All @@ -2423,7 +2446,8 @@ mod tests {
false,
&crate::types::NoopProgressReporter,
)
.unwrap_err();
.err()
.expect("network mismatch must fail");

assert!(
err.to_string().contains(
Expand Down
Loading
Loading