1use std::borrow::BorrowMut;
4use std::fmt;
5use std::rc::Rc;
6
7use rand_core::RngCore;
8use regex::Regex;
9use schemerz::{Migrator, MigratorError};
10use schemerz_rusqlite::{RusqliteAdapter, RusqliteMigration};
11use secrecy::SecretVec;
12use shardtree::error::ShardTreeError;
13use uuid::Uuid;
14
15use zcash_client_backend::data_api::{SeedRelevance, WalletRead};
16use zcash_keys::keys::AddressGenerationError;
17use zcash_protocol::{consensus, value::BalanceError};
18
19use self::migrations::verify_network_compatibility;
20
21use super::commitment_tree;
22use crate::{error::SqliteClientError, util::Clock, WalletDb};
23
24pub mod migrations;
25
26const SQLITE_MAJOR_VERSION: u32 = 3;
27const MIN_SQLITE_MINOR_VERSION: u32 = 35;
28
29const MIGRATIONS_TABLE: &str = "schemer_migrations";
30
31#[derive(Debug)]
32pub enum WalletMigrationError {
33 DatabaseNotSupported(String),
36
37 SeedRequired,
39
40 SeedNotRelevant,
50
51 CorruptedData(String),
53
54 AddressGeneration(AddressGenerationError),
56
57 DbError(rusqlite::Error),
59
60 BalanceError(BalanceError),
62
63 CommitmentTree(Box<ShardTreeError<commitment_tree::Error>>),
65
66 CannotRevert(Uuid),
68
69 Other(Box<SqliteClientError>),
71}
72
73impl From<rusqlite::Error> for WalletMigrationError {
74 fn from(e: rusqlite::Error) -> Self {
75 WalletMigrationError::DbError(e)
76 }
77}
78
79impl From<BalanceError> for WalletMigrationError {
80 fn from(e: BalanceError) -> Self {
81 WalletMigrationError::BalanceError(e)
82 }
83}
84
85impl From<ShardTreeError<commitment_tree::Error>> for WalletMigrationError {
86 fn from(e: ShardTreeError<commitment_tree::Error>) -> Self {
87 WalletMigrationError::CommitmentTree(Box::new(e))
88 }
89}
90
91impl From<AddressGenerationError> for WalletMigrationError {
92 fn from(e: AddressGenerationError) -> Self {
93 WalletMigrationError::AddressGeneration(e)
94 }
95}
96
97impl From<SqliteClientError> for WalletMigrationError {
98 fn from(value: SqliteClientError) -> Self {
99 match value {
100 SqliteClientError::CorruptedData(err) => WalletMigrationError::CorruptedData(err),
101 SqliteClientError::DbError(err) => WalletMigrationError::DbError(err),
102 SqliteClientError::CommitmentTree(err) => {
103 WalletMigrationError::CommitmentTree(Box::new(err))
104 }
105 SqliteClientError::BalanceError(err) => WalletMigrationError::BalanceError(err),
106 SqliteClientError::AddressGeneration(err) => {
107 WalletMigrationError::AddressGeneration(err)
108 }
109 other => WalletMigrationError::Other(Box::new(other)),
110 }
111 }
112}
113
114impl fmt::Display for WalletMigrationError {
115 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
116 match &self {
117 WalletMigrationError::DatabaseNotSupported(version) => {
118 write!(
119 f,
120 "The installed SQLite version {} does not support operations required by the wallet.",
121 version
122 )
123 }
124 WalletMigrationError::SeedRequired => {
125 write!(
126 f,
127 "The wallet seed is required in order to update the database."
128 )
129 }
130 WalletMigrationError::SeedNotRelevant => {
131 write!(
132 f,
133 "The provided seed is not relevant to any derived accounts in the database."
134 )
135 }
136 WalletMigrationError::CorruptedData(reason) => {
137 write!(f, "Wallet database is corrupted: {}", reason)
138 }
139 WalletMigrationError::DbError(e) => write!(f, "{}", e),
140 WalletMigrationError::BalanceError(e) => write!(f, "Balance error: {:?}", e),
141 WalletMigrationError::CommitmentTree(e) => write!(f, "Commitment tree error: {:?}", e),
142 WalletMigrationError::AddressGeneration(e) => {
143 write!(f, "Address generation error: {:?}", e)
144 }
145 WalletMigrationError::CannotRevert(uuid) => {
146 write!(f, "Reverting migration {} is not supported", uuid)
147 }
148 WalletMigrationError::Other(err) => {
149 write!(
150 f,
151 "Unexpected violation of database business rules: {}",
152 err
153 )
154 }
155 }
156 }
157}
158
159impl std::error::Error for WalletMigrationError {
160 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
161 match &self {
162 WalletMigrationError::DbError(e) => Some(e),
163 WalletMigrationError::BalanceError(e) => Some(e),
164 WalletMigrationError::CommitmentTree(e) => Some(e),
165 WalletMigrationError::AddressGeneration(e) => Some(e),
166 WalletMigrationError::Other(e) => Some(e),
167 _ => None,
168 }
169 }
170}
171
172fn sqlite_client_error_to_wallet_migration_error(e: SqliteClientError) -> WalletMigrationError {
178 match e {
179 SqliteClientError::CorruptedData(e) => WalletMigrationError::CorruptedData(e),
180 SqliteClientError::Protobuf(e) => WalletMigrationError::CorruptedData(e.to_string()),
181 SqliteClientError::InvalidNote => {
182 WalletMigrationError::CorruptedData("invalid note".into())
183 }
184 SqliteClientError::DecodingError(e) => WalletMigrationError::CorruptedData(e.to_string()),
185 #[cfg(feature = "transparent-inputs")]
186 SqliteClientError::TransparentDerivation(e) => {
187 WalletMigrationError::CorruptedData(e.to_string())
188 }
189 #[cfg(feature = "transparent-inputs")]
190 SqliteClientError::TransparentAddress(e) => {
191 WalletMigrationError::CorruptedData(e.to_string())
192 }
193 SqliteClientError::DbError(e) => WalletMigrationError::DbError(e),
194 SqliteClientError::Io(e) => WalletMigrationError::CorruptedData(e.to_string()),
195 SqliteClientError::InvalidMemo(e) => WalletMigrationError::CorruptedData(e.to_string()),
196 SqliteClientError::AddressGeneration(e) => WalletMigrationError::AddressGeneration(e),
197 SqliteClientError::BadAccountData(e) => WalletMigrationError::CorruptedData(e),
198 SqliteClientError::CommitmentTree(e) => WalletMigrationError::CommitmentTree(Box::new(e)),
199 SqliteClientError::UnsupportedPoolType(pool) => WalletMigrationError::CorruptedData(
200 format!("Wallet DB contains unsupported pool type {}", pool),
201 ),
202 SqliteClientError::BalanceError(e) => WalletMigrationError::BalanceError(e),
203 SqliteClientError::TableNotEmpty => unreachable!("wallet already initialized"),
204 SqliteClientError::BlockConflict(_)
205 | SqliteClientError::NonSequentialBlocks
206 | SqliteClientError::RequestedRewindInvalid { .. }
207 | SqliteClientError::KeyDerivationError(_)
208 | SqliteClientError::Zip32AccountIndexOutOfRange
209 | SqliteClientError::AccountCollision(_)
210 | SqliteClientError::CacheMiss(_) => {
211 unreachable!("we only call WalletRead methods; mutations can't occur")
212 }
213 #[cfg(feature = "transparent-inputs")]
214 SqliteClientError::AddressNotRecognized(_) => {
215 unreachable!("we only call WalletRead methods; mutations can't occur")
216 }
217 SqliteClientError::AccountUnknown => {
218 unreachable!("all accounts are known in migration context")
219 }
220 SqliteClientError::UnknownZip32Derivation => {
221 unreachable!("we don't call methods that require operating on imported accounts")
222 }
223 SqliteClientError::ChainHeightUnknown => {
224 unreachable!("we don't call methods that require a known chain height")
225 }
226 #[cfg(feature = "transparent-inputs")]
227 SqliteClientError::ReachedGapLimit(..) => {
228 unreachable!("we don't do ephemeral address tracking")
229 }
230 SqliteClientError::DiversifierIndexReuse(i, _) => {
231 WalletMigrationError::CorruptedData(format!(
232 "invalid attempt to overwrite address at diversifier index {}",
233 u128::from(i)
234 ))
235 }
236 SqliteClientError::AddressReuse(_, _) => {
237 unreachable!("we don't create transactions in migrations")
238 }
239 SqliteClientError::NoteFilterInvalid(_) => {
240 unreachable!("we don't do note selection in migrations")
241 }
242 #[cfg(feature = "transparent-inputs")]
243 SqliteClientError::Scheduling(e) => {
244 WalletMigrationError::Other(Box::new(SqliteClientError::Scheduling(e)))
245 }
246 }
247}
248
249pub fn init_wallet_db<
328 C: BorrowMut<rusqlite::Connection>,
329 P: consensus::Parameters + 'static,
330 CL: Clock + Clone + 'static,
331 R: RngCore + Clone + 'static,
332>(
333 wdb: &mut WalletDb<C, P, CL, R>,
334 seed: Option<SecretVec<u8>>,
335) -> Result<(), MigratorError<Uuid, WalletMigrationError>> {
336 if let Some(seed) = seed {
337 WalletMigrator::new().with_seed(seed)
338 } else {
339 WalletMigrator::new()
340 }
341 .init_or_migrate(wdb)
342}
343
344pub struct WalletMigrator {
420 seed: Option<SecretVec<u8>>,
421 verify_seed_relevance: bool,
422 external_migrations: Option<Vec<Box<dyn RusqliteMigration<Error = WalletMigrationError>>>>,
423}
424
425impl Default for WalletMigrator {
426 fn default() -> Self {
427 Self::new()
428 }
429}
430
431impl WalletMigrator {
432 pub fn new() -> Self {
434 Self {
435 seed: None,
436 verify_seed_relevance: true,
437 external_migrations: None,
438 }
439 }
440
441 pub fn with_seed(mut self, seed: SecretVec<u8>) -> Self {
443 self.seed = Some(seed);
444 self
445 }
446
447 #[cfg(test)]
449 pub(crate) fn ignore_seed_relevance(mut self) -> Self {
450 self.verify_seed_relevance = false;
451 self
452 }
453
454 pub fn with_external_migrations(
504 mut self,
505 migrations: Vec<Box<dyn RusqliteMigration<Error = WalletMigrationError>>>,
506 ) -> Self {
507 self.external_migrations = Some(migrations);
508 self
509 }
510
511 pub fn init_or_migrate<
514 C: BorrowMut<rusqlite::Connection>,
515 P: consensus::Parameters + 'static,
516 CL: Clock + Clone + 'static,
517 R: RngCore + Clone + 'static,
518 >(
519 self,
520 wdb: &mut WalletDb<C, P, CL, R>,
521 ) -> Result<(), MigratorError<Uuid, WalletMigrationError>> {
522 self.init_or_migrate_to(wdb, &[])
523 }
524
525 pub(crate) fn init_or_migrate_to<
528 C: BorrowMut<rusqlite::Connection>,
529 P: consensus::Parameters + 'static,
530 CL: Clock + Clone + 'static,
531 R: RngCore + Clone + 'static,
532 >(
533 self,
534 wdb: &mut WalletDb<C, P, CL, R>,
535 target_migrations: &[Uuid],
536 ) -> Result<(), MigratorError<Uuid, WalletMigrationError>> {
537 init_wallet_db_internal(
538 wdb,
539 self.seed,
540 self.external_migrations,
541 target_migrations,
542 self.verify_seed_relevance,
543 )
544 }
545}
546
547fn init_wallet_db_internal<
548 C: BorrowMut<rusqlite::Connection>,
549 P: consensus::Parameters + 'static,
550 CL: Clock + Clone + 'static,
551 R: RngCore + Clone + 'static,
552>(
553 wdb: &mut WalletDb<C, P, CL, R>,
554 seed: Option<SecretVec<u8>>,
555 external_migrations: Option<Vec<Box<dyn RusqliteMigration<Error = WalletMigrationError>>>>,
556 target_migrations: &[Uuid],
557 verify_seed_relevance: bool,
558) -> Result<(), MigratorError<Uuid, WalletMigrationError>> {
559 let seed = seed.map(Rc::new);
560
561 verify_sqlite_version_compatibility(wdb.conn.borrow()).map_err(MigratorError::Adapter)?;
562
563 wdb.conn
569 .borrow()
570 .execute_batch("PRAGMA foreign_keys = OFF;")
571 .map_err(|e| MigratorError::Adapter(WalletMigrationError::from(e)))?;
572
573 {
579 let adapter = RusqliteAdapter::<'_, WalletMigrationError>::new(
580 wdb.conn.borrow_mut(),
581 Some(MIGRATIONS_TABLE.to_string()),
582 );
583 adapter.init().expect("Migrations table setup succeeds.");
584 }
585
586 verify_network_compatibility(wdb.conn.borrow(), &wdb.params).map_err(MigratorError::Adapter)?;
590
591 let adapter = RusqliteAdapter::new(wdb.conn.borrow_mut(), Some(MIGRATIONS_TABLE.to_string()));
594 let mut migrator = Migrator::new(adapter);
595 migrator
596 .register_multiple(
597 migrations::all_migrations(
598 &wdb.params,
599 wdb.clock.clone(),
600 wdb.rng.clone(),
601 seed.clone(),
602 )
603 .into_iter(),
604 )
605 .expect("Wallet migration registration should have been successful.");
606 if let Some(migrations) = external_migrations {
607 migrator.register_multiple(migrations.into_iter())?;
608 }
609 if target_migrations.is_empty() {
610 migrator.up(None)?;
611 } else {
612 for target_migration in target_migrations {
613 migrator.up(Some(*target_migration))?;
614 }
615 }
616 wdb.conn
617 .borrow()
618 .execute("PRAGMA foreign_keys = ON", [])
619 .map_err(|e| MigratorError::Adapter(WalletMigrationError::from(e)))?;
620
621 if verify_seed_relevance {
627 if let Some(seed) = seed {
628 match wdb
629 .seed_relevance_to_derived_accounts(&seed)
630 .map_err(sqlite_client_error_to_wallet_migration_error)?
631 {
632 SeedRelevance::Relevant { .. } => (),
633 SeedRelevance::NoAccounts => (),
636 SeedRelevance::NotRelevant | SeedRelevance::NoDerivedAccounts => {
638 return Err(WalletMigrationError::SeedNotRelevant.into())
639 }
640 }
641 }
642 }
643
644 Ok(())
645}
646
647fn verify_sqlite_version_compatibility(
651 conn: &rusqlite::Connection,
652) -> Result<(), WalletMigrationError> {
653 let sqlite_version =
654 conn.query_row("SELECT sqlite_version()", [], |row| row.get::<_, String>(0))?;
655
656 let version_re = Regex::new(r"^(?<major>[0-9]+)\.(?<minor>[0-9]+).*$").unwrap();
657 let captures =
658 version_re
659 .captures(&sqlite_version)
660 .ok_or(WalletMigrationError::DatabaseNotSupported(
661 "Unknown".to_owned(),
662 ))?;
663 let parse_version_part = |part: &str| {
664 captures[part].parse::<u32>().map_err(|_| {
665 WalletMigrationError::CorruptedData(format!(
666 "Cannot decode SQLite {} version component {}",
667 part, &captures[part]
668 ))
669 })
670 };
671 let major = parse_version_part("major")?;
672 let minor = parse_version_part("minor")?;
673
674 if major != SQLITE_MAJOR_VERSION || minor < MIN_SQLITE_MINOR_VERSION {
675 Err(WalletMigrationError::DatabaseNotSupported(sqlite_version))
676 } else {
677 Ok(())
678 }
679}
680
681#[cfg(test)]
682pub(crate) mod testing {
683 use rand::RngCore;
684 use schemerz::MigratorError;
685 use secrecy::SecretVec;
686 use uuid::Uuid;
687 use zcash_protocol::consensus;
688
689 use crate::{util::Clock, WalletDb};
690
691 use super::WalletMigrationError;
692
693 pub(crate) fn init_wallet_db<
694 P: consensus::Parameters + 'static,
695 CL: Clock + Clone + 'static,
696 R: RngCore + Clone + 'static,
697 >(
698 wdb: &mut WalletDb<rusqlite::Connection, P, CL, R>,
699 seed: Option<SecretVec<u8>>,
700 ) -> Result<(), MigratorError<Uuid, WalletMigrationError>> {
701 super::init_wallet_db_internal(wdb, seed, None, &[], true)
702 }
703}
704
705#[cfg(test)]
706mod tests {
707 use rand::RngCore;
708 use rusqlite::{self, named_params, Connection, ToSql};
709 use secrecy::Secret;
710
711 use tempfile::NamedTempFile;
712
713 use ::sapling::zip32::ExtendedFullViewingKey;
714 use zcash_client_backend::data_api::testing::TestBuilder;
715 use zcash_keys::{
716 address::Address,
717 encoding::{encode_extended_full_viewing_key, encode_payment_address},
718 keys::{
719 sapling, ReceiverRequirement::*, UnifiedAddressRequest, UnifiedFullViewingKey,
720 UnifiedSpendingKey,
721 },
722 };
723 use zcash_primitives::transaction::{TransactionData, TxVersion};
724 use zcash_protocol::consensus::{self, BlockHeight, BranchId, Network, NetworkConstants};
725 use zip32::AccountId;
726
727 use super::testing::init_wallet_db;
728 use crate::{
729 testing::db::{test_clock, test_rng, TestDbFactory},
730 util::Clock,
731 wallet::db,
732 WalletDb, UA_TRANSPARENT,
733 };
734
735 #[cfg(feature = "transparent-inputs")]
736 use {
737 super::WalletMigrationError,
738 crate::wallet::{self, pool_code, PoolType},
739 zcash_address::test_vectors,
740 zcash_client_backend::data_api::WalletWrite,
741 zip32::DiversifierIndex,
742 };
743
744 pub(crate) fn describe_tables(conn: &Connection) -> Result<Vec<String>, rusqlite::Error> {
745 let result = conn
746 .prepare("SELECT sql FROM sqlite_schema WHERE type = 'table' ORDER BY tbl_name")?
747 .query_and_then([], |row| row.get::<_, String>(0))?
748 .collect::<Result<Vec<_>, _>>()?;
749
750 Ok(result)
751 }
752
753 #[test]
754 fn verify_schema() {
755 let st = TestBuilder::new()
756 .with_data_store_factory(TestDbFactory::default())
757 .build();
758
759 use regex::Regex;
760 let re = Regex::new(r"\s+").unwrap();
761
762 let expected_tables = vec![
763 db::TABLE_ACCOUNTS,
764 db::TABLE_ADDRESSES,
765 db::TABLE_BLOCKS,
766 db::TABLE_NULLIFIER_MAP,
767 db::TABLE_ORCHARD_RECEIVED_NOTE_SPENDS,
768 db::TABLE_ORCHARD_RECEIVED_NOTES,
769 db::TABLE_ORCHARD_TREE_CAP,
770 db::TABLE_ORCHARD_TREE_CHECKPOINT_MARKS_REMOVED,
771 db::TABLE_ORCHARD_TREE_CHECKPOINTS,
772 db::TABLE_ORCHARD_TREE_SHARDS,
773 db::TABLE_SAPLING_RECEIVED_NOTE_SPENDS,
774 db::TABLE_SAPLING_RECEIVED_NOTES,
775 db::TABLE_SAPLING_TREE_CAP,
776 db::TABLE_SAPLING_TREE_CHECKPOINT_MARKS_REMOVED,
777 db::TABLE_SAPLING_TREE_CHECKPOINTS,
778 db::TABLE_SAPLING_TREE_SHARDS,
779 db::TABLE_SCAN_QUEUE,
780 db::TABLE_SCHEMERZ_MIGRATIONS,
781 db::TABLE_SENT_NOTES,
782 db::TABLE_SQLITE_SEQUENCE,
783 db::TABLE_TRANSACTIONS,
784 db::TABLE_TRANSPARENT_RECEIVED_OUTPUT_SPENDS,
785 db::TABLE_TRANSPARENT_RECEIVED_OUTPUTS,
786 db::TABLE_TRANSPARENT_SPEND_MAP,
787 db::TABLE_TRANSPARENT_SPEND_SEARCH_QUEUE,
788 db::TABLE_TX_LOCATOR_MAP,
789 db::TABLE_TX_RETRIEVAL_QUEUE,
790 ];
791
792 let rows = describe_tables(&st.wallet().db().conn).unwrap();
793 assert_eq!(rows.len(), expected_tables.len());
794 for (actual, expected) in rows.iter().zip(expected_tables.iter()) {
795 assert_eq!(
796 re.replace_all(actual, " "),
797 re.replace_all(expected, " ").trim(),
798 );
799 }
800
801 let expected_indices = vec![
802 db::INDEX_ACCOUNTS_UFVK,
803 db::INDEX_ACCOUNTS_UIVK,
804 db::INDEX_ACCOUNTS_UUID,
805 db::INDEX_HD_ACCOUNT,
806 db::INDEX_ADDRESSES_ACCOUNTS,
807 db::INDEX_ADDRESSES_INDICES,
808 db::INDEX_ADDRESSES_T_INDICES,
809 db::INDEX_NF_MAP_LOCATOR_IDX,
810 db::INDEX_ORCHARD_RECEIVED_NOTES_ACCOUNT,
811 db::INDEX_ORCHARD_RECEIVED_NOTES_TX,
812 db::INDEX_SAPLING_RECEIVED_NOTES_ACCOUNT,
813 db::INDEX_SAPLING_RECEIVED_NOTES_TX,
814 db::INDEX_SENT_NOTES_FROM_ACCOUNT,
815 db::INDEX_SENT_NOTES_TO_ACCOUNT,
816 db::INDEX_SENT_NOTES_TX,
817 db::INDEX_TRANSPARENT_RECEIVED_OUTPUTS_ACCOUNT_ID,
818 ];
819 let mut indices_query = st
820 .wallet()
821 .db()
822 .conn
823 .prepare("SELECT sql FROM sqlite_master WHERE type = 'index' AND sql != '' ORDER BY tbl_name, name")
824 .unwrap();
825 let mut rows = indices_query.query([]).unwrap();
826 let mut expected_idx = 0;
827 while let Some(row) = rows.next().unwrap() {
828 let sql: String = row.get(0).unwrap();
829 assert_eq!(
830 re.replace_all(&sql, " "),
831 re.replace_all(expected_indices[expected_idx], " ").trim(),
832 );
833 expected_idx += 1;
834 }
835
836 let expected_views = vec![
837 db::VIEW_ADDRESS_FIRST_USE.to_owned(),
838 db::VIEW_ADDRESS_USES.to_owned(),
839 db::view_orchard_shard_scan_ranges(st.network()),
840 db::view_orchard_shard_unscanned_ranges(),
841 db::VIEW_ORCHARD_SHARDS_SCAN_STATE.to_owned(),
842 db::VIEW_RECEIVED_OUTPUT_SPENDS.to_owned(),
843 db::VIEW_RECEIVED_OUTPUTS.to_owned(),
844 db::view_sapling_shard_scan_ranges(st.network()),
845 db::view_sapling_shard_unscanned_ranges(),
846 db::VIEW_SAPLING_SHARDS_SCAN_STATE.to_owned(),
847 db::VIEW_TRANSACTIONS.to_owned(),
848 db::VIEW_TX_OUTPUTS.to_owned(),
849 ];
850
851 let mut views_query = st
852 .wallet()
853 .db()
854 .conn
855 .prepare("SELECT sql FROM sqlite_schema WHERE type = 'view' ORDER BY tbl_name")
856 .unwrap();
857 let mut rows = views_query.query([]).unwrap();
858 let mut expected_idx = 0;
859 while let Some(row) = rows.next().unwrap() {
860 let sql: String = row.get(0).unwrap();
861 assert_eq!(
862 re.replace_all(&sql, " "),
863 re.replace_all(&expected_views[expected_idx], " ").trim(),
864 );
865 expected_idx += 1;
866 }
867 }
868
869 #[test]
870 fn external_schema_prefix_unused() {
871 let st = TestBuilder::new()
872 .with_data_store_factory(TestDbFactory::default())
873 .build();
874
875 let mut names_query = st
876 .wallet()
877 .db()
878 .conn
879 .prepare("SELECT tbl_name FROM sqlite_schema")
880 .unwrap();
881 let mut rows = names_query.query([]).unwrap();
882 while let Some(row) = rows.next().unwrap() {
883 let name: String = row.get(0).unwrap();
884 assert!(!name.starts_with("ext_"));
885 }
886 }
887
888 #[test]
889 fn init_migrate_from_0_3_0() {
890 fn init_0_3_0<P: consensus::Parameters, CL: Clock + Clone, R: RngCore + Clone>(
891 wdb: &mut WalletDb<rusqlite::Connection, P, CL, R>,
892 extfvk: &ExtendedFullViewingKey,
893 account: AccountId,
894 ) -> Result<(), rusqlite::Error> {
895 wdb.conn.execute(
896 "CREATE TABLE accounts (
897 account INTEGER PRIMARY KEY,
898 extfvk TEXT NOT NULL,
899 address TEXT NOT NULL
900 )",
901 [],
902 )?;
903 wdb.conn.execute(
904 "CREATE TABLE blocks (
905 height INTEGER PRIMARY KEY,
906 hash BLOB NOT NULL,
907 time INTEGER NOT NULL,
908 sapling_tree BLOB NOT NULL
909 )",
910 [],
911 )?;
912 wdb.conn.execute(
913 "CREATE TABLE transactions (
914 id_tx INTEGER PRIMARY KEY,
915 txid BLOB NOT NULL UNIQUE,
916 created TEXT,
917 block INTEGER,
918 tx_index INTEGER,
919 expiry_height INTEGER,
920 raw BLOB,
921 FOREIGN KEY (block) REFERENCES blocks(height)
922 )",
923 [],
924 )?;
925 wdb.conn.execute(
926 "CREATE TABLE received_notes (
927 id_note INTEGER PRIMARY KEY,
928 tx INTEGER NOT NULL,
929 output_index INTEGER NOT NULL,
930 account INTEGER NOT NULL,
931 diversifier BLOB NOT NULL,
932 value INTEGER NOT NULL,
933 rcm BLOB NOT NULL,
934 nf BLOB NOT NULL UNIQUE,
935 is_change INTEGER NOT NULL,
936 memo BLOB,
937 spent INTEGER,
938 FOREIGN KEY (tx) REFERENCES transactions(id_tx),
939 FOREIGN KEY (account) REFERENCES accounts(account),
940 FOREIGN KEY (spent) REFERENCES transactions(id_tx),
941 CONSTRAINT tx_output UNIQUE (tx, output_index)
942 )",
943 [],
944 )?;
945 wdb.conn.execute(
946 "CREATE TABLE sapling_witnesses (
947 id_witness INTEGER PRIMARY KEY,
948 note INTEGER NOT NULL,
949 block INTEGER NOT NULL,
950 witness BLOB NOT NULL,
951 FOREIGN KEY (note) REFERENCES received_notes(id_note),
952 FOREIGN KEY (block) REFERENCES blocks(height),
953 CONSTRAINT witness_height UNIQUE (note, block)
954 )",
955 [],
956 )?;
957 wdb.conn.execute(
958 "CREATE TABLE sent_notes (
959 id_note INTEGER PRIMARY KEY,
960 tx INTEGER NOT NULL,
961 output_index INTEGER NOT NULL,
962 from_account INTEGER NOT NULL,
963 address TEXT NOT NULL,
964 value INTEGER NOT NULL,
965 memo BLOB,
966 FOREIGN KEY (tx) REFERENCES transactions(id_tx),
967 FOREIGN KEY (from_account) REFERENCES accounts(account),
968 CONSTRAINT tx_output UNIQUE (tx, output_index)
969 )",
970 [],
971 )?;
972
973 let address = encode_payment_address(
974 wdb.params.hrp_sapling_payment_address(),
975 &extfvk.default_address().1,
976 );
977 let extfvk = encode_extended_full_viewing_key(
978 wdb.params.hrp_sapling_extended_full_viewing_key(),
979 extfvk,
980 );
981 wdb.conn.execute(
982 "INSERT INTO accounts (account, extfvk, address)
983 VALUES (?, ?, ?)",
984 [
985 u32::from(account).to_sql()?,
986 extfvk.to_sql()?,
987 address.to_sql()?,
988 ],
989 )?;
990
991 Ok(())
992 }
993
994 let data_file = NamedTempFile::new().unwrap();
995 let mut db_data = WalletDb::for_path(
996 data_file.path(),
997 Network::TestNetwork,
998 test_clock(),
999 test_rng(),
1000 )
1001 .unwrap();
1002
1003 let seed = [0xab; 32];
1004 let account = AccountId::ZERO;
1005 let secret_key = sapling::spending_key(&seed, db_data.params.coin_type(), account);
1006 #[allow(deprecated)]
1007 let extfvk = secret_key.to_extended_full_viewing_key();
1008
1009 init_0_3_0(&mut db_data, &extfvk, account).unwrap();
1010 assert_matches!(
1011 init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))),
1012 Ok(_)
1013 );
1014 }
1015
1016 #[test]
1017 fn init_migrate_from_autoshielding_poc() {
1018 fn init_autoshielding<P: consensus::Parameters, CL, R>(
1019 wdb: &mut WalletDb<rusqlite::Connection, P, CL, R>,
1020 extfvk: &ExtendedFullViewingKey,
1021 account: AccountId,
1022 ) -> Result<(), rusqlite::Error> {
1023 wdb.conn.execute(
1024 "CREATE TABLE accounts (
1025 account INTEGER PRIMARY KEY,
1026 extfvk TEXT NOT NULL,
1027 address TEXT NOT NULL,
1028 transparent_address TEXT NOT NULL
1029 )",
1030 [],
1031 )?;
1032 wdb.conn.execute(
1033 "CREATE TABLE blocks (
1034 height INTEGER PRIMARY KEY,
1035 hash BLOB NOT NULL,
1036 time INTEGER NOT NULL,
1037 sapling_tree BLOB NOT NULL
1038 )",
1039 [],
1040 )?;
1041 wdb.conn.execute(
1042 "CREATE TABLE transactions (
1043 id_tx INTEGER PRIMARY KEY,
1044 txid BLOB NOT NULL UNIQUE,
1045 created TEXT,
1046 block INTEGER,
1047 tx_index INTEGER,
1048 expiry_height INTEGER,
1049 raw BLOB,
1050 FOREIGN KEY (block) REFERENCES blocks(height)
1051 )",
1052 [],
1053 )?;
1054 wdb.conn.execute(
1055 "CREATE TABLE received_notes (
1056 id_note INTEGER PRIMARY KEY,
1057 tx INTEGER NOT NULL,
1058 output_index INTEGER NOT NULL,
1059 account INTEGER NOT NULL,
1060 diversifier BLOB NOT NULL,
1061 value INTEGER NOT NULL,
1062 rcm BLOB NOT NULL,
1063 nf BLOB NOT NULL UNIQUE,
1064 is_change INTEGER NOT NULL,
1065 memo BLOB,
1066 spent INTEGER,
1067 FOREIGN KEY (tx) REFERENCES transactions(id_tx),
1068 FOREIGN KEY (account) REFERENCES accounts(account),
1069 FOREIGN KEY (spent) REFERENCES transactions(id_tx),
1070 CONSTRAINT tx_output UNIQUE (tx, output_index)
1071 )",
1072 [],
1073 )?;
1074 wdb.conn.execute(
1075 "CREATE TABLE sapling_witnesses (
1076 id_witness INTEGER PRIMARY KEY,
1077 note INTEGER NOT NULL,
1078 block INTEGER NOT NULL,
1079 witness BLOB NOT NULL,
1080 FOREIGN KEY (note) REFERENCES received_notes(id_note),
1081 FOREIGN KEY (block) REFERENCES blocks(height),
1082 CONSTRAINT witness_height UNIQUE (note, block)
1083 )",
1084 [],
1085 )?;
1086 wdb.conn.execute(
1087 "CREATE TABLE sent_notes (
1088 id_note INTEGER PRIMARY KEY,
1089 tx INTEGER NOT NULL,
1090 output_index INTEGER NOT NULL,
1091 from_account INTEGER NOT NULL,
1092 address TEXT NOT NULL,
1093 value INTEGER NOT NULL,
1094 memo BLOB,
1095 FOREIGN KEY (tx) REFERENCES transactions(id_tx),
1096 FOREIGN KEY (from_account) REFERENCES accounts(account),
1097 CONSTRAINT tx_output UNIQUE (tx, output_index)
1098 )",
1099 [],
1100 )?;
1101 wdb.conn.execute(
1102 "CREATE TABLE utxos (
1103 id_utxo INTEGER PRIMARY KEY,
1104 address TEXT NOT NULL,
1105 prevout_txid BLOB NOT NULL,
1106 prevout_idx INTEGER NOT NULL,
1107 script BLOB NOT NULL,
1108 value_zat INTEGER NOT NULL,
1109 height INTEGER NOT NULL,
1110 spent_in_tx INTEGER,
1111 FOREIGN KEY (spent_in_tx) REFERENCES transactions(id_tx),
1112 CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx)
1113 )",
1114 [],
1115 )?;
1116
1117 let address = encode_payment_address(
1118 wdb.params.hrp_sapling_payment_address(),
1119 &extfvk.default_address().1,
1120 );
1121 let extfvk = encode_extended_full_viewing_key(
1122 wdb.params.hrp_sapling_extended_full_viewing_key(),
1123 extfvk,
1124 );
1125 wdb.conn.execute(
1126 "INSERT INTO accounts (account, extfvk, address, transparent_address)
1127 VALUES (?, ?, ?, '')",
1128 [
1129 u32::from(account).to_sql()?,
1130 extfvk.to_sql()?,
1131 address.to_sql()?,
1132 ],
1133 )?;
1134
1135 wdb.conn.execute(
1137 "INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (0, 0, 0, x'000000')",
1138 [],
1139 )?;
1140
1141 let tx = TransactionData::from_parts(
1142 TxVersion::V4,
1143 BranchId::Canopy,
1144 0,
1145 BlockHeight::from(0),
1146 None,
1147 None,
1148 None,
1149 None,
1150 )
1151 .freeze()
1152 .unwrap();
1153
1154 let mut tx_bytes = vec![];
1155 tx.write(&mut tx_bytes).unwrap();
1156 wdb.conn.execute(
1157 "INSERT INTO transactions (block, id_tx, txid, raw) VALUES (0, 0, :txid, :tx_bytes)",
1158 named_params![
1159 ":txid": tx.txid().as_ref(),
1160 ":tx_bytes": &tx_bytes[..]
1161 ],
1162 )?;
1163 wdb.conn.execute(
1164 "INSERT INTO sent_notes (tx, output_index, from_account, address, value)
1165 VALUES (0, 0, ?, ?, 0)",
1166 [u32::from(account).to_sql()?, address.to_sql()?],
1167 )?;
1168
1169 Ok(())
1170 }
1171
1172 let data_file = NamedTempFile::new().unwrap();
1173 let mut db_data = WalletDb::for_path(
1174 data_file.path(),
1175 Network::TestNetwork,
1176 test_clock(),
1177 test_rng(),
1178 )
1179 .unwrap();
1180
1181 let seed = [0xab; 32];
1182 let account = AccountId::ZERO;
1183 let secret_key = sapling::spending_key(&seed, db_data.params.coin_type(), account);
1184 #[allow(deprecated)]
1185 let extfvk = secret_key.to_extended_full_viewing_key();
1186
1187 init_autoshielding(&mut db_data, &extfvk, account).unwrap();
1188 assert_matches!(
1189 init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))),
1190 Ok(_)
1191 );
1192 }
1193
1194 #[test]
1195 fn init_migrate_from_main_pre_migrations() {
1196 fn init_main<P: consensus::Parameters, CL, R>(
1197 wdb: &mut WalletDb<rusqlite::Connection, P, CL, R>,
1198 ufvk: &UnifiedFullViewingKey,
1199 account: AccountId,
1200 ) -> Result<(), rusqlite::Error> {
1201 wdb.conn.execute(
1202 "CREATE TABLE accounts (
1203 account INTEGER PRIMARY KEY,
1204 ufvk TEXT,
1205 address TEXT,
1206 transparent_address TEXT
1207 )",
1208 [],
1209 )?;
1210 wdb.conn.execute(
1211 "CREATE TABLE blocks (
1212 height INTEGER PRIMARY KEY,
1213 hash BLOB NOT NULL,
1214 time INTEGER NOT NULL,
1215 sapling_tree BLOB NOT NULL
1216 )",
1217 [],
1218 )?;
1219 wdb.conn.execute(
1220 "CREATE TABLE transactions (
1221 id_tx INTEGER PRIMARY KEY,
1222 txid BLOB NOT NULL UNIQUE,
1223 created TEXT,
1224 block INTEGER,
1225 tx_index INTEGER,
1226 expiry_height INTEGER,
1227 raw BLOB,
1228 FOREIGN KEY (block) REFERENCES blocks(height)
1229 )",
1230 [],
1231 )?;
1232 wdb.conn.execute(
1233 "CREATE TABLE received_notes (
1234 id_note INTEGER PRIMARY KEY,
1235 tx INTEGER NOT NULL,
1236 output_index INTEGER NOT NULL,
1237 account INTEGER NOT NULL,
1238 diversifier BLOB NOT NULL,
1239 value INTEGER NOT NULL,
1240 rcm BLOB NOT NULL,
1241 nf BLOB NOT NULL UNIQUE,
1242 is_change INTEGER NOT NULL,
1243 memo BLOB,
1244 spent INTEGER,
1245 FOREIGN KEY (tx) REFERENCES transactions(id_tx),
1246 FOREIGN KEY (account) REFERENCES accounts(account),
1247 FOREIGN KEY (spent) REFERENCES transactions(id_tx),
1248 CONSTRAINT tx_output UNIQUE (tx, output_index)
1249 )",
1250 [],
1251 )?;
1252 wdb.conn.execute(
1253 "CREATE TABLE sapling_witnesses (
1254 id_witness INTEGER PRIMARY KEY,
1255 note INTEGER NOT NULL,
1256 block INTEGER NOT NULL,
1257 witness BLOB NOT NULL,
1258 FOREIGN KEY (note) REFERENCES received_notes(id_note),
1259 FOREIGN KEY (block) REFERENCES blocks(height),
1260 CONSTRAINT witness_height UNIQUE (note, block)
1261 )",
1262 [],
1263 )?;
1264 wdb.conn.execute(
1265 "CREATE TABLE sent_notes (
1266 id_note INTEGER PRIMARY KEY,
1267 tx INTEGER NOT NULL,
1268 output_pool INTEGER NOT NULL,
1269 output_index INTEGER NOT NULL,
1270 from_account INTEGER NOT NULL,
1271 address TEXT NOT NULL,
1272 value INTEGER NOT NULL,
1273 memo BLOB,
1274 FOREIGN KEY (tx) REFERENCES transactions(id_tx),
1275 FOREIGN KEY (from_account) REFERENCES accounts(account),
1276 CONSTRAINT tx_output UNIQUE (tx, output_pool, output_index)
1277 )",
1278 [],
1279 )?;
1280 wdb.conn.execute(
1281 "CREATE TABLE utxos (
1282 id_utxo INTEGER PRIMARY KEY,
1283 address TEXT NOT NULL,
1284 prevout_txid BLOB NOT NULL,
1285 prevout_idx INTEGER NOT NULL,
1286 script BLOB NOT NULL,
1287 value_zat INTEGER NOT NULL,
1288 height INTEGER NOT NULL,
1289 spent_in_tx INTEGER,
1290 FOREIGN KEY (spent_in_tx) REFERENCES transactions(id_tx),
1291 CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx)
1292 )",
1293 [],
1294 )?;
1295
1296 let ufvk_str = ufvk.encode(&wdb.params);
1297
1298 let ua_request = UnifiedAddressRequest::unsafe_custom(Omit, Require, UA_TRANSPARENT);
1301 let address_str = Address::Unified(
1302 ufvk.default_address(ua_request)
1303 .expect("A valid default address exists for the UFVK")
1304 .0,
1305 )
1306 .encode(&wdb.params);
1307 wdb.conn.execute(
1308 "INSERT INTO accounts (account, ufvk, address, transparent_address)
1309 VALUES (?, ?, ?, '')",
1310 [
1311 u32::from(account).to_sql()?,
1312 ufvk_str.to_sql()?,
1313 address_str.to_sql()?,
1314 ],
1315 )?;
1316
1317 #[cfg(feature = "transparent-inputs")]
1319 {
1320 let taddr = Address::Transparent(
1321 *ufvk
1322 .default_address(ua_request)
1323 .expect("A valid default address exists for the UFVK")
1324 .0
1325 .transparent()
1326 .unwrap(),
1327 )
1328 .encode(&wdb.params);
1329 wdb.conn.execute(
1330 "INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (0, 0, 0, x'000000')",
1331 [],
1332 )?;
1333 wdb.conn.execute(
1334 "INSERT INTO transactions (block, id_tx, txid) VALUES (0, 0, '')",
1335 [],
1336 )?;
1337 wdb.conn.execute(
1338 "INSERT INTO sent_notes (tx, output_pool, output_index, from_account, address, value)
1339 VALUES (0, ?, 0, ?, ?, 0)",
1340 [pool_code(PoolType::TRANSPARENT).to_sql()?, u32::from(account).to_sql()?, taddr.to_sql()?])?;
1341 }
1342
1343 Ok(())
1344 }
1345
1346 let data_file = NamedTempFile::new().unwrap();
1347 let mut db_data = WalletDb::for_path(
1348 data_file.path(),
1349 Network::TestNetwork,
1350 test_clock(),
1351 test_rng(),
1352 )
1353 .unwrap();
1354
1355 let seed = [0xab; 32];
1356 let account = AccountId::ZERO;
1357 let secret_key = UnifiedSpendingKey::from_seed(&db_data.params, &seed, account).unwrap();
1358
1359 init_main(
1360 &mut db_data,
1361 &secret_key.to_unified_full_viewing_key(),
1362 account,
1363 )
1364 .unwrap();
1365 assert_matches!(
1366 init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))),
1367 Ok(_)
1368 );
1369 }
1370
1371 #[test]
1372 #[cfg(feature = "transparent-inputs")]
1373 fn account_produces_expected_ua_sequence() {
1374 use zcash_client_backend::data_api::{AccountBirthday, AccountSource, WalletRead};
1375 use zcash_primitives::block::BlockHash;
1376
1377 let network = Network::MainNetwork;
1378 let data_file = NamedTempFile::new().unwrap();
1379 let mut db_data =
1380 WalletDb::for_path(data_file.path(), network, test_clock(), test_rng()).unwrap();
1381 assert_matches!(init_wallet_db(&mut db_data, None), Ok(_));
1382
1383 let seed = test_vectors::UNIFIED[0].root_seed;
1385 let other_seed = [7; 32];
1386 assert_matches!(
1387 init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))),
1388 Ok(())
1389 );
1390 assert_matches!(
1391 init_wallet_db(&mut db_data, Some(Secret::new(other_seed.to_vec()))),
1392 Ok(())
1393 );
1394
1395 let birthday = AccountBirthday::from_sapling_activation(&network, BlockHash([0; 32]));
1396 let (account_id, _usk) = db_data
1397 .create_account("", &Secret::new(seed.to_vec()), &birthday, None)
1398 .unwrap();
1399
1400 db_data.update_chain_tip(birthday.height()).unwrap();
1403
1404 assert_matches!(
1405 db_data.get_account(account_id),
1406 Ok(Some(account)) if matches!(
1407 &account.kind,
1408 AccountSource::Derived{derivation, ..} if derivation.account_index() == zip32::AccountId::ZERO,
1409 )
1410 );
1411
1412 assert_matches!(
1414 init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))),
1415 Ok(())
1416 );
1417 assert_matches!(
1418 init_wallet_db(&mut db_data, Some(Secret::new(other_seed.to_vec()))),
1419 Err(schemerz::MigratorError::Adapter(
1420 WalletMigrationError::SeedNotRelevant
1421 ))
1422 );
1423
1424 for tv in &test_vectors::UNIFIED[..3] {
1425 if let Some(Address::Unified(tvua)) =
1426 Address::decode(&Network::MainNetwork, tv.unified_addr)
1427 {
1428 let ua_request = UnifiedAddressRequest::unsafe_custom(Omit, Require, Require);
1430
1431 let (ua, di) = wallet::get_last_generated_address_matching(
1432 &db_data.conn,
1433 &db_data.params,
1434 account_id,
1435 if tv.diversifier_index == 0 {
1436 UnifiedAddressRequest::AllAvailableKeys
1437 } else {
1438 ua_request
1439 },
1440 )
1441 .unwrap()
1442 .expect("create_account generated the first address");
1443 assert_eq!(DiversifierIndex::from(tv.diversifier_index), di);
1444 assert_eq!(tvua.transparent(), ua.transparent());
1445 assert_eq!(tvua.sapling(), ua.sapling());
1446 #[cfg(not(feature = "orchard"))]
1447 assert_eq!(tv.unified_addr, ua.encode(&Network::MainNetwork));
1448
1449 db_data
1450 .get_next_available_address(account_id, ua_request)
1451 .unwrap()
1452 .expect("get_next_available_address generated an address");
1453 } else {
1454 panic!(
1455 "{} did not decode to a valid unified address",
1456 tv.unified_addr
1457 );
1458 }
1459 }
1460 }
1461}