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
39 changes: 36 additions & 3 deletions librustvoting/src/storage/migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use rusqlite::Connection;

use crate::VotingError;

const CURRENT_VERSION: u32 = 4;
const CURRENT_VERSION: u32 = 5;

pub fn migrate(conn: &Connection) -> Result<(), VotingError> {
let version: u32 = conn
Expand Down Expand Up @@ -55,7 +55,9 @@ pub fn migrate(conn: &Connection) -> Result<(), VotingError> {
// v3: delegation data moved from rounds to bundles table, witnesses
// gained bundle_index. Drop everything and recreate from 001_init.sql.
conn.execute_batch(
"DROP TABLE IF EXISTS votes;
"DROP TABLE IF EXISTS share_delegations;
DROP TABLE IF EXISTS keystone_signatures;
DROP TABLE IF EXISTS votes;
DROP TABLE IF EXISTS witnesses;
DROP TABLE IF EXISTS proofs;
DROP TABLE IF EXISTS bundles;
Expand All @@ -79,7 +81,9 @@ pub fn migrate(conn: &Connection) -> Result<(), VotingError> {
// 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 share_delegations;
DROP TABLE IF EXISTS keystone_signatures;
DROP TABLE IF EXISTS votes;
DROP TABLE IF EXISTS witnesses;
DROP TABLE IF EXISTS proofs;
DROP TABLE IF EXISTS bundles;
Expand All @@ -99,6 +103,33 @@ pub fn migrate(conn: &Connection) -> Result<(), VotingError> {
})?;
}

if version < 5 {
// v5: add share_delegations, keystone_signatures tables; add columns to
// bundles (delegation_tx_hash) and votes (tx_hash, vc_tree_position,
// commitment_bundle_json). Drop-all-recreate for pre-production.
conn.execute_batch(
"DROP TABLE IF EXISTS share_delegations;
DROP TABLE IF EXISTS keystone_signatures;
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 5 failed (drop): {}", e),
})?;
conn.execute_batch(include_str!("migrations/001_init.sql"))
.map_err(|e| VotingError::Internal {
message: format!("migration to version 5 failed (create): {}", e),
})?;
conn.pragma_update(None, "user_version", 5)
.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 {
Expand Down Expand Up @@ -162,6 +193,8 @@ mod tests {
assert!(tables.contains(&"cached_tree_state".to_string()));
assert!(tables.contains(&"proofs".to_string()));
assert!(tables.contains(&"votes".to_string()));
assert!(tables.contains(&"share_delegations".to_string()));
assert!(tables.contains(&"keystone_signatures".to_string()));
}

/// Verify that the bundles table columns exist after migration and can round-trip BLOB data.
Expand Down
32 changes: 32 additions & 0 deletions librustvoting/src/storage/migrations/001_init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ CREATE TABLE bundles (
gov_nullifiers_blob BLOB,
padded_note_secrets BLOB,
pczt_sighash BLOB,
delegation_tx_hash TEXT,
PRIMARY KEY (round_id, wallet_id, bundle_index),
FOREIGN KEY (round_id, wallet_id) REFERENCES rounds(round_id, wallet_id) ON DELETE CASCADE
);
Expand Down Expand Up @@ -81,6 +82,37 @@ CREATE TABLE votes (
commitment BLOB,
submitted INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
tx_hash TEXT,
vc_tree_position INTEGER,
commitment_bundle_json TEXT,
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
);

CREATE TABLE share_delegations (
round_id TEXT NOT NULL,
wallet_id TEXT NOT NULL DEFAULT '',
bundle_index INTEGER NOT NULL,
proposal_id INTEGER NOT NULL,
share_index INTEGER NOT NULL,
helper_url TEXT NOT NULL,
nullifier BLOB NOT NULL,
confirmed INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
PRIMARY KEY (round_id, wallet_id, bundle_index, proposal_id, share_index),
FOREIGN KEY (round_id, wallet_id, bundle_index)
REFERENCES bundles(round_id, wallet_id, bundle_index) ON DELETE CASCADE
);

CREATE TABLE keystone_signatures (
round_id TEXT NOT NULL,
wallet_id TEXT NOT NULL DEFAULT '',
bundle_index INTEGER NOT NULL,
sig BLOB NOT NULL,
sighash BLOB NOT NULL,
rk BLOB NOT NULL,
created_at INTEGER NOT NULL,
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
);
11 changes: 10 additions & 1 deletion librustvoting/src/storage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ pub struct RoundSummary {
pub created_at: u64,
}

/// A Keystone bundle signature stored in the DB.
#[derive(Clone, Debug)]
pub struct KeystoneSignatureRecord {
pub bundle_index: u32,
pub sig: Vec<u8>,
pub sighash: Vec<u8>,
pub rk: Vec<u8>,
}

/// 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 {
Expand Down Expand Up @@ -142,7 +151,7 @@ mod tests {
let version: u32 = conn
.pragma_query_value(None, "user_version", |r| r.get(0))
.unwrap();
assert_eq!(version, 4);
assert_eq!(version, 5);
}

#[test]
Expand Down
102 changes: 101 additions & 1 deletion librustvoting/src/storage/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ use std::collections::HashMap;
use ff::PrimeField;

use crate::storage::queries;
use crate::storage::{RoundPhase, RoundState, RoundSummary, VoteRecord, VotingDb};
use crate::storage::{
KeystoneSignatureRecord, RoundPhase, RoundState, RoundSummary, VoteRecord, VotingDb,
};
use crate::types::{
DelegationProofResult, DelegationSubmissionData, EncryptedShare,
GovernancePczt, NoteInfo, ProofProgressReporter, SharePayload, VoteCommitmentBundle,
Expand Down Expand Up @@ -722,6 +724,104 @@ impl VotingDb {
let wallet_id = self.wallet_id();
queries::mark_vote_submitted(&conn, round_id, &wallet_id, bundle_index, proposal_id)
}

// --- Recovery state ---

pub fn store_delegation_tx_hash(
&self,
round_id: &str,
bundle_index: u32,
tx_hash: &str,
) -> Result<(), VotingError> {
let conn = self.conn();
let wallet_id = self.wallet_id();
queries::store_delegation_tx_hash(&conn, round_id, &wallet_id, bundle_index, tx_hash)
}

pub fn get_delegation_tx_hash(
&self,
round_id: &str,
bundle_index: u32,
) -> Result<Option<String>, VotingError> {
let conn = self.conn();
let wallet_id = self.wallet_id();
queries::get_delegation_tx_hash(&conn, round_id, &wallet_id, bundle_index)
}

pub fn store_vote_tx_hash(
&self,
round_id: &str,
bundle_index: u32,
proposal_id: u32,
tx_hash: &str,
) -> Result<(), VotingError> {
let conn = self.conn();
let wallet_id = self.wallet_id();
queries::store_vote_tx_hash(&conn, round_id, &wallet_id, bundle_index, proposal_id, tx_hash)
}

pub fn get_vote_tx_hash(
&self,
round_id: &str,
bundle_index: u32,
proposal_id: u32,
) -> Result<Option<String>, VotingError> {
let conn = self.conn();
let wallet_id = self.wallet_id();
queries::get_vote_tx_hash(&conn, round_id, &wallet_id, bundle_index, proposal_id)
}

pub fn store_commitment_bundle(
&self,
round_id: &str,
bundle_index: u32,
proposal_id: u32,
bundle_json: &str,
vc_tree_position: u64,
) -> Result<(), VotingError> {
let conn = self.conn();
let wallet_id = self.wallet_id();
queries::store_commitment_bundle(&conn, round_id, &wallet_id, bundle_index, proposal_id, bundle_json, vc_tree_position)
}

pub fn get_commitment_bundle(
&self,
round_id: &str,
bundle_index: u32,
proposal_id: u32,
) -> Result<Option<(String, u64)>, VotingError> {
let conn = self.conn();
let wallet_id = self.wallet_id();
queries::get_commitment_bundle(&conn, round_id, &wallet_id, bundle_index, proposal_id)
}

pub fn store_keystone_signature(
&self,
round_id: &str,
bundle_index: u32,
sig: &[u8],
sighash: &[u8],
rk: &[u8],
) -> Result<(), VotingError> {
let conn = self.conn();
let wallet_id = self.wallet_id();
queries::store_keystone_signature(&conn, round_id, &wallet_id, bundle_index, sig, sighash, rk)
}

pub fn get_keystone_signatures(
&self,
round_id: &str,
) -> Result<Vec<KeystoneSignatureRecord>, VotingError> {
let conn = self.conn();
let wallet_id = self.wallet_id();
queries::get_keystone_signatures(&conn, round_id, &wallet_id)
}

pub fn clear_recovery_state(&self, round_id: &str) -> Result<(), VotingError> {
let conn = self.conn();
let wallet_id = self.wallet_id();
queries::clear_recovery_state(&conn, round_id, &wallet_id)
}
}

#[cfg(test)]
Expand Down
Loading