use std::collections::HashSet;
use schemerz_rusqlite::RusqliteMigration;
use uuid::Uuid;
use crate::wallet::init::WalletMigrationError;
use super::account_delete_cascade;
pub const MIGRATION_ID: Uuid = Uuid::from_u128(0x944f8a1e_bdfa_4d52_90ca_663dee8efc62);
const DEPENDENCIES: &[Uuid] = &[account_delete_cascade::MIGRATION_ID];
pub(super) struct Migration;
impl schemerz::Migration<Uuid> for Migration {
fn id(&self) -> Uuid {
MIGRATION_ID
}
fn dependencies(&self) -> HashSet<Uuid> {
DEPENDENCIES.iter().copied().collect()
}
fn description(&self) -> &'static str {
"Relaxes the addresses table constraint to allow standalone P2SH addresses."
}
}
impl RusqliteMigration for Migration {
type Error = WalletMigrationError;
fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), Self::Error> {
transaction.execute_batch(
r#"
CREATE TABLE addresses_new (
id INTEGER NOT NULL PRIMARY KEY,
account_id INTEGER NOT NULL
REFERENCES accounts(id) ON DELETE CASCADE,
key_scope INTEGER NOT NULL,
diversifier_index_be BLOB,
address TEXT NOT NULL,
transparent_child_index INTEGER,
cached_transparent_receiver_address TEXT,
exposed_at_height INTEGER,
receiver_flags INTEGER NOT NULL,
transparent_receiver_next_check_time INTEGER,
imported_transparent_receiver_pubkey BLOB,
imported_transparent_receiver_script BLOB,
UNIQUE (account_id, key_scope, diversifier_index_be),
UNIQUE (imported_transparent_receiver_pubkey),
UNIQUE (imported_transparent_receiver_script),
CONSTRAINT ck_addr_transparent_index_consistency CHECK (
(transparent_child_index IS NULL OR diversifier_index_be < x'0000000F00000000000000')
AND (
(
cached_transparent_receiver_address IS NULL
AND transparent_child_index IS NULL
AND imported_transparent_receiver_pubkey IS NULL
AND imported_transparent_receiver_script IS NULL
)
OR (
cached_transparent_receiver_address IS NOT NULL
AND (
(transparent_child_index IS NULL) == (
key_scope = -1 AND (
(imported_transparent_receiver_pubkey IS NULL) !=
(imported_transparent_receiver_script IS NULL)
)
)
)
)
)
),
CONSTRAINT ck_addr_foreign_or_diversified CHECK (
(diversifier_index_be IS NULL) == (key_scope = -1)
)
);
INSERT INTO addresses_new (
id, account_id, key_scope, diversifier_index_be, address,
transparent_child_index, cached_transparent_receiver_address,
exposed_at_height, receiver_flags, transparent_receiver_next_check_time,
imported_transparent_receiver_pubkey
)
SELECT
id, account_id, key_scope, diversifier_index_be, address,
transparent_child_index, cached_transparent_receiver_address,
exposed_at_height, receiver_flags, transparent_receiver_next_check_time,
imported_transparent_receiver_pubkey
FROM addresses;
PRAGMA legacy_alter_table = ON;
DROP TABLE addresses;
ALTER TABLE addresses_new RENAME TO addresses;
PRAGMA legacy_alter_table = OFF;
-- Recreate the existing indices
CREATE INDEX idx_addresses_accounts ON addresses (
account_id ASC
);
CREATE INDEX idx_addresses_indices ON addresses (
diversifier_index_be ASC
);
CREATE INDEX idx_addresses_pubkeys ON addresses (
imported_transparent_receiver_pubkey ASC
);
CREATE INDEX idx_addresses_t_indices ON addresses (
transparent_child_index ASC
);
"#,
)?;
Ok(())
}
fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), Self::Error> {
Err(WalletMigrationError::CannotRevert(MIGRATION_ID))
}
}
#[cfg(test)]
mod tests {
use rusqlite::named_params;
use secrecy::Secret;
use tempfile::NamedTempFile;
use zcash_keys::keys::UnifiedSpendingKey;
use zcash_protocol::consensus::Network;
use crate::{
WalletDb,
testing::db::{test_clock, test_rng},
wallet::init::{WalletMigrator, migrations::tests::test_migrate},
};
use super::{DEPENDENCIES, MIGRATION_ID};
#[test]
fn migrate() {
test_migrate(&[MIGRATION_ID]);
}
#[test]
fn migrate_preserves_data() {
let network = Network::TestNetwork;
let data_file = NamedTempFile::new().unwrap();
let mut db_data =
WalletDb::for_path(data_file.path(), network, test_clock(), test_rng()).unwrap();
let seed_bytes = vec![0xab; 32];
WalletMigrator::new()
.with_seed(Secret::new(seed_bytes.clone()))
.ignore_seed_relevance()
.init_or_migrate_to(&mut db_data, DEPENDENCIES)
.unwrap();
let usk = UnifiedSpendingKey::from_seed(&network, &seed_bytes[..], zip32::AccountId::ZERO)
.unwrap();
let ufvk = usk.to_unified_full_viewing_key();
let ufvk_str = ufvk.encode(&network);
let uivk_str = ufvk.to_unified_incoming_viewing_key().encode(&network);
db_data
.conn
.execute(
"INSERT INTO accounts (uuid, account_kind, hd_seed_fingerprint,
hd_account_index, ufvk, uivk, has_spend_key, birthday_height)
VALUES (X'0000000000000000000000000000AAAA', 0, X'00000000000000000000000000000000000000000000000000000000000000AB',
0, :ufvk, :uivk, 1, 1)",
named_params![":ufvk": ufvk_str, ":uivk": uivk_str],
)
.unwrap();
let account_id: i64 = db_data
.conn
.query_row(
"SELECT id FROM accounts WHERE uuid = X'0000000000000000000000000000AAAA'",
[],
|row| row.get(0),
)
.unwrap();
db_data
.conn
.execute(
"INSERT INTO addresses (account_id, key_scope, diversifier_index_be, address,
transparent_child_index, cached_transparent_receiver_address, receiver_flags)
VALUES (?1, 0, X'00000000000000000000000000', 'addr_derived', 0, 't_derived', 5)",
[account_id],
)
.unwrap();
db_data
.conn
.execute(
"INSERT INTO addresses (account_id, key_scope, diversifier_index_be, address,
receiver_flags)
VALUES (?1, 0, X'00000000000000000100000000', 'addr_shielded', 4)",
[account_id],
)
.unwrap();
db_data
.conn
.execute(
"INSERT INTO addresses (account_id, key_scope, address,
cached_transparent_receiver_address, receiver_flags,
imported_transparent_receiver_pubkey)
VALUES (?1, -1, 'ttest_addr', 'ttest_addr', 1, X'0000000000000000000000000000000000000000000000000000000000000001')",
[account_id],
)
.unwrap();
WalletMigrator::new()
.with_seed(Secret::new(seed_bytes))
.ignore_seed_relevance()
.init_or_migrate_to(&mut db_data, &[MIGRATION_ID])
.unwrap();
let count: i64 = db_data
.conn
.query_row(
"SELECT COUNT(*) FROM addresses WHERE account_id = ?1",
[account_id],
|row| row.get(0),
)
.unwrap();
assert_eq!(count, 3);
let rows: Vec<(String, Option<Vec<u8>>)> = {
let mut stmt = db_data
.conn
.prepare(
"SELECT address, imported_transparent_receiver_script FROM addresses
WHERE account_id = ?1 ORDER BY address",
)
.unwrap();
stmt.query_map([account_id], |row| Ok((row.get(0)?, row.get(1)?)))
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap()
};
assert_eq!(rows.len(), 3);
assert_eq!(rows[0].0, "addr_derived");
assert!(rows[0].1.is_none());
assert_eq!(rows[1].0, "addr_shielded");
assert!(rows[1].1.is_none());
assert_eq!(rows[2].0, "ttest_addr");
assert!(rows[2].1.is_none());
db_data
.conn
.execute(
"INSERT INTO addresses (account_id, key_scope, address,
cached_transparent_receiver_address, receiver_flags, imported_transparent_receiver_script)
VALUES (?1, -1, 't_p2sh', 't_p2sh', 2, X'0102030405')",
[account_id],
)
.unwrap();
let result = db_data.conn.execute(
"INSERT INTO addresses (account_id, key_scope, address,
receiver_flags, imported_transparent_receiver_script)
VALUES (?1, -1, 't_p2sh_bad', 2, X'AABBCCDD')",
[account_id],
);
assert!(result.is_err());
let result = db_data.conn.execute(
"INSERT INTO addresses (account_id, key_scope, diversifier_index_be, address,
cached_transparent_receiver_address, receiver_flags, imported_transparent_receiver_script)
VALUES (?1, 0, X'00000000000000000200000000', 'bad_non_foreign_p2sh', 'bad_non_foreign_p2sh', 2, X'AABBCCDD')",
[account_id],
);
assert!(result.is_err());
}
}