use std::collections::HashSet;
use schemerz_rusqlite::RusqliteMigration;
use uuid::Uuid;
use crate::wallet::init::WalletMigrationError;
use super::note_locking;
pub const MIGRATION_ID: Uuid = Uuid::from_u128(0x2eac815d_67ca_4fb4_b534_066102a0fba2);
pub(super) struct Migration;
impl schemerz::Migration<Uuid> for Migration {
fn id(&self) -> Uuid {
MIGRATION_ID
}
fn dependencies(&self) -> HashSet<Uuid> {
[note_locking::MIGRATION_ID].into_iter().collect()
}
fn description(&self) -> &'static str {
"Adds compact encryption context to existing Ironwood notes."
}
}
impl RusqliteMigration for Migration {
type Error = WalletMigrationError;
fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), Self::Error> {
let columns = transaction
.prepare("PRAGMA table_info(ironwood_received_notes)")?
.query_map([], |row| row.get::<_, String>(1))?
.collect::<Result<HashSet<_>, _>>()?;
match (
columns.contains("ephemeral_key"),
columns.contains("compact_ciphertext"),
) {
(true, true) => return Ok(()),
(false, false) => {}
_ => {
return Err(WalletMigrationError::CorruptedData(
"incomplete Ironwood compact encryption schema".into(),
));
}
}
transaction.execute_batch(
"ALTER TABLE ironwood_received_notes ADD COLUMN ephemeral_key BLOB;
ALTER TABLE ironwood_received_notes ADD COLUMN compact_ciphertext BLOB
CHECK ((ephemeral_key IS NULL AND compact_ciphertext IS NULL) OR
(ephemeral_key IS NOT NULL AND compact_ciphertext IS NOT NULL AND
length(ephemeral_key) = 32 AND length(compact_ciphertext) = 52));",
)?;
Ok(())
}
fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), Self::Error> {
Err(WalletMigrationError::CannotRevert(MIGRATION_ID))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::wallet::init::migrations::{ironwood_received_notes, tests::test_migrate};
#[test]
fn migrate() {
test_migrate(&[MIGRATION_ID]);
}
#[test]
fn rc5_upgrade_preserves_notes_and_spends() {
let mut conn = rusqlite::Connection::open_in_memory().unwrap();
conn.pragma_update(None, "foreign_keys", false).unwrap();
let tx = conn.transaction().unwrap();
ironwood_received_notes::Migration.up(&tx).unwrap();
tx.execute_batch(
"ALTER TABLE ironwood_received_notes ADD COLUMN lock_expiry_height INTEGER;
ALTER TABLE ironwood_received_notes ADD COLUMN lock_owner BLOB;
INSERT INTO ironwood_received_notes
(id, transaction_id, action_index, account_id, diversifier, value,
rho, rseed, nf, is_change, memo, commitment_tree_position, note_version,
lock_expiry_height, lock_owner)
VALUES (1, 2, 3, 4, X'01', 500, X'02', X'03', X'04', 0, X'05', 6, 3, 700, X'06');
INSERT INTO ironwood_received_note_spends VALUES (1, 8);",
)
.unwrap();
assert!(
tx.prepare("SELECT ephemeral_key FROM ironwood_received_notes")
.is_err()
);
let read_note = |tx: &rusqlite::Transaction<'_>| {
let mut stmt = tx.prepare("SELECT * FROM ironwood_received_notes").unwrap();
let columns = stmt.column_count();
stmt.query_row([], |row| {
(0..columns)
.map(|i| row.get::<_, rusqlite::types::Value>(i))
.collect::<Result<Vec<_>, _>>()
})
.unwrap()
};
let before = read_note(&tx);
Migration.up(&tx).unwrap();
let after = read_note(&tx);
assert_eq!(&after[..before.len()], before.as_slice());
assert_eq!(
&after[before.len()..],
&[rusqlite::types::Value::Null, rusqlite::types::Value::Null]
);
assert_eq!(
tx.query_row("SELECT * FROM ironwood_received_note_spends", [], |row| Ok(
(row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)
))
.unwrap(),
(1, 8)
);
for invalid in [
"ephemeral_key = zeroblob(32)",
"compact_ciphertext = zeroblob(52)",
"ephemeral_key = zeroblob(31), compact_ciphertext = zeroblob(52)",
"ephemeral_key = zeroblob(32), compact_ciphertext = zeroblob(51)",
] {
assert!(
tx.execute(&format!("UPDATE ironwood_received_notes SET {invalid}"), [])
.is_err()
);
}
tx.execute("UPDATE ironwood_received_notes SET ephemeral_key = zeroblob(32), compact_ciphertext = zeroblob(52)", []).unwrap();
Migration.up(&tx).unwrap();
assert_eq!(tx.query_row("SELECT length(ephemeral_key), length(compact_ciphertext) FROM ironwood_received_notes", [], |r| Ok((r.get::<_, i64>(0)?, r.get::<_, i64>(1)?))).unwrap(), (32, 52));
tx.commit().unwrap();
}
}