SQLite database structure and initialisation

This commit is contained in:
Jack Grigg 2019-03-09 02:16:00 +00:00
parent 7134ab8215
commit c0cf55c127
6 changed files with 403 additions and 0 deletions

View File

@ -5,6 +5,7 @@ members = [
"group",
"pairing",
"zcash_client_backend",
"zcash_client_sqlite",
"zcash_history",
"zcash_primitives",
"zcash_proofs",

View File

@ -0,0 +1,21 @@
[package]
name = "zcash_client_sqlite"
description = "An SQLite-based Zcash light client"
version = "0.0.0"
authors = [
"Jack Grigg <jack@z.cash>",
]
homepage = "https://github.com/zcash/librustzcash"
repository = "https://github.com/zcash/librustzcash"
readme = "README.md"
license = "MIT OR Apache-2.0"
edition = "2018"
[dependencies]
rusqlite = { version = "0.20", features = ["bundled"] }
zcash_client_backend = { version = "0.2", path = "../zcash_client_backend" }
zcash_primitives = { version = "0.2", path = "../zcash_primitives" }
[dev-dependencies]
remove_dir_all = "=0.5.2" # tempfile dependency; 0.5.3 bumped the MSRV
tempfile = "3"

View File

@ -0,0 +1,21 @@
# zcash_client_sqlite
This library contains APIs that collectively implement a Zcash light client in
an SQLite database.
## License
Licensed under either of
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
### Contribution
Unless you explicitly state otherwise, any contribution intentionally
submitted for inclusion in the work by you, as defined in the Apache-2.0
license, shall be dual licensed as above, without any additional terms or
conditions.

View File

@ -0,0 +1,41 @@
use std::error;
use std::fmt;
#[derive(Debug)]
pub enum ErrorKind {
TableNotEmpty,
Database(rusqlite::Error),
}
#[derive(Debug)]
pub struct Error(pub(crate) ErrorKind);
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self.0 {
ErrorKind::TableNotEmpty => write!(f, "Table is not empty"),
ErrorKind::Database(e) => write!(f, "{}", e),
}
}
}
impl error::Error for Error {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match &self.0 {
ErrorKind::Database(e) => Some(e),
_ => None,
}
}
}
impl From<rusqlite::Error> for Error {
fn from(e: rusqlite::Error) -> Self {
Error(ErrorKind::Database(e))
}
}
impl Error {
pub fn kind(&self) -> &ErrorKind {
&self.0
}
}

View File

@ -0,0 +1,286 @@
//! 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<P: AsRef<Path>>(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<P: AsRef<Path>>(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<P: AsRef<Path>>(
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<P: AsRef<Path>>(
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();
}
}

View File

@ -0,0 +1,33 @@
//! *An SQLite-based Zcash light client.*
//!
//! `zcash_client_backend` contains a set of APIs that collectively implement an
//! SQLite-based light client for the Zcash network.
//!
//! # Design
//!
//! The light client is built around two SQLite databases:
//!
//! - A cache database, used to inform the light client about new [`CompactBlock`]s. It is
//! read-only within all light client APIs *except* for [`init_cache_database`] which
//! can be used to initialize the database.
//!
//! - A data database, where the light client's state is stored. It is read-write within
//! the light client APIs, and **assumed to be read-only outside these APIs**. Callers
//! **MUST NOT** write to the database without using these APIs. Callers **MAY** read
//! the database directly in order to extract information for display to users.
//!
//! [`CompactBlock`]: zcash_client_backend::proto::compact_formats::CompactBlock
//! [`init_cache_database`]: crate::init::init_cache_database
use zcash_client_backend::{
constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS, encoding::encode_payment_address,
};
use zcash_primitives::zip32::ExtendedFullViewingKey;
pub mod error;
pub mod init;
fn address_from_extfvk(extfvk: &ExtendedFullViewingKey) -> String {
let addr = extfvk.default_address().unwrap().1;
encode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS, &addr)
}