//! Functions for initializing the various databases. use rusqlite::{types::ToSql, Connection, NO_PARAMS}; use std::path::Path; use zcash_client_backend::{ constants::testnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, encoding::encode_extended_full_viewing_key, }; use zcash_primitives::{block::BlockHash, zip32::ExtendedFullViewingKey}; use crate::{ address_from_extfvk, error::{Error, ErrorKind}, }; /// Sets up the internal structure of the cache database. /// /// # Examples /// /// ``` /// use tempfile::NamedTempFile; /// use zcash_client_sqlite::init::init_cache_database; /// /// let data_file = NamedTempFile::new().unwrap(); /// let db_cache = data_file.path(); /// init_cache_database(&db_cache).unwrap(); /// ``` pub fn init_cache_database>(db_cache: P) -> Result<(), Error> { let cache = Connection::open(db_cache)?; cache.execute( "CREATE TABLE IF NOT EXISTS compactblocks ( height INTEGER PRIMARY KEY, data BLOB NOT NULL )", NO_PARAMS, )?; Ok(()) } /// Sets up the internal structure of the data database. /// /// # Examples /// /// ``` /// use tempfile::NamedTempFile; /// use zcash_client_sqlite::init::init_data_database; /// /// let data_file = NamedTempFile::new().unwrap(); /// let db_data = data_file.path(); /// init_data_database(&db_data).unwrap(); /// ``` pub fn init_data_database>(db_data: P) -> Result<(), Error> { let data = Connection::open(db_data)?; data.execute( "CREATE TABLE IF NOT EXISTS accounts ( account INTEGER PRIMARY KEY, extfvk TEXT NOT NULL, address TEXT NOT NULL )", NO_PARAMS, )?; data.execute( "CREATE TABLE IF NOT EXISTS blocks ( height INTEGER PRIMARY KEY, hash BLOB NOT NULL, time INTEGER NOT NULL, sapling_tree BLOB NOT NULL )", NO_PARAMS, )?; data.execute( "CREATE TABLE IF NOT EXISTS transactions ( id_tx INTEGER PRIMARY KEY, txid BLOB NOT NULL UNIQUE, created TEXT, block INTEGER, tx_index INTEGER, expiry_height INTEGER, raw BLOB, FOREIGN KEY (block) REFERENCES blocks(height) )", NO_PARAMS, )?; data.execute( "CREATE TABLE IF NOT EXISTS received_notes ( id_note INTEGER PRIMARY KEY, tx INTEGER NOT NULL, output_index INTEGER NOT NULL, account INTEGER NOT NULL, diversifier BLOB NOT NULL, value INTEGER NOT NULL, rcm BLOB NOT NULL, nf BLOB NOT NULL UNIQUE, is_change BOOLEAN NOT NULL, memo BLOB, spent INTEGER, FOREIGN KEY (tx) REFERENCES transactions(id_tx), FOREIGN KEY (account) REFERENCES accounts(account), FOREIGN KEY (spent) REFERENCES transactions(id_tx), CONSTRAINT tx_output UNIQUE (tx, output_index) )", NO_PARAMS, )?; data.execute( "CREATE TABLE IF NOT EXISTS sapling_witnesses ( id_witness INTEGER PRIMARY KEY, note INTEGER NOT NULL, block INTEGER NOT NULL, witness BLOB NOT NULL, FOREIGN KEY (note) REFERENCES received_notes(id_note), FOREIGN KEY (block) REFERENCES blocks(height), CONSTRAINT witness_height UNIQUE (note, block) )", NO_PARAMS, )?; data.execute( "CREATE TABLE IF NOT EXISTS sent_notes ( id_note INTEGER PRIMARY KEY, tx INTEGER NOT NULL, output_index INTEGER NOT NULL, from_account INTEGER NOT NULL, address TEXT NOT NULL, value INTEGER NOT NULL, memo BLOB, FOREIGN KEY (tx) REFERENCES transactions(id_tx), FOREIGN KEY (from_account) REFERENCES accounts(account), CONSTRAINT tx_output UNIQUE (tx, output_index) )", NO_PARAMS, )?; Ok(()) } /// Initialises the data database with the given [`ExtendedFullViewingKey`]s. /// /// The [`ExtendedFullViewingKey`]s are stored internally and used by other APIs such as /// [`get_address`], [`scan_cached_blocks`], and [`create_to_address`]. `extfvks` **MUST** /// be arranged in account-order; that is, the [`ExtendedFullViewingKey`] for ZIP 32 /// account `i` **MUST** be at `extfvks[i]`. /// /// # Examples /// /// ``` /// use tempfile::NamedTempFile; /// use zcash_client_sqlite::init::{init_accounts_table, init_data_database}; /// use zcash_primitives::zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}; /// /// let data_file = NamedTempFile::new().unwrap(); /// let db_data = data_file.path(); /// init_data_database(&db_data).unwrap(); /// /// let extsk = ExtendedSpendingKey::master(&[]); /// let extfvks = [ExtendedFullViewingKey::from(&extsk)]; /// init_accounts_table(&db_data, &extfvks).unwrap(); /// ``` /// /// [`get_address`]: crate::query::get_address /// [`scan_cached_blocks`]: crate::scan::scan_cached_blocks /// [`create_to_address`]: crate::transact::create_to_address pub fn init_accounts_table>( db_data: P, extfvks: &[ExtendedFullViewingKey], ) -> Result<(), Error> { let data = Connection::open(db_data)?; let mut empty_check = data.prepare("SELECT * FROM accounts LIMIT 1")?; if empty_check.exists(NO_PARAMS)? { return Err(Error(ErrorKind::TableNotEmpty)); } // Insert accounts atomically data.execute("BEGIN IMMEDIATE", NO_PARAMS)?; for (account, extfvk) in extfvks.iter().enumerate() { let address = address_from_extfvk(extfvk); let extfvk = encode_extended_full_viewing_key(HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, extfvk); data.execute( "INSERT INTO accounts (account, extfvk, address) VALUES (?, ?, ?)", &[ (account as u32).to_sql()?, extfvk.to_sql()?, address.to_sql()?, ], )?; } data.execute("COMMIT", NO_PARAMS)?; Ok(()) } /// Initialises the data database with the given block. /// /// This enables a newly-created database to be immediately-usable, without needing to /// synchronise historic blocks. /// /// # Examples /// /// ``` /// use zcash_client_sqlite::init::init_blocks_table; /// use zcash_primitives::block::BlockHash; /// /// // The block height. /// let height = 500_000; /// // The hash of the block header. /// let hash = BlockHash([0; 32]); /// // The nTime field from the block header. /// let time = 12_3456_7890; /// // The serialized Sapling commitment tree as of this block. /// // Pre-compute and hard-code, or obtain from a service. /// let sapling_tree = &[]; /// /// init_blocks_table("/path/to/data.db", height, hash, time, sapling_tree); /// ``` pub fn init_blocks_table>( db_data: P, height: i32, hash: BlockHash, time: u32, sapling_tree: &[u8], ) -> Result<(), Error> { let data = Connection::open(db_data)?; let mut empty_check = data.prepare("SELECT * FROM blocks LIMIT 1")?; if empty_check.exists(NO_PARAMS)? { return Err(Error(ErrorKind::TableNotEmpty)); } data.execute( "INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (?, ?, ?, ?)", &[ height.to_sql()?, hash.0.to_sql()?, time.to_sql()?, sapling_tree.to_sql()?, ], )?; Ok(()) } #[cfg(test)] mod tests { use tempfile::NamedTempFile; use zcash_primitives::{ block::BlockHash, zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, }; use super::{init_accounts_table, init_blocks_table, init_data_database}; #[test] fn init_accounts_table_only_works_once() { let data_file = NamedTempFile::new().unwrap(); let db_data = data_file.path(); init_data_database(&db_data).unwrap(); // We can call the function as many times as we want with no data init_accounts_table(&db_data, &[]).unwrap(); init_accounts_table(&db_data, &[]).unwrap(); // First call with data should initialise the accounts table let extfvks = [ExtendedFullViewingKey::from(&ExtendedSpendingKey::master( &[], ))]; init_accounts_table(&db_data, &extfvks).unwrap(); // Subsequent calls should return an error init_accounts_table(&db_data, &[]).unwrap_err(); init_accounts_table(&db_data, &extfvks).unwrap_err(); } #[test] fn init_blocks_table_only_works_once() { let data_file = NamedTempFile::new().unwrap(); let db_data = data_file.path(); init_data_database(&db_data).unwrap(); // First call with data should initialise the blocks table init_blocks_table(&db_data, 1, BlockHash([1; 32]), 1, &[]).unwrap(); // Subsequent calls should return an error init_blocks_table(&db_data, 2, BlockHash([2; 32]), 2, &[]).unwrap_err(); } }