From 30388a254582458a5bbf872b58a870eaf3759ac1 Mon Sep 17 00:00:00 2001 From: Adam Tucker Date: Sun, 15 Mar 2026 14:14:27 -0600 Subject: [PATCH] feat: add wallet_id column for per-wallet state isolation Add wallet_id to all voting DB tables so round state is natively scoped per wallet when both hotkey and keystone are loaded. - Schema: wallet_id TEXT NOT NULL DEFAULT '' added to rounds (PK becomes (round_id, wallet_id)), bundles, cached_tree_state, proofs, witnesses, votes with cascading FKs - VotingDb: stores wallet_id on the struct, set once via set_wallet_id(). All operations pass it internally to queries. No public API signature changes. - Migration: v4 drop-and-recreate - Tests: wallet isolation test verifies two wallets in same round don't cross-contaminate --- librustvoting/src/storage/migrations.rs | 30 ++- .../src/storage/migrations/001_init.sql | 34 ++- librustvoting/src/storage/mod.rs | 109 ++++++--- librustvoting/src/storage/operations.rs | 115 +++++---- librustvoting/src/storage/queries.rs | 225 ++++++++++-------- 5 files changed, 324 insertions(+), 189 deletions(-) diff --git a/librustvoting/src/storage/migrations.rs b/librustvoting/src/storage/migrations.rs index 889ac9d48..ef3e5d02a 100644 --- a/librustvoting/src/storage/migrations.rs +++ b/librustvoting/src/storage/migrations.rs @@ -2,7 +2,7 @@ use rusqlite::Connection; use crate::VotingError; -const CURRENT_VERSION: u32 = 3; +const CURRENT_VERSION: u32 = 4; pub fn migrate(conn: &Connection) -> Result<(), VotingError> { let version: u32 = conn @@ -75,6 +75,30 @@ pub fn migrate(conn: &Connection) -> Result<(), VotingError> { })?; } + if version < 4 { + // v4: add wallet_id column for per-wallet state isolation. + // Drop everything and recreate from 001_init.sql. + conn.execute_batch( + "DROP TABLE IF EXISTS votes; + DROP TABLE IF EXISTS witnesses; + DROP TABLE IF EXISTS proofs; + DROP TABLE IF EXISTS bundles; + DROP TABLE IF EXISTS cached_tree_state; + DROP TABLE IF EXISTS rounds;" + ) + .map_err(|e| VotingError::Internal { + message: format!("migration to version 4 failed (drop): {}", e), + })?; + conn.execute_batch(include_str!("migrations/001_init.sql")) + .map_err(|e| VotingError::Internal { + message: format!("migration to version 4 failed (create): {}", e), + })?; + conn.pragma_update(None, "user_version", 4) + .map_err(|e| VotingError::Internal { + message: format!("failed to update database version: {}", e), + })?; + } + let final_version: u32 = conn .pragma_query_value(None, "user_version", |r| r.get(0)) .map_err(|e| VotingError::Internal { @@ -148,13 +172,13 @@ mod tests { // Insert a round first conn.execute( - "INSERT INTO rounds (round_id, snapshot_height, ea_pk, nc_root, nullifier_imt_root, phase, created_at) VALUES ('test', 1, X'00', X'00', X'00', 0, 0)", + "INSERT INTO rounds (round_id, wallet_id, snapshot_height, ea_pk, nc_root, nullifier_imt_root, phase, created_at) VALUES ('test', 'w1', 1, X'00', X'00', X'00', 0, 0)", [], ).unwrap(); // Insert a bundle row using all nullable BLOB columns. conn.execute( - "INSERT INTO bundles (round_id, bundle_index, van_comm_rand, dummy_nullifiers, rho_signed, padded_note_data, nf_signed, cmx_new, alpha, rseed_signed, rseed_output) VALUES ('test', 0, X'AA', X'BB', X'CC', X'DD', X'EE', X'FF', X'11', X'22', X'33')", + "INSERT INTO bundles (round_id, wallet_id, bundle_index, van_comm_rand, dummy_nullifiers, rho_signed, padded_note_data, nf_signed, cmx_new, alpha, rseed_signed, rseed_output) VALUES ('test', 'w1', 0, X'AA', X'BB', X'CC', X'DD', X'EE', X'FF', X'11', X'22', X'33')", [], ).unwrap(); diff --git a/librustvoting/src/storage/migrations/001_init.sql b/librustvoting/src/storage/migrations/001_init.sql index 0a662f1ee..e3e631576 100644 --- a/librustvoting/src/storage/migrations/001_init.sql +++ b/librustvoting/src/storage/migrations/001_init.sql @@ -1,16 +1,19 @@ CREATE TABLE rounds ( - round_id TEXT PRIMARY KEY, + round_id TEXT NOT NULL, + wallet_id TEXT NOT NULL DEFAULT '', snapshot_height INTEGER NOT NULL, ea_pk BLOB NOT NULL, nc_root BLOB NOT NULL, nullifier_imt_root BLOB NOT NULL, session_json TEXT, phase INTEGER NOT NULL DEFAULT 0, - created_at INTEGER NOT NULL + created_at INTEGER NOT NULL, + PRIMARY KEY (round_id, wallet_id) ); CREATE TABLE bundles ( - round_id TEXT NOT NULL REFERENCES rounds(round_id) ON DELETE CASCADE, + round_id TEXT NOT NULL, + wallet_id TEXT NOT NULL DEFAULT '', bundle_index INTEGER NOT NULL, note_positions_blob BLOB, van_comm_rand BLOB, @@ -30,47 +33,54 @@ CREATE TABLE bundles ( gov_nullifiers_blob BLOB, padded_note_secrets BLOB, pczt_sighash BLOB, - PRIMARY KEY (round_id, bundle_index) + PRIMARY KEY (round_id, wallet_id, bundle_index), + FOREIGN KEY (round_id, wallet_id) REFERENCES rounds(round_id, wallet_id) ON DELETE CASCADE ); CREATE TABLE cached_tree_state ( - round_id TEXT PRIMARY KEY REFERENCES rounds(round_id) ON DELETE CASCADE, + round_id TEXT NOT NULL, + wallet_id TEXT NOT NULL DEFAULT '', snapshot_height INTEGER NOT NULL, - tree_state BLOB NOT NULL + tree_state BLOB NOT NULL, + PRIMARY KEY (round_id, wallet_id), + FOREIGN KEY (round_id, wallet_id) REFERENCES rounds(round_id, wallet_id) ON DELETE CASCADE ); CREATE TABLE proofs ( round_id TEXT NOT NULL, + wallet_id TEXT NOT NULL DEFAULT '', bundle_index INTEGER NOT NULL, witness BLOB, proof BLOB, success INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, - PRIMARY KEY (round_id, bundle_index), - FOREIGN KEY (round_id, bundle_index) REFERENCES bundles(round_id, bundle_index) ON DELETE CASCADE + PRIMARY KEY (round_id, wallet_id, bundle_index), + FOREIGN KEY (round_id, wallet_id, bundle_index) REFERENCES bundles(round_id, wallet_id, bundle_index) ON DELETE CASCADE ); CREATE TABLE witnesses ( round_id TEXT NOT NULL, + wallet_id TEXT NOT NULL DEFAULT '', bundle_index INTEGER NOT NULL, note_position INTEGER NOT NULL, note_commitment BLOB NOT NULL, root BLOB NOT NULL, auth_path BLOB NOT NULL, created_at INTEGER NOT NULL, - PRIMARY KEY (round_id, bundle_index, note_position), - FOREIGN KEY (round_id, bundle_index) REFERENCES bundles(round_id, bundle_index) ON DELETE CASCADE + PRIMARY KEY (round_id, wallet_id, bundle_index, note_position), + FOREIGN KEY (round_id, wallet_id, bundle_index) REFERENCES bundles(round_id, wallet_id, bundle_index) ON DELETE CASCADE ); CREATE TABLE votes ( id INTEGER PRIMARY KEY, round_id TEXT NOT NULL, + wallet_id TEXT NOT NULL DEFAULT '', bundle_index INTEGER NOT NULL, proposal_id INTEGER NOT NULL, choice INTEGER NOT NULL, commitment BLOB, submitted INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, - UNIQUE(round_id, bundle_index, proposal_id), - FOREIGN KEY (round_id, bundle_index) REFERENCES bundles(round_id, bundle_index) ON DELETE CASCADE + UNIQUE(round_id, wallet_id, bundle_index, proposal_id), + FOREIGN KEY (round_id, wallet_id, bundle_index) REFERENCES bundles(round_id, wallet_id, bundle_index) ON DELETE CASCADE ); diff --git a/librustvoting/src/storage/mod.rs b/librustvoting/src/storage/mod.rs index 8417af64a..a6ea04d51 100644 --- a/librustvoting/src/storage/mod.rs +++ b/librustvoting/src/storage/mod.rs @@ -56,19 +56,23 @@ pub struct VoteRecord { #[derive(Clone, Debug)] pub struct RoundSummary { pub round_id: String, + pub wallet_id: String, pub phase: RoundPhase, pub snapshot_height: u64, pub created_at: u64, } -/// Database handle for voting state. Wraps a SQLite connection. +/// Database handle for voting state. Wraps a SQLite connection and a +/// wallet identifier that scopes all round data to a single wallet. pub struct VotingDb { conn: Mutex, + wallet_id: Mutex, } impl VotingDb { /// Open (or create) the voting database at the given path. /// Runs migrations automatically. + /// Call `set_wallet_id` before performing any round operations. pub fn open(path: &str) -> Result { let conn = if path == ":memory:" { Connection::open_in_memory() @@ -88,9 +92,22 @@ impl VotingDb { Ok(Self { conn: Mutex::new(conn), + wallet_id: Mutex::new(String::new()), }) } + /// Set the wallet identifier used to scope all subsequent operations. + pub fn set_wallet_id(&self, id: &str) { + *self.wallet_id.lock().expect("wallet_id mutex poisoned") = id.to_string(); + } + + /// Get the current wallet identifier. Panics if not set. + pub fn wallet_id(&self) -> String { + let id = self.wallet_id.lock().expect("wallet_id mutex poisoned").clone(); + assert!(!id.is_empty(), "wallet_id must be set before performing voting operations"); + id + } + /// Get a lock on the underlying connection for query execution. pub fn conn(&self) -> std::sync::MutexGuard<'_, Connection> { self.conn.lock().expect("database mutex poisoned") @@ -102,6 +119,8 @@ mod tests { use super::*; use crate::types::VotingRoundParams; + const W: &str = "test-wallet"; + fn test_db() -> VotingDb { VotingDb::open(":memory:").unwrap() } @@ -123,7 +142,7 @@ mod tests { let version: u32 = conn .pragma_query_value(None, "user_version", |r| r.get(0)) .unwrap(); - assert_eq!(version, 3); + assert_eq!(version, 4); } #[test] @@ -132,19 +151,19 @@ mod tests { let conn = db.conn(); let params = test_params(); - queries::insert_round(&conn, ¶ms, None).unwrap(); + queries::insert_round(&conn, W, ¶ms, None).unwrap(); - let state = queries::get_round_state(&conn, "test-round-1").unwrap(); + let state = queries::get_round_state(&conn, "test-round-1", W).unwrap(); assert_eq!(state.phase, RoundPhase::Initialized); assert_eq!(state.snapshot_height, 1000); assert!(!state.proof_generated); - let rounds = queries::list_rounds(&conn).unwrap(); + let rounds = queries::list_rounds(&conn, W).unwrap(); assert_eq!(rounds.len(), 1); assert_eq!(rounds[0].round_id, "test-round-1"); - queries::clear_round(&conn, "test-round-1").unwrap(); - let rounds = queries::list_rounds(&conn).unwrap(); + queries::clear_round(&conn, "test-round-1", W).unwrap(); + let rounds = queries::list_rounds(&conn, W).unwrap(); assert!(rounds.is_empty()); } @@ -152,12 +171,12 @@ mod tests { fn test_tree_state_cache() { let db = test_db(); let conn = db.conn(); - queries::insert_round(&conn, &test_params(), None).unwrap(); + queries::insert_round(&conn, W, &test_params(), None).unwrap(); let tree_state = vec![0xCC; 1024]; - queries::store_tree_state(&conn, "test-round-1", 1000, &tree_state).unwrap(); + queries::store_tree_state(&conn, "test-round-1", W, 1000, &tree_state).unwrap(); - let loaded = queries::load_tree_state(&conn, "test-round-1").unwrap(); + let loaded = queries::load_tree_state(&conn, "test-round-1", W).unwrap(); assert_eq!(loaded, tree_state); } @@ -165,16 +184,15 @@ mod tests { fn test_proof_storage() { let db = test_db(); let conn = db.conn(); - queries::insert_round(&conn, &test_params(), None).unwrap(); - queries::insert_bundle(&conn, "test-round-1", 0, &[]).unwrap(); - queries::store_proof(&conn, "test-round-1", 0, &vec![0xAB; 256]).unwrap(); + queries::insert_round(&conn, W, &test_params(), None).unwrap(); + queries::insert_bundle(&conn, "test-round-1", W, 0, &[]).unwrap(); + queries::store_proof(&conn, "test-round-1", W, 0, &vec![0xAB; 256]).unwrap(); - // proof_generated requires both proof AND VAN position - let state = queries::get_round_state(&conn, "test-round-1").unwrap(); + let state = queries::get_round_state(&conn, "test-round-1", W).unwrap(); assert!(!state.proof_generated, "proof alone should not be enough"); - queries::store_van_position(&conn, "test-round-1", 0, 42).unwrap(); - let state = queries::get_round_state(&conn, "test-round-1").unwrap(); + queries::store_van_position(&conn, "test-round-1", W, 0, 42).unwrap(); + let state = queries::get_round_state(&conn, "test-round-1", W).unwrap(); assert!(state.proof_generated, "proof + VAN position should be enough"); } @@ -182,33 +200,31 @@ mod tests { fn test_vote_storage() { let db = test_db(); let conn = db.conn(); - queries::insert_round(&conn, &test_params(), None).unwrap(); - queries::insert_bundle(&conn, "test-round-1", 0, &[]).unwrap(); + queries::insert_round(&conn, W, &test_params(), None).unwrap(); + queries::insert_bundle(&conn, "test-round-1", W, 0, &[]).unwrap(); let commitment = vec![0xCC; 128]; - queries::store_vote(&conn, "test-round-1", 0, 0, 0, &commitment).unwrap(); - queries::store_vote(&conn, "test-round-1", 0, 1, 1, &commitment).unwrap(); + queries::store_vote(&conn, "test-round-1", W, 0, 0, 0, &commitment).unwrap(); + queries::store_vote(&conn, "test-round-1", W, 0, 1, 1, &commitment).unwrap(); - queries::mark_vote_submitted(&conn, "test-round-1", 0, 0).unwrap(); + queries::mark_vote_submitted(&conn, "test-round-1", W, 0, 0).unwrap(); } #[test] fn test_get_votes() { let db = test_db(); let conn = db.conn(); - queries::insert_round(&conn, &test_params(), None).unwrap(); - queries::insert_bundle(&conn, "test-round-1", 0, &[]).unwrap(); + queries::insert_round(&conn, W, &test_params(), None).unwrap(); + queries::insert_bundle(&conn, "test-round-1", W, 0, &[]).unwrap(); - // No votes initially - let votes = queries::get_votes(&conn, "test-round-1").unwrap(); + let votes = queries::get_votes(&conn, "test-round-1", W).unwrap(); assert!(votes.is_empty()); - // Store two votes with different choices let commitment = vec![0xCC; 128]; - queries::store_vote(&conn, "test-round-1", 0, 0, 0, &commitment).unwrap(); - queries::store_vote(&conn, "test-round-1", 0, 1, 2, &commitment).unwrap(); + queries::store_vote(&conn, "test-round-1", W, 0, 0, 0, &commitment).unwrap(); + queries::store_vote(&conn, "test-round-1", W, 0, 1, 2, &commitment).unwrap(); - let votes = queries::get_votes(&conn, "test-round-1").unwrap(); + let votes = queries::get_votes(&conn, "test-round-1", W).unwrap(); assert_eq!(votes.len(), 2); assert_eq!(votes[0].proposal_id, 0); assert_eq!(votes[0].choice, 0); @@ -216,10 +232,37 @@ mod tests { assert_eq!(votes[1].proposal_id, 1); assert_eq!(votes[1].choice, 2); - // Mark first vote submitted and verify - queries::mark_vote_submitted(&conn, "test-round-1", 0, 0).unwrap(); - let votes = queries::get_votes(&conn, "test-round-1").unwrap(); + queries::mark_vote_submitted(&conn, "test-round-1", W, 0, 0).unwrap(); + let votes = queries::get_votes(&conn, "test-round-1", W).unwrap(); assert!(votes[0].submitted); assert!(!votes[1].submitted); } + + #[test] + fn test_wallet_isolation() { + let db = test_db(); + let conn = db.conn(); + let params = test_params(); + + queries::insert_round(&conn, "wallet-a", ¶ms, None).unwrap(); + queries::insert_round(&conn, "wallet-b", ¶ms, None).unwrap(); + + queries::insert_bundle(&conn, "test-round-1", "wallet-a", 0, &[]).unwrap(); + queries::insert_bundle(&conn, "test-round-1", "wallet-b", 0, &[]).unwrap(); + + let commitment = vec![0xCC; 128]; + queries::store_vote(&conn, "test-round-1", "wallet-a", 0, 0, 1, &commitment).unwrap(); + queries::store_vote(&conn, "test-round-1", "wallet-b", 0, 0, 2, &commitment).unwrap(); + + let votes_a = queries::get_votes(&conn, "test-round-1", "wallet-a").unwrap(); + let votes_b = queries::get_votes(&conn, "test-round-1", "wallet-b").unwrap(); + assert_eq!(votes_a.len(), 1); + assert_eq!(votes_b.len(), 1); + assert_eq!(votes_a[0].choice, 1); + assert_eq!(votes_b[0].choice, 2); + + queries::clear_round(&conn, "test-round-1", "wallet-a").unwrap(); + let rounds_b = queries::list_rounds(&conn, "wallet-b").unwrap(); + assert_eq!(rounds_b.len(), 1, "wallet-b round should survive wallet-a clear"); + } } diff --git a/librustvoting/src/storage/operations.rs b/librustvoting/src/storage/operations.rs index 73a070256..a95d7d97b 100644 --- a/librustvoting/src/storage/operations.rs +++ b/librustvoting/src/storage/operations.rs @@ -20,31 +20,36 @@ impl VotingDb { session_json: Option<&str>, ) -> Result<(), VotingError> { let conn = self.conn(); - queries::insert_round(&conn, params, session_json) + let wallet_id = self.wallet_id(); + queries::insert_round(&conn, &wallet_id, params, session_json) } /// Get the current state of a voting round. pub fn get_round_state(&self, round_id: &str) -> Result { let conn = self.conn(); - queries::get_round_state(&conn, round_id) + let wallet_id = self.wallet_id(); + queries::get_round_state(&conn, round_id, &wallet_id) } /// List all rounds. pub fn list_rounds(&self) -> Result, VotingError> { let conn = self.conn(); - queries::list_rounds(&conn) + let wallet_id = self.wallet_id(); + queries::list_rounds(&conn, &wallet_id) } /// Get all votes for a round (with choice, bundle_index, and submitted status). pub fn get_votes(&self, round_id: &str) -> Result, VotingError> { let conn = self.conn(); - queries::get_votes(&conn, round_id) + let wallet_id = self.wallet_id(); + queries::get_votes(&conn, round_id, &wallet_id) } /// Delete all data for a round. pub fn clear_round(&self, round_id: &str) -> Result<(), VotingError> { let conn = self.conn(); - queries::clear_round(&conn, round_id) + let wallet_id = self.wallet_id(); + queries::clear_round(&conn, round_id, &wallet_id) } // --- Bundles --- @@ -54,6 +59,7 @@ impl VotingDb { /// threshold are created. Notes in sub-threshold bundles are dropped. pub fn setup_bundles(&self, round_id: &str, notes: &[NoteInfo]) -> Result<(u32, u64), VotingError> { let conn = self.conn(); + let wallet_id = self.wallet_id(); let result = crate::types::chunk_notes(notes); if result.dropped_count > 0 { eprintln!( @@ -65,7 +71,7 @@ impl VotingDb { } for (i, chunk) in result.bundles.iter().enumerate() { let positions: Vec = chunk.iter().map(|n| n.position).collect(); - queries::insert_bundle(&conn, round_id, i as u32, &positions)?; + queries::insert_bundle(&conn, round_id, &wallet_id, i as u32, &positions)?; } Ok((result.bundles.len() as u32, result.eligible_weight)) } @@ -73,7 +79,8 @@ impl VotingDb { /// Get the number of bundles for a round. pub fn get_bundle_count(&self, round_id: &str) -> Result { let conn = self.conn(); - queries::get_bundle_count(&conn, round_id) + let wallet_id = self.wallet_id(); + queries::get_bundle_count(&conn, round_id, &wallet_id) } // --- Phase 1: Delegation setup --- @@ -114,7 +121,8 @@ impl VotingDb { address_index: u32, ) -> Result { let conn = self.conn(); - let params = queries::load_round_params(&conn, round_id)?; + let wallet_id = self.wallet_id(); + let params = queries::load_round_params(&conn, round_id, &wallet_id)?; let result = crate::action::build_governance_pczt( notes, ¶ms, @@ -138,6 +146,7 @@ impl VotingDb { queries::store_delegation_data( &conn, round_id, + &wallet_id, bundle_index, &result.van_comm_rand, &result.dummy_nullifiers, @@ -160,8 +169,9 @@ impl VotingDb { /// Cache tree state fetched from lightwalletd by SDK. pub fn store_tree_state(&self, round_id: &str, tree_state: &[u8]) -> Result<(), VotingError> { let conn = self.conn(); - let params = queries::load_round_params(&conn, round_id)?; - queries::store_tree_state(&conn, round_id, params.snapshot_height, tree_state) + let wallet_id = self.wallet_id(); + let params = queries::load_round_params(&conn, round_id, &wallet_id)?; + queries::store_tree_state(&conn, round_id, &wallet_id, params.snapshot_height, tree_state) } /// Verify and cache Merkle inclusion witnesses for notes in a bundle. @@ -177,9 +187,10 @@ impl VotingDb { witnesses: &[WitnessData], ) -> Result<(), VotingError> { let conn = self.conn(); + let wallet_id = self.wallet_id(); // Return early if already cached - if queries::has_witnesses(&conn, round_id, bundle_index)? { + if queries::has_witnesses(&conn, round_id, &wallet_id, bundle_index)? { return Ok(()); } @@ -197,7 +208,7 @@ impl VotingDb { } // Cache results - queries::store_witnesses(&conn, round_id, bundle_index, witnesses)?; + queries::store_witnesses(&conn, round_id, &wallet_id, bundle_index, witnesses)?; Ok(()) } @@ -233,16 +244,17 @@ impl VotingDb { // Phase 1: DB queries let db_start = std::time::Instant::now(); let conn = self.conn(); - let params = queries::load_round_params(&conn, round_id)?; - let alpha = queries::load_alpha(&conn, round_id, bundle_index)?; - let van_comm_rand = queries::load_van_comm_rand(&conn, round_id, bundle_index)?; - let witnesses = queries::load_witnesses(&conn, round_id, bundle_index)?; + let wallet_id = self.wallet_id(); + let params = queries::load_round_params(&conn, round_id, &wallet_id)?; + let alpha = queries::load_alpha(&conn, round_id, &wallet_id, bundle_index)?; + let van_comm_rand = queries::load_van_comm_rand(&conn, round_id, &wallet_id, bundle_index)?; + let witnesses = queries::load_witnesses(&conn, round_id, &wallet_id, bundle_index)?; // Load Phase 1 randomness for ZCA-74 fix: ensures Phase 2 produces // the same nf_signed/cmx_new that Phase 1 committed to in the PCZT. - let rseed_signed = queries::load_rseed_signed(&conn, round_id, bundle_index)?; - let rseed_output = queries::load_rseed_output(&conn, round_id, bundle_index)?; - let padded_secrets = queries::load_padded_note_secrets(&conn, round_id, bundle_index)?; + let rseed_signed = queries::load_rseed_signed(&conn, round_id, &wallet_id, bundle_index)?; + let rseed_output = queries::load_rseed_output(&conn, round_id, &wallet_id, bundle_index)?; + let padded_secrets = queries::load_padded_note_secrets(&conn, round_id, &wallet_id, bundle_index)?; // Align witnesses (keyed by commitment) to notes order let witness_count = witnesses.len(); @@ -407,20 +419,21 @@ impl VotingDb { ); // Store proof bytes for debugging/recovery - queries::store_proof(&conn, round_id, bundle_index, &result.proof)?; + queries::store_proof(&conn, round_id, &wallet_id, bundle_index, &result.proof)?; // Persist prover's public inputs — needed later for delegation TX submission. // With PrecomputedRandomness (ZCA-74 fix), nf_signed/cmx_new should match // Phase 1 values. We still store them to be explicit and support the legacy path. queries::store_proof_result_fields( &conn, round_id, + &wallet_id, bundle_index, &result.rk, &result.gov_nullifiers, &result.nf_signed, &result.cmx_new, )?; - queries::update_round_phase(&conn, round_id, RoundPhase::DelegationProved)?; + queries::update_round_phase(&conn, round_id, &wallet_id, RoundPhase::DelegationProved)?; let total_elapsed = total_start.elapsed(); eprintln!( @@ -444,7 +457,8 @@ impl VotingDb { shares: &[u64], ) -> Result, VotingError> { let conn = self.conn(); - let params = queries::load_round_params(&conn, round_id)?; + let wallet_id = self.wallet_id(); + let params = queries::load_round_params(&conn, round_id, &wallet_id)?; crate::elgamal::encrypt_shares(shares, ¶ms.ea_pk) } @@ -471,7 +485,8 @@ impl VotingDb { progress: &dyn ProofProgressReporter, ) -> Result { let conn = self.conn(); - let zkp2_data = queries::load_zkp2_inputs(&conn, round_id, bundle_index)?; + let wallet_id = self.wallet_id(); + let zkp2_data = queries::load_zkp2_inputs(&conn, round_id, &wallet_id, bundle_index)?; // Decode voting_round_id from hex string to 32 bytes let voting_round_id_bytes = @@ -514,12 +529,13 @@ impl VotingDb { queries::store_vote( &conn, round_id, + &wallet_id, bundle_index, proposal_id, choice, &commitment_bytes, )?; - queries::update_round_phase(&conn, round_id, RoundPhase::VoteReady)?; + queries::update_round_phase(&conn, round_id, &wallet_id, RoundPhase::VoteReady)?; Ok(bundle) } @@ -555,13 +571,15 @@ impl VotingDb { position: u32, ) -> Result<(), VotingError> { let conn = self.conn(); - queries::store_van_position(&conn, round_id, bundle_index, position) + let wallet_id = self.wallet_id(); + queries::store_van_position(&conn, round_id, &wallet_id, bundle_index, position) } /// Load the VAN leaf position for a bundle. pub fn load_van_position(&self, round_id: &str, bundle_index: u32) -> Result { let conn = self.conn(); - queries::load_van_position(&conn, round_id, bundle_index) + let wallet_id = self.wallet_id(); + queries::load_van_position(&conn, round_id, &wallet_id, bundle_index) } /// Reconstruct the full chain-ready delegation TX payload from DB + seed. @@ -580,8 +598,9 @@ impl VotingDb { _account_index: u32, ) -> Result { let conn = self.conn(); - let data = queries::load_delegation_submission_data(&conn, round_id, bundle_index)?; - let sighash_vec = queries::load_pczt_sighash(&conn, round_id, bundle_index)?; + let wallet_id = self.wallet_id(); + let data = queries::load_delegation_submission_data(&conn, round_id, &wallet_id, bundle_index)?; + let sighash_vec = queries::load_pczt_sighash(&conn, round_id, &wallet_id, bundle_index)?; drop(conn); let sighash: [u8; 32] = @@ -666,7 +685,8 @@ impl VotingDb { } let conn = self.conn(); - let data = queries::load_delegation_submission_data(&conn, round_id, bundle_index)?; + let wallet_id = self.wallet_id(); + let data = queries::load_delegation_submission_data(&conn, round_id, &wallet_id, bundle_index)?; Ok(DelegationSubmissionData { proof: data.proof, @@ -687,7 +707,8 @@ impl VotingDb { /// Returns the number of deleted rows. pub fn delete_skipped_bundles(&self, round_id: &str, keep_count: u32) -> Result { let conn = self.conn(); - queries::delete_bundles_from(&conn, round_id, keep_count) + let wallet_id = self.wallet_id(); + queries::delete_bundles_from(&conn, round_id, &wallet_id, keep_count) } /// Mark a vote as submitted to the vote chain. @@ -698,7 +719,8 @@ impl VotingDb { proposal_id: u32, ) -> Result<(), VotingError> { let conn = self.conn(); - queries::mark_vote_submitted(&conn, round_id, bundle_index, proposal_id) + let wallet_id = self.wallet_id(); + queries::mark_vote_submitted(&conn, round_id, &wallet_id, bundle_index, proposal_id) } } @@ -709,9 +731,12 @@ mod tests { // 64 hex chars = 32 bytes when decoded. Required because build_governance_pczt // hex-decodes vote_round_id and validates it as exactly 32 bytes (a Pallas field element). const ROUND_ID: &str = "0101010101010101010101010101010101010101010101010101010101010101"; + const W: &str = "test-wallet"; fn test_db() -> VotingDb { - VotingDb::open(":memory:").unwrap() + let db = VotingDb::open(":memory:").unwrap(); + db.set_wallet_id(W); + db } fn test_params() -> VotingRoundParams { @@ -794,7 +819,7 @@ mod tests { db.store_tree_state(ROUND_ID, &tree_state).unwrap(); let conn = db.conn(); - let loaded = queries::load_tree_state(&conn, ROUND_ID).unwrap(); + let loaded = queries::load_tree_state(&conn, ROUND_ID, W).unwrap(); assert_eq!(loaded, tree_state); } @@ -830,7 +855,7 @@ mod tests { ) .unwrap(); - queries::store_vote(&db.conn(), ROUND_ID, 0, 0, 0, &[0xAA; 32]).unwrap(); + queries::store_vote(&db.conn(), ROUND_ID, W, 0, 0, 0, &[0xAA; 32]).unwrap(); db.mark_vote_submitted(ROUND_ID, 0, 0).unwrap(); } @@ -873,9 +898,9 @@ mod tests { // Verify note positions per bundle (sequential fill) let conn = db.conn(); - let positions_0 = queries::load_bundle_note_positions(&conn, ROUND_ID, 0).unwrap(); + let positions_0 = queries::load_bundle_note_positions(&conn, ROUND_ID, W, 0).unwrap(); assert_eq!(positions_0, vec![0, 1, 2, 3, 4]); - let positions_1 = queries::load_bundle_note_positions(&conn, ROUND_ID, 1).unwrap(); + let positions_1 = queries::load_bundle_note_positions(&conn, ROUND_ID, W, 1).unwrap(); assert_eq!(positions_1, vec![5]); drop(conn); @@ -917,13 +942,13 @@ mod tests { // Verify data persisted per bundle let conn = db.conn(); - let stored_rand = queries::load_van_comm_rand(&conn, ROUND_ID, i as u32).unwrap(); + let stored_rand = queries::load_van_comm_rand(&conn, ROUND_ID, W, i as u32).unwrap(); assert_eq!(stored_rand, result.van_comm_rand); - let stored_alpha = queries::load_alpha(&conn, ROUND_ID, i as u32).unwrap(); + let stored_alpha = queries::load_alpha(&conn, ROUND_ID, W, i as u32).unwrap(); assert_eq!(stored_alpha, result.alpha); // ZKP2 inputs loadable per bundle - let zkp2 = queries::load_zkp2_inputs(&conn, ROUND_ID, i as u32).unwrap(); + let zkp2 = queries::load_zkp2_inputs(&conn, ROUND_ID, W, i as u32).unwrap(); assert_eq!(zkp2.gov_comm_rand.len(), 32); } @@ -931,18 +956,18 @@ mod tests { db.store_van_position(ROUND_ID, 0, 100).unwrap(); db.store_van_position(ROUND_ID, 1, 101).unwrap(); assert_eq!( - queries::load_van_position(&db.conn(), ROUND_ID, 0).unwrap(), + queries::load_van_position(&db.conn(), ROUND_ID, W, 0).unwrap(), 100 ); assert_eq!( - queries::load_van_position(&db.conn(), ROUND_ID, 1).unwrap(), + queries::load_van_position(&db.conn(), ROUND_ID, W, 1).unwrap(), 101 ); // Store votes for proposal 0 across both bundles let conn = db.conn(); - queries::store_vote(&conn, ROUND_ID, 0, 0, 0, &[0xAA; 32]).unwrap(); - queries::store_vote(&conn, ROUND_ID, 1, 0, 0, &[0xBB; 32]).unwrap(); + queries::store_vote(&conn, ROUND_ID, W, 0, 0, 0, &[0xAA; 32]).unwrap(); + queries::store_vote(&conn, ROUND_ID, W, 1, 0, 0, &[0xBB; 32]).unwrap(); drop(conn); let votes = db.get_votes(ROUND_ID).unwrap(); @@ -970,9 +995,9 @@ mod tests { // Verify proposal_authority reflects per-bundle submission state let conn = db.conn(); - let zkp2_0 = queries::load_zkp2_inputs(&conn, ROUND_ID, 0).unwrap(); + let zkp2_0 = queries::load_zkp2_inputs(&conn, ROUND_ID, W, 0).unwrap(); assert_eq!(zkp2_0.proposal_authority, 0xFFFF & !(1u64 << 0)); // bit 0 cleared - let zkp2_1 = queries::load_zkp2_inputs(&conn, ROUND_ID, 1).unwrap(); + let zkp2_1 = queries::load_zkp2_inputs(&conn, ROUND_ID, W, 1).unwrap(); assert_eq!(zkp2_1.proposal_authority, 0xFFFF); // no bits cleared drop(conn); diff --git a/librustvoting/src/storage/queries.rs b/librustvoting/src/storage/queries.rs index 91b0870d1..cab09f5d9 100644 --- a/librustvoting/src/storage/queries.rs +++ b/librustvoting/src/storage/queries.rs @@ -7,6 +7,7 @@ use crate::types::{VotingError, VotingRoundParams}; pub fn insert_round( conn: &Connection, + wallet_id: &str, params: &VotingRoundParams, session_json: Option<&str>, ) -> Result<(), VotingError> { @@ -16,10 +17,11 @@ pub fn insert_round( .as_secs() as i64; conn.execute( - "INSERT INTO rounds (round_id, snapshot_height, ea_pk, nc_root, nullifier_imt_root, session_json, phase, created_at) - VALUES (:round_id, :snapshot_height, :ea_pk, :nc_root, :nullifier_imt_root, :session_json, :phase, :created_at)", + "INSERT INTO rounds (round_id, wallet_id, snapshot_height, ea_pk, nc_root, nullifier_imt_root, session_json, phase, created_at) + VALUES (:round_id, :wallet_id, :snapshot_height, :ea_pk, :nc_root, :nullifier_imt_root, :session_json, :phase, :created_at)", named_params! { ":round_id": params.vote_round_id, + ":wallet_id": wallet_id, ":snapshot_height": params.snapshot_height as i64, ":ea_pk": params.ea_pk, ":nc_root": params.nc_root, @@ -39,14 +41,16 @@ pub fn insert_round( pub fn update_round_phase( conn: &Connection, round_id: &str, + wallet_id: &str, phase: RoundPhase, ) -> Result<(), VotingError> { let rows = conn .execute( - "UPDATE rounds SET phase = :phase WHERE round_id = :round_id", + "UPDATE rounds SET phase = :phase WHERE round_id = :round_id AND wallet_id = :wallet_id", named_params! { ":phase": phase as i32, ":round_id": round_id, + ":wallet_id": wallet_id, }, ) .map_err(|e| VotingError::Internal { @@ -65,10 +69,11 @@ pub fn update_round_phase( pub fn load_round_params( conn: &Connection, round_id: &str, + wallet_id: &str, ) -> Result { conn.query_row( - "SELECT round_id, snapshot_height, ea_pk, nc_root, nullifier_imt_root FROM rounds WHERE round_id = :round_id", - named_params! { ":round_id": round_id }, + "SELECT round_id, snapshot_height, ea_pk, nc_root, nullifier_imt_root FROM rounds WHERE round_id = :round_id AND wallet_id = :wallet_id", + named_params! { ":round_id": round_id, ":wallet_id": wallet_id }, |row| { Ok(VotingRoundParams { vote_round_id: row.get(0)?, @@ -84,11 +89,11 @@ pub fn load_round_params( }) } -pub fn get_round_state(conn: &Connection, round_id: &str) -> Result { +pub fn get_round_state(conn: &Connection, round_id: &str, wallet_id: &str) -> Result { let (phase_int, snapshot_height): (i32, i64) = conn .query_row( - "SELECT phase, snapshot_height FROM rounds WHERE round_id = :round_id", - named_params! { ":round_id": round_id }, + "SELECT phase, snapshot_height FROM rounds WHERE round_id = :round_id AND wallet_id = :wallet_id", + named_params! { ":round_id": round_id, ":wallet_id": wallet_id }, |row| Ok((row.get(0)?, row.get(1)?)), ) .map_err(|e| VotingError::InvalidInput { @@ -101,8 +106,8 @@ pub fn get_round_state(conn: &Connection, round_id: &str) -> Result Result Result Result Result, VotingError> { +pub fn list_rounds(conn: &Connection, wallet_id: &str) -> Result, VotingError> { let mut stmt = conn - .prepare("SELECT round_id, phase, snapshot_height, created_at FROM rounds ORDER BY created_at DESC") + .prepare("SELECT round_id, wallet_id, phase, snapshot_height, created_at FROM rounds WHERE wallet_id = :wallet_id ORDER BY created_at DESC") .map_err(|e| VotingError::Internal { message: format!("failed to prepare list_rounds query: {}", e), })?; let rounds = stmt - .query_map([], |row| { + .query_map(named_params! { ":wallet_id": wallet_id }, |row| { Ok(RoundSummary { round_id: row.get(0)?, - phase: RoundPhase::from_i32(row.get(1)?), - snapshot_height: row.get::<_, i64>(2)? as u64, - created_at: row.get::<_, i64>(3)? as u64, + wallet_id: row.get(1)?, + phase: RoundPhase::from_i32(row.get(2)?), + snapshot_height: row.get::<_, i64>(3)? as u64, + created_at: row.get::<_, i64>(4)? as u64, }) }) .map_err(|e| VotingError::Internal { @@ -174,10 +180,10 @@ pub fn list_rounds(conn: &Connection) -> Result, VotingError> /// Delete a round and all associated data. Child tables (bundles, cached_tree_state, /// proofs, witnesses, votes) are removed automatically via ON DELETE CASCADE. -pub fn clear_round(conn: &Connection, round_id: &str) -> Result<(), VotingError> { +pub fn clear_round(conn: &Connection, round_id: &str, wallet_id: &str) -> Result<(), VotingError> { conn.execute( - "DELETE FROM rounds WHERE round_id = :round_id", - named_params! { ":round_id": round_id }, + "DELETE FROM rounds WHERE round_id = :round_id AND wallet_id = :wallet_id", + named_params! { ":round_id": round_id, ":wallet_id": wallet_id }, ) .map_err(|e| VotingError::Internal { message: format!("failed to clear round: {}", e), @@ -191,6 +197,7 @@ pub fn clear_round(conn: &Connection, round_id: &str) -> Result<(), VotingError> pub fn insert_bundle( conn: &Connection, round_id: &str, + wallet_id: &str, bundle_index: u32, note_positions: &[u64], ) -> Result<(), VotingError> { @@ -200,10 +207,11 @@ pub fn insert_bundle( .collect(); conn.execute( - "INSERT INTO bundles (round_id, bundle_index, note_positions_blob) - VALUES (:round_id, :bundle_index, :note_positions_blob)", + "INSERT INTO bundles (round_id, wallet_id, bundle_index, note_positions_blob) + VALUES (:round_id, :wallet_id, :bundle_index, :note_positions_blob)", named_params! { ":round_id": round_id, + ":wallet_id": wallet_id, ":bundle_index": bundle_index as i64, ":note_positions_blob": blob, }, @@ -216,10 +224,10 @@ pub fn insert_bundle( } /// Get the number of bundles for a round. -pub fn get_bundle_count(conn: &Connection, round_id: &str) -> Result { +pub fn get_bundle_count(conn: &Connection, round_id: &str, wallet_id: &str) -> Result { conn.query_row( - "SELECT COUNT(*) FROM bundles WHERE round_id = :round_id", - named_params! { ":round_id": round_id }, + "SELECT COUNT(*) FROM bundles WHERE round_id = :round_id AND wallet_id = :wallet_id", + named_params! { ":round_id": round_id, ":wallet_id": wallet_id }, |row| row.get::<_, i64>(0).map(|c| c as u32), ) .map_err(|e| VotingError::Internal { @@ -231,13 +239,15 @@ pub fn get_bundle_count(conn: &Connection, round_id: &str) -> Result Result, VotingError> { let blob: Vec = conn .query_row( - "SELECT note_positions_blob FROM bundles WHERE round_id = :round_id AND bundle_index = :bundle_index", + "SELECT note_positions_blob FROM bundles WHERE round_id = :round_id AND wallet_id = :wallet_id AND bundle_index = :bundle_index", named_params! { ":round_id": round_id, + ":wallet_id": wallet_id, ":bundle_index": bundle_index as i64, }, |row| row.get(0), @@ -276,6 +286,7 @@ pub fn load_bundle_note_positions( pub fn store_delegation_data( conn: &Connection, round_id: &str, + wallet_id: &str, bundle_index: u32, van_comm_rand: &[u8], dummy_nullifiers: &[Vec], @@ -317,7 +328,7 @@ pub fn store_delegation_data( rseed_output = :rseed_output, gov_comm = :gov_comm, \ total_note_value = :total_note_value, address_index = :address_index, \ padded_note_secrets = :secrets, pczt_sighash = :sighash \ - WHERE round_id = :round_id AND bundle_index = :bundle_index", + WHERE round_id = :round_id AND wallet_id = :wallet_id AND bundle_index = :bundle_index", named_params! { ":rand": van_comm_rand, ":dummies": dummy_blob, @@ -334,6 +345,7 @@ pub fn store_delegation_data( ":secrets": secrets_blob, ":sighash": pczt_sighash, ":round_id": round_id, + ":wallet_id": wallet_id, ":bundle_index": bundle_index as i64, }, ) @@ -352,10 +364,10 @@ pub fn store_delegation_data( } /// Load nf_signed (signed note nullifier, 32 bytes) for a bundle. -pub fn load_nf_signed(conn: &Connection, round_id: &str, bundle_index: u32) -> Result, VotingError> { +pub fn load_nf_signed(conn: &Connection, round_id: &str, wallet_id: &str, bundle_index: u32) -> Result, VotingError> { conn.query_row( - "SELECT nf_signed FROM bundles WHERE round_id = :round_id AND bundle_index = :bundle_index", - named_params! { ":round_id": round_id, ":bundle_index": bundle_index as i64 }, + "SELECT nf_signed FROM bundles WHERE round_id = :round_id AND wallet_id = :wallet_id AND bundle_index = :bundle_index", + named_params! { ":round_id": round_id, ":wallet_id": wallet_id, ":bundle_index": bundle_index as i64 }, |row| row.get(0), ) .map_err(|e| VotingError::InvalidInput { @@ -364,10 +376,10 @@ pub fn load_nf_signed(conn: &Connection, round_id: &str, bundle_index: u32) -> R } /// Load cmx_new (output note commitment, 32 bytes) for a bundle. -pub fn load_cmx_new(conn: &Connection, round_id: &str, bundle_index: u32) -> Result, VotingError> { +pub fn load_cmx_new(conn: &Connection, round_id: &str, wallet_id: &str, bundle_index: u32) -> Result, VotingError> { conn.query_row( - "SELECT cmx_new FROM bundles WHERE round_id = :round_id AND bundle_index = :bundle_index", - named_params! { ":round_id": round_id, ":bundle_index": bundle_index as i64 }, + "SELECT cmx_new FROM bundles WHERE round_id = :round_id AND wallet_id = :wallet_id AND bundle_index = :bundle_index", + named_params! { ":round_id": round_id, ":wallet_id": wallet_id, ":bundle_index": bundle_index as i64 }, |row| row.get(0), ) .map_err(|e| VotingError::InvalidInput { @@ -376,10 +388,10 @@ pub fn load_cmx_new(conn: &Connection, round_id: &str, bundle_index: u32) -> Res } /// Load alpha (spend auth randomizer scalar, 32 bytes) for a bundle. -pub fn load_alpha(conn: &Connection, round_id: &str, bundle_index: u32) -> Result, VotingError> { +pub fn load_alpha(conn: &Connection, round_id: &str, wallet_id: &str, bundle_index: u32) -> Result, VotingError> { conn.query_row( - "SELECT alpha FROM bundles WHERE round_id = :round_id AND bundle_index = :bundle_index", - named_params! { ":round_id": round_id, ":bundle_index": bundle_index as i64 }, + "SELECT alpha FROM bundles WHERE round_id = :round_id AND wallet_id = :wallet_id AND bundle_index = :bundle_index", + named_params! { ":round_id": round_id, ":wallet_id": wallet_id, ":bundle_index": bundle_index as i64 }, |row| row.get(0), ) .map_err(|e| VotingError::InvalidInput { @@ -388,10 +400,10 @@ pub fn load_alpha(conn: &Connection, round_id: &str, bundle_index: u32) -> Resul } /// Load signed note rseed (32 bytes) for a bundle. -pub fn load_rseed_signed(conn: &Connection, round_id: &str, bundle_index: u32) -> Result, VotingError> { +pub fn load_rseed_signed(conn: &Connection, round_id: &str, wallet_id: &str, bundle_index: u32) -> Result, VotingError> { conn.query_row( - "SELECT rseed_signed FROM bundles WHERE round_id = :round_id AND bundle_index = :bundle_index", - named_params! { ":round_id": round_id, ":bundle_index": bundle_index as i64 }, + "SELECT rseed_signed FROM bundles WHERE round_id = :round_id AND wallet_id = :wallet_id AND bundle_index = :bundle_index", + named_params! { ":round_id": round_id, ":wallet_id": wallet_id, ":bundle_index": bundle_index as i64 }, |row| row.get(0), ) .map_err(|e| VotingError::InvalidInput { @@ -400,10 +412,10 @@ pub fn load_rseed_signed(conn: &Connection, round_id: &str, bundle_index: u32) - } /// Load output note rseed (32 bytes) for a bundle. -pub fn load_rseed_output(conn: &Connection, round_id: &str, bundle_index: u32) -> Result, VotingError> { +pub fn load_rseed_output(conn: &Connection, round_id: &str, wallet_id: &str, bundle_index: u32) -> Result, VotingError> { conn.query_row( - "SELECT rseed_output FROM bundles WHERE round_id = :round_id AND bundle_index = :bundle_index", - named_params! { ":round_id": round_id, ":bundle_index": bundle_index as i64 }, + "SELECT rseed_output FROM bundles WHERE round_id = :round_id AND wallet_id = :wallet_id AND bundle_index = :bundle_index", + named_params! { ":round_id": round_id, ":wallet_id": wallet_id, ":bundle_index": bundle_index as i64 }, |row| row.get(0), ) .map_err(|e| VotingError::InvalidInput { @@ -416,12 +428,13 @@ pub fn load_rseed_output(conn: &Connection, round_id: &str, bundle_index: u32) - pub fn load_padded_note_secrets( conn: &Connection, round_id: &str, + wallet_id: &str, bundle_index: u32, ) -> Result, Vec)>, VotingError> { let blob: Vec = conn .query_row( - "SELECT padded_note_secrets FROM bundles WHERE round_id = :round_id AND bundle_index = :bundle_index", - named_params! { ":round_id": round_id, ":bundle_index": bundle_index as i64 }, + "SELECT padded_note_secrets FROM bundles WHERE round_id = :round_id AND wallet_id = :wallet_id AND bundle_index = :bundle_index", + named_params! { ":round_id": round_id, ":wallet_id": wallet_id, ":bundle_index": bundle_index as i64 }, |row| row.get(0), ) .map_err(|e| VotingError::InvalidInput { @@ -446,11 +459,12 @@ pub fn load_padded_note_secrets( pub fn load_pczt_sighash( conn: &Connection, round_id: &str, + wallet_id: &str, bundle_index: u32, ) -> Result, VotingError> { conn.query_row( - "SELECT pczt_sighash FROM bundles WHERE round_id = :round_id AND bundle_index = :bundle_index", - named_params! { ":round_id": round_id, ":bundle_index": bundle_index as i64 }, + "SELECT pczt_sighash FROM bundles WHERE round_id = :round_id AND wallet_id = :wallet_id AND bundle_index = :bundle_index", + named_params! { ":round_id": round_id, ":wallet_id": wallet_id, ":bundle_index": bundle_index as i64 }, |row| row.get(0), ) .map_err(|e| VotingError::InvalidInput { @@ -459,10 +473,10 @@ pub fn load_pczt_sighash( } /// Load the VAN blinding factor for a bundle. Needed as a private witness in ZKP #2. -pub fn load_van_comm_rand(conn: &Connection, round_id: &str, bundle_index: u32) -> Result, VotingError> { +pub fn load_van_comm_rand(conn: &Connection, round_id: &str, wallet_id: &str, bundle_index: u32) -> Result, VotingError> { conn.query_row( - "SELECT van_comm_rand FROM bundles WHERE round_id = :round_id AND bundle_index = :bundle_index", - named_params! { ":round_id": round_id, ":bundle_index": bundle_index as i64 }, + "SELECT van_comm_rand FROM bundles WHERE round_id = :round_id AND wallet_id = :wallet_id AND bundle_index = :bundle_index", + named_params! { ":round_id": round_id, ":wallet_id": wallet_id, ":bundle_index": bundle_index as i64 }, |row| row.get(0), ) .map_err(|e| VotingError::InvalidInput { @@ -475,12 +489,13 @@ pub fn load_van_comm_rand(conn: &Connection, round_id: &str, bundle_index: u32) pub fn load_dummy_nullifiers( conn: &Connection, round_id: &str, + wallet_id: &str, bundle_index: u32, ) -> Result>, VotingError> { let blob: Vec = conn .query_row( - "SELECT dummy_nullifiers FROM bundles WHERE round_id = :round_id AND bundle_index = :bundle_index", - named_params! { ":round_id": round_id, ":bundle_index": bundle_index as i64 }, + "SELECT dummy_nullifiers FROM bundles WHERE round_id = :round_id AND wallet_id = :wallet_id AND bundle_index = :bundle_index", + named_params! { ":round_id": round_id, ":wallet_id": wallet_id, ":bundle_index": bundle_index as i64 }, |row| row.get(0), ) .map_err(|e| VotingError::InvalidInput { @@ -502,10 +517,10 @@ pub fn load_dummy_nullifiers( // --- Rho & Padded Note Data --- /// Load rho_signed for a bundle (32-byte constrained rho). -pub fn load_rho_signed(conn: &Connection, round_id: &str, bundle_index: u32) -> Result, VotingError> { +pub fn load_rho_signed(conn: &Connection, round_id: &str, wallet_id: &str, bundle_index: u32) -> Result, VotingError> { conn.query_row( - "SELECT rho_signed FROM bundles WHERE round_id = :round_id AND bundle_index = :bundle_index", - named_params! { ":round_id": round_id, ":bundle_index": bundle_index as i64 }, + "SELECT rho_signed FROM bundles WHERE round_id = :round_id AND wallet_id = :wallet_id AND bundle_index = :bundle_index", + named_params! { ":round_id": round_id, ":wallet_id": wallet_id, ":bundle_index": bundle_index as i64 }, |row| row.get(0), ) .map_err(|e| VotingError::InvalidInput { @@ -514,11 +529,11 @@ pub fn load_rho_signed(conn: &Connection, round_id: &str, bundle_index: u32) -> } /// Load padded note cmx data. Returns 0-3 entries of 32 bytes each. -pub fn load_padded_cmx(conn: &Connection, round_id: &str, bundle_index: u32) -> Result>, VotingError> { +pub fn load_padded_cmx(conn: &Connection, round_id: &str, wallet_id: &str, bundle_index: u32) -> Result>, VotingError> { let blob: Vec = conn .query_row( - "SELECT padded_note_data FROM bundles WHERE round_id = :round_id AND bundle_index = :bundle_index", - named_params! { ":round_id": round_id, ":bundle_index": bundle_index as i64 }, + "SELECT padded_note_data FROM bundles WHERE round_id = :round_id AND wallet_id = :wallet_id AND bundle_index = :bundle_index", + named_params! { ":round_id": round_id, ":wallet_id": wallet_id, ":bundle_index": bundle_index as i64 }, |row| row.get(0), ) .map_err(|e| VotingError::InvalidInput { @@ -563,13 +578,14 @@ const MAX_PROPOSAL_AUTHORITY: u64 = 65535; pub fn load_zkp2_inputs( conn: &Connection, round_id: &str, + wallet_id: &str, bundle_index: u32, ) -> Result { let data = conn.query_row( "SELECT b.van_comm_rand, b.total_note_value, b.address_index, r.ea_pk, r.round_id \ - FROM bundles b JOIN rounds r ON b.round_id = r.round_id \ - WHERE b.round_id = :round_id AND b.bundle_index = :bundle_index", - named_params! { ":round_id": round_id, ":bundle_index": bundle_index as i64 }, + FROM bundles b JOIN rounds r ON b.round_id = r.round_id AND b.wallet_id = r.wallet_id \ + WHERE b.round_id = :round_id AND b.wallet_id = :wallet_id AND b.bundle_index = :bundle_index", + named_params! { ":round_id": round_id, ":wallet_id": wallet_id, ":bundle_index": bundle_index as i64 }, |row| { Ok(Zkp2DelegationData { gov_comm_rand: row.get(0)?, @@ -589,13 +605,13 @@ pub fn load_zkp2_inputs( // for THIS bundle specifically. let mut authority = MAX_PROPOSAL_AUTHORITY; let mut stmt = conn - .prepare("SELECT proposal_id FROM votes WHERE round_id = :round_id AND bundle_index = :bundle_index AND submitted = 1") + .prepare("SELECT proposal_id FROM votes WHERE round_id = :round_id AND wallet_id = :wallet_id AND bundle_index = :bundle_index AND submitted = 1") .map_err(|e| VotingError::Internal { message: format!("failed to prepare proposal_authority query: {}", e), })?; let rows = stmt .query_map( - named_params! { ":round_id": round_id, ":bundle_index": bundle_index as i64 }, + named_params! { ":round_id": round_id, ":wallet_id": wallet_id, ":bundle_index": bundle_index as i64 }, |row| row.get::<_, i64>(0), ) .map_err(|e| VotingError::Internal { @@ -620,15 +636,17 @@ pub fn load_zkp2_inputs( pub fn store_van_position( conn: &Connection, round_id: &str, + wallet_id: &str, bundle_index: u32, position: u32, ) -> Result<(), VotingError> { let rows = conn .execute( - "UPDATE bundles SET van_leaf_position = :position WHERE round_id = :round_id AND bundle_index = :bundle_index", + "UPDATE bundles SET van_leaf_position = :position WHERE round_id = :round_id AND wallet_id = :wallet_id AND bundle_index = :bundle_index", named_params! { ":position": position as i64, ":round_id": round_id, + ":wallet_id": wallet_id, ":bundle_index": bundle_index as i64, }, ) @@ -644,10 +662,10 @@ pub fn store_van_position( } /// Load the VAN leaf position for witness generation. -pub fn load_van_position(conn: &Connection, round_id: &str, bundle_index: u32) -> Result { +pub fn load_van_position(conn: &Connection, round_id: &str, wallet_id: &str, bundle_index: u32) -> Result { conn.query_row( - "SELECT van_leaf_position FROM bundles WHERE round_id = :round_id AND bundle_index = :bundle_index", - named_params! { ":round_id": round_id, ":bundle_index": bundle_index as i64 }, + "SELECT van_leaf_position FROM bundles WHERE round_id = :round_id AND wallet_id = :wallet_id AND bundle_index = :bundle_index", + named_params! { ":round_id": round_id, ":wallet_id": wallet_id, ":bundle_index": bundle_index as i64 }, |row| row.get::<_, Option>(0), ) .map_err(|e| VotingError::InvalidInput { @@ -666,6 +684,7 @@ pub fn load_van_position(conn: &Connection, round_id: &str, bundle_index: u32) - pub fn store_proof_result_fields( conn: &Connection, round_id: &str, + wallet_id: &str, bundle_index: u32, rk: &[u8], gov_nullifiers: &[Vec], @@ -682,13 +701,14 @@ pub fn store_proof_result_fields( .execute( "UPDATE bundles SET rk = :rk, gov_nullifiers_blob = :gov_nullifiers_blob, \ nf_signed = :nf_signed, cmx_new = :cmx_new \ - WHERE round_id = :round_id AND bundle_index = :bundle_index", + WHERE round_id = :round_id AND wallet_id = :wallet_id AND bundle_index = :bundle_index", named_params! { ":rk": rk, ":gov_nullifiers_blob": gov_nullifiers_blob, ":nf_signed": nf_signed, ":cmx_new": cmx_new, ":round_id": round_id, + ":wallet_id": wallet_id, ":bundle_index": bundle_index as i64, }, ) @@ -721,6 +741,7 @@ pub struct DelegationDbFields { pub fn load_delegation_submission_data( conn: &Connection, round_id: &str, + wallet_id: &str, bundle_index: u32, ) -> Result { let (proof_bytes, rk, nf_signed, cmx_new, gov_comm, gov_nullifiers_blob, alpha, vote_round_id): ( @@ -729,9 +750,9 @@ pub fn load_delegation_submission_data( .query_row( "SELECT p.proof, b.rk, b.nf_signed, b.cmx_new, b.gov_comm, \ b.gov_nullifiers_blob, b.alpha, b.round_id \ - FROM bundles b JOIN proofs p ON b.round_id = p.round_id AND b.bundle_index = p.bundle_index \ - WHERE b.round_id = :round_id AND b.bundle_index = :bundle_index AND p.success = 1", - named_params! { ":round_id": round_id, ":bundle_index": bundle_index as i64 }, + FROM bundles b JOIN proofs p ON b.round_id = p.round_id AND b.bundle_index = p.bundle_index AND b.wallet_id = p.wallet_id \ + WHERE b.round_id = :round_id AND b.wallet_id = :wallet_id AND b.bundle_index = :bundle_index AND p.success = 1", + named_params! { ":round_id": round_id, ":wallet_id": wallet_id, ":bundle_index": bundle_index as i64 }, |row| { Ok(( row.get(0)?, @@ -783,14 +804,16 @@ pub fn load_delegation_submission_data( pub fn store_tree_state( conn: &Connection, round_id: &str, + wallet_id: &str, snapshot_height: u64, tree_state: &[u8], ) -> Result<(), VotingError> { conn.execute( - "INSERT OR REPLACE INTO cached_tree_state (round_id, snapshot_height, tree_state) - VALUES (:round_id, :snapshot_height, :tree_state)", + "INSERT OR REPLACE INTO cached_tree_state (round_id, wallet_id, snapshot_height, tree_state) + VALUES (:round_id, :wallet_id, :snapshot_height, :tree_state)", named_params! { ":round_id": round_id, + ":wallet_id": wallet_id, ":snapshot_height": snapshot_height as i64, ":tree_state": tree_state, }, @@ -801,10 +824,10 @@ pub fn store_tree_state( Ok(()) } -pub fn load_tree_state(conn: &Connection, round_id: &str) -> Result, VotingError> { +pub fn load_tree_state(conn: &Connection, round_id: &str, wallet_id: &str) -> Result, VotingError> { conn.query_row( - "SELECT tree_state FROM cached_tree_state WHERE round_id = :round_id", - named_params! { ":round_id": round_id }, + "SELECT tree_state FROM cached_tree_state WHERE round_id = :round_id AND wallet_id = :wallet_id", + named_params! { ":round_id": round_id, ":wallet_id": wallet_id }, |row| row.get(0), ) .map_err(|e| VotingError::InvalidInput { @@ -815,10 +838,10 @@ pub fn load_tree_state(conn: &Connection, round_id: &str) -> Result, Vot // --- Witnesses (Merkle inclusion proofs for Orchard notes) --- /// Check if witnesses are already cached for a bundle. -pub fn has_witnesses(conn: &Connection, round_id: &str, bundle_index: u32) -> Result { +pub fn has_witnesses(conn: &Connection, round_id: &str, wallet_id: &str, bundle_index: u32) -> Result { conn.query_row( - "SELECT COUNT(*) FROM witnesses WHERE round_id = :round_id AND bundle_index = :bundle_index", - named_params! { ":round_id": round_id, ":bundle_index": bundle_index as i64 }, + "SELECT COUNT(*) FROM witnesses WHERE round_id = :round_id AND wallet_id = :wallet_id AND bundle_index = :bundle_index", + named_params! { ":round_id": round_id, ":wallet_id": wallet_id, ":bundle_index": bundle_index as i64 }, |row| row.get::<_, i64>(0).map(|c| c > 0), ) .map_err(|e| VotingError::Internal { @@ -832,6 +855,7 @@ pub fn has_witnesses(conn: &Connection, round_id: &str, bundle_index: u32) -> Re pub fn store_witnesses( conn: &Connection, round_id: &str, + wallet_id: &str, bundle_index: u32, witnesses: &[crate::types::WitnessData], ) -> Result<(), VotingError> { @@ -845,10 +869,11 @@ pub fn store_witnesses( let auth_blob: Vec = w.auth_path.iter().flat_map(|h| h.iter().copied()).collect(); conn.execute( - "INSERT OR REPLACE INTO witnesses (round_id, bundle_index, note_position, note_commitment, root, auth_path, created_at) - VALUES (:round_id, :bundle_index, :position, :commitment, :root, :auth_path, :created_at)", + "INSERT OR REPLACE INTO witnesses (round_id, wallet_id, bundle_index, note_position, note_commitment, root, auth_path, created_at) + VALUES (:round_id, :wallet_id, :bundle_index, :position, :commitment, :root, :auth_path, :created_at)", named_params! { ":round_id": round_id, + ":wallet_id": wallet_id, ":bundle_index": bundle_index as i64, ":position": w.position as i64, ":commitment": w.note_commitment, @@ -866,11 +891,11 @@ pub fn store_witnesses( } /// Load cached witnesses for a bundle, ordered by position. -pub fn load_witnesses(conn: &Connection, round_id: &str, bundle_index: u32) -> Result, VotingError> { +pub fn load_witnesses(conn: &Connection, round_id: &str, wallet_id: &str, bundle_index: u32) -> Result, VotingError> { let mut stmt = conn .prepare( "SELECT note_position, note_commitment, root, auth_path FROM witnesses - WHERE round_id = :round_id AND bundle_index = :bundle_index ORDER BY note_position", + WHERE round_id = :round_id AND wallet_id = :wallet_id AND bundle_index = :bundle_index ORDER BY note_position", ) .map_err(|e| VotingError::Internal { message: format!("failed to prepare load_witnesses: {}", e), @@ -878,7 +903,7 @@ pub fn load_witnesses(conn: &Connection, round_id: &str, bundle_index: u32) -> R let witnesses = stmt .query_map( - named_params! { ":round_id": round_id, ":bundle_index": bundle_index as i64 }, + named_params! { ":round_id": round_id, ":wallet_id": wallet_id, ":bundle_index": bundle_index as i64 }, |row| { let position: i64 = row.get(0)?; let note_commitment: Vec = row.get(1)?; @@ -925,16 +950,18 @@ pub fn load_witnesses(conn: &Connection, round_id: &str, bundle_index: u32) -> R pub fn store_proof( conn: &Connection, round_id: &str, + wallet_id: &str, bundle_index: u32, proof_bytes: &[u8], ) -> Result<(), VotingError> { conn.execute( - "INSERT INTO proofs (round_id, bundle_index, proof, success, created_at) - VALUES (:round_id, :bundle_index, :proof, 1, strftime('%s','now')) - ON CONFLICT(round_id, bundle_index) DO UPDATE SET proof = :proof, success = 1", + "INSERT INTO proofs (round_id, wallet_id, bundle_index, proof, success, created_at) + VALUES (:round_id, :wallet_id, :bundle_index, :proof, 1, strftime('%s','now')) + ON CONFLICT(round_id, wallet_id, bundle_index) DO UPDATE SET proof = :proof, success = 1", named_params! { ":proof": proof_bytes, ":round_id": round_id, + ":wallet_id": wallet_id, ":bundle_index": bundle_index as i64, }, ) @@ -949,6 +976,7 @@ pub fn store_proof( pub fn store_vote( conn: &Connection, round_id: &str, + wallet_id: &str, bundle_index: u32, proposal_id: u32, choice: u32, @@ -960,10 +988,11 @@ pub fn store_vote( .as_secs() as i64; conn.execute( - "INSERT OR REPLACE INTO votes (round_id, bundle_index, proposal_id, choice, commitment, submitted, created_at) - VALUES (:round_id, :bundle_index, :proposal_id, :choice, :commitment, 0, :created_at)", + "INSERT OR REPLACE INTO votes (round_id, wallet_id, bundle_index, proposal_id, choice, commitment, submitted, created_at) + VALUES (:round_id, :wallet_id, :bundle_index, :proposal_id, :choice, :commitment, 0, :created_at)", named_params! { ":round_id": round_id, + ":wallet_id": wallet_id, ":bundle_index": bundle_index as i64, ":proposal_id": proposal_id as i64, ":choice": choice as i64, @@ -978,15 +1007,15 @@ pub fn store_vote( } /// Get all votes for a round (across all bundles). -pub fn get_votes(conn: &Connection, round_id: &str) -> Result, VotingError> { +pub fn get_votes(conn: &Connection, round_id: &str, wallet_id: &str) -> Result, VotingError> { let mut stmt = conn - .prepare("SELECT proposal_id, bundle_index, choice, submitted FROM votes WHERE round_id = :round_id") + .prepare("SELECT proposal_id, bundle_index, choice, submitted FROM votes WHERE round_id = :round_id AND wallet_id = :wallet_id") .map_err(|e| VotingError::Internal { message: format!("failed to prepare get_votes: {}", e), })?; let votes = stmt - .query_map(named_params! { ":round_id": round_id }, |row| { + .query_map(named_params! { ":round_id": round_id, ":wallet_id": wallet_id }, |row| { Ok(VoteRecord { proposal_id: row.get::<_, i64>(0)? as u32, bundle_index: row.get::<_, i64>(1)? as u32, @@ -1012,13 +1041,15 @@ pub fn get_votes(conn: &Connection, round_id: &str) -> Result, V pub fn delete_bundles_from( conn: &Connection, round_id: &str, + wallet_id: &str, from_index: u32, ) -> Result { let rows = conn .execute( - "DELETE FROM bundles WHERE round_id = :round_id AND bundle_index >= :from_index", + "DELETE FROM bundles WHERE round_id = :round_id AND wallet_id = :wallet_id AND bundle_index >= :from_index", named_params! { ":round_id": round_id, + ":wallet_id": wallet_id, ":from_index": from_index as i64, }, ) @@ -1031,13 +1062,15 @@ pub fn delete_bundles_from( pub fn mark_vote_submitted( conn: &Connection, round_id: &str, + wallet_id: &str, bundle_index: u32, proposal_id: u32, ) -> Result<(), VotingError> { conn.execute( - "UPDATE votes SET submitted = 1 WHERE round_id = :round_id AND bundle_index = :bundle_index AND proposal_id = :proposal_id", + "UPDATE votes SET submitted = 1 WHERE round_id = :round_id AND wallet_id = :wallet_id AND bundle_index = :bundle_index AND proposal_id = :proposal_id", named_params! { ":round_id": round_id, + ":wallet_id": wallet_id, ":bundle_index": bundle_index as i64, ":proposal_id": proposal_id as i64, },