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