From a437df191ec4d7d81cba0bdabf0925d9615444c9 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Wed, 22 Jul 2020 20:44:06 -0600 Subject: [PATCH] Initial skeleton of low-level database access API. --- zcash_client_backend/src/data_api/chain.rs | 82 +++++++++++++ zcash_client_backend/src/data_api/error.rs | 131 +++++++++++++++++++++ zcash_client_backend/src/data_api/mod.rs | 98 +++++++++++++++ zcash_client_backend/src/lib.rs | 1 + zcash_client_backend/src/proto/mod.rs | 19 +-- zcash_primitives/src/consensus.rs | 32 ++--- 6 files changed, 339 insertions(+), 24 deletions(-) create mode 100644 zcash_client_backend/src/data_api/chain.rs create mode 100644 zcash_client_backend/src/data_api/error.rs create mode 100644 zcash_client_backend/src/data_api/mod.rs diff --git a/zcash_client_backend/src/data_api/chain.rs b/zcash_client_backend/src/data_api/chain.rs new file mode 100644 index 000000000..b388aee59 --- /dev/null +++ b/zcash_client_backend/src/data_api/chain.rs @@ -0,0 +1,82 @@ +use zcash_primitives::consensus::{self, NetworkUpgrade}; + +use crate::data_api::{ + error::{ChainInvalid, Error}, + CacheOps, DBOps, +}; + +pub const ANCHOR_OFFSET: u32 = 10; + +/// Checks that the scanned blocks in the data database, when combined with the recent +/// `CompactBlock`s in the cache database, form a valid chain. +/// +/// This function is built on the core assumption that the information provided in the +/// cache database is more likely to be accurate than the previously-scanned information. +/// This follows from the design (and trust) assumption that the `lightwalletd` server +/// provides accurate block information as of the time it was requested. +/// +/// Returns: +/// - `Ok(())` if the combined chain is valid. +/// - `Err(ErrorKind::InvalidChain(upper_bound, cause))` if the combined chain is invalid. +/// `upper_bound` is the height of the highest invalid block (on the assumption that the +/// highest block in the cache database is correct). +/// - `Err(e)` if there was an error during validation unrelated to chain validity. +/// +/// This function does not mutate either of the databases. +pub fn validate_combined_chain< + E, + P: consensus::Parameters, + C: CacheOps>, + D: DBOps>, +>( + parameters: &P, + cache: &C, + data: &D, +) -> Result<(), Error> { + let sapling_activation_height = parameters + .activation_height(NetworkUpgrade::Sapling) + .ok_or(Error::SaplingNotActive)?; + + // Recall where we synced up to previously. + // If we have never synced, use Sapling activation height to select all cached CompactBlocks. + let data_scan_max_height = data + .block_height_extrema()? + .map(|(_, max)| max) + .unwrap_or(sapling_activation_height - 1); + + // The cache will contain blocks above the maximum height of data in the database; + // validate from that maximum height up to the chain tip, returning the + // hash of the block at data_scan_max_height + let cached_hash_opt = cache.validate_chain(data_scan_max_height, |top_block, next_block| { + if next_block.height() != top_block.height() - 1 { + Err(ChainInvalid::block_height_mismatch( + top_block.height() - 1, + next_block.height(), + )) + } else if next_block.hash() != top_block.prev_hash() { + Err(ChainInvalid::prev_hash_mismatch(next_block.height())) + } else { + Ok(()) + } + })?; + + match (cached_hash_opt, data.get_block_hash(data_scan_max_height)?) { + (Some(cached_hash), Some(data_scan_max_hash)) => + // Cached blocks must hash-chain to the last scanned block. + { + if cached_hash == data_scan_max_hash { + Ok(()) + } else { + Err(ChainInvalid::prev_hash_mismatch::(data_scan_max_height)) + } + } + (Some(_), None) => Err(Error::CorruptedData( + "No block hash available at last scanned height.", + )), + (None, _) => + // No cached blocks are present, this is fine. + { + Ok(()) + } + } +} diff --git a/zcash_client_backend/src/data_api/error.rs b/zcash_client_backend/src/data_api/error.rs new file mode 100644 index 000000000..ba4376e22 --- /dev/null +++ b/zcash_client_backend/src/data_api/error.rs @@ -0,0 +1,131 @@ +use std::error; +use std::fmt; +use zcash_primitives::{ + consensus::BlockHeight, + sapling::Node, + transaction::{builder, TxId}, +}; + +#[derive(Debug)] +pub enum ChainInvalid { + PrevHashMismatch, + /// (expected_height, actual_height) + BlockHeightMismatch(BlockHeight), +} + +#[derive(Debug)] +pub enum Error { + CorruptedData(&'static str), + IncorrectHRPExtFVK, + InsufficientBalance(u64, u64), + InvalidChain(BlockHeight, ChainInvalid), + InvalidExtSK(u32), + InvalidMemo(std::str::Utf8Error), + InvalidNewWitnessAnchor(usize, TxId, BlockHeight, Node), + InvalidNote, + InvalidWitnessAnchor(i64, BlockHeight), + ScanRequired, + TableNotEmpty, + Bech32(bech32::Error), + Base58(bs58::decode::Error), + Builder(builder::Error), + Database(DbError), + Io(std::io::Error), + Protobuf(protobuf::ProtobufError), + SaplingNotActive, +} + +impl ChainInvalid { + pub fn prev_hash_mismatch(at_height: BlockHeight) -> Error { + Error::InvalidChain(at_height, ChainInvalid::PrevHashMismatch) + } + + pub fn block_height_mismatch(at_height: BlockHeight, found: BlockHeight) -> Error { + Error::InvalidChain(at_height, ChainInvalid::BlockHeightMismatch(found)) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self { + Error::CorruptedData(reason) => write!(f, "Data DB is corrupted: {}", reason), + Error::IncorrectHRPExtFVK => write!(f, "Incorrect HRP for extfvk"), + Error::InsufficientBalance(have, need) => write!( + f, + "Insufficient balance (have {}, need {} including fee)", + have, need + ), + Error::InvalidChain(upper_bound, cause) => { + write!(f, "Invalid chain (upper bound: {}): {:?}", u32::from(*upper_bound), cause) + } + Error::InvalidExtSK(account) => { + write!(f, "Incorrect ExtendedSpendingKey for account {}", account) + } + Error::InvalidMemo(e) => write!(f, "{}", e), + Error::InvalidNewWitnessAnchor(output, txid, last_height, anchor) => write!( + f, + "New witness for output {} in tx {} has incorrect anchor after scanning block {}: {:?}", + output, txid, last_height, anchor, + ), + Error::InvalidNote => write!(f, "Invalid note"), + Error::InvalidWitnessAnchor(id_note, last_height) => write!( + f, + "Witness for note {} has incorrect anchor after scanning block {}", + id_note, last_height + ), + Error::ScanRequired => write!(f, "Must scan blocks first"), + Error::TableNotEmpty => write!(f, "Table is not empty"), + Error::Bech32(e) => write!(f, "{}", e), + Error::Base58(e) => write!(f, "{}", e), + Error::Builder(e) => write!(f, "{:?}", e), + Error::Database(e) => write!(f, "{}", e), + Error::Io(e) => write!(f, "{}", e), + Error::Protobuf(e) => write!(f, "{}", e), + Error::SaplingNotActive => write!(f, "Could not determine Sapling upgrade activation height."), + } + } +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match &self { + Error::InvalidMemo(e) => Some(e), + Error::Bech32(e) => Some(e), + Error::Builder(e) => Some(e), + Error::Database(e) => Some(e), + Error::Io(e) => Some(e), + Error::Protobuf(e) => Some(e), + _ => None, + } + } +} + +impl From for Error { + fn from(e: bech32::Error) -> Self { + Error::Bech32(e) + } +} + +impl From for Error { + fn from(e: bs58::decode::Error) -> Self { + Error::Base58(e) + } +} + +impl From for Error { + fn from(e: builder::Error) -> Self { + Error::Builder(e) + } +} + +impl From for Error { + fn from(e: std::io::Error) -> Self { + Error::Io(e) + } +} + +impl From for Error { + fn from(e: protobuf::ProtobufError) -> Self { + Error::Protobuf(e) + } +} diff --git a/zcash_client_backend/src/data_api/mod.rs b/zcash_client_backend/src/data_api/mod.rs new file mode 100644 index 000000000..f987c1933 --- /dev/null +++ b/zcash_client_backend/src/data_api/mod.rs @@ -0,0 +1,98 @@ +use zcash_primitives::{ + block::BlockHash, + //merkle_tree::{CommitmentTree, IncrementalWitness}, + //sapling::Node, + //transaction::{ + // Transaction, + // TxId, + // components::Amount, + //}, + //zip32::ExtendedFullViewingKey, + consensus::{self, BlockHeight}, +}; + +use crate::proto::compact_formats::CompactBlock; + +pub mod chain; +pub mod error; + +pub trait DBOps { + type Error; + // type TxRef; // Backend-specific transaction handle + // type NoteRef; // Backend-specific note identifier` + + // fn init_db() -> Result<(), Self::Error>; + // + // fn init_accounts(extfvks: &[ExtendedFullViewingKey]) -> Result<(), Self::Error>; + // + // fn init_blocks( + // height: i32, + // hash: BlockHash, + // time: u32, + // sapling_tree: &[u8], + // ) -> Result<(), Self::Error>; + // + fn block_height_extrema(&self) -> Result, Self::Error>; + + fn get_block_hash(&self, block_height: BlockHeight) -> Result, Self::Error>; + + fn rewind_to_height( + &self, + parameters: &P, + block_height: BlockHeight, + ) -> Result<(), Self::Error>; + // + // // fn get_target_and_anchor_heights() -> Result<(u32, u32), Self::Error>; + // + // fn get_address(account: Account) -> Result; + // + // fn get_balance(account: Account) -> Result; + // + // fn get_verified_balance(account: Account) -> Result; + // + // fn get_received_memo_as_utf8(id_note: i64) -> Result, Self::Error>; + // + // fn get_extended_full_viewing_keys() -> Result>, Self::Error>; + // + // fn get_commitment_tree(block_height: BlockHeight) -> Result>, Self::Error>; + // + // fn get_witnesses(block_height: BlockHeight) -> Result>>, Self::Error>; + // + // fn get_nullifiers() -> Result<(Vec, Account), Self::Error>; + // + // fn create_block(block_height: BlockHeight, hash: BlockHash, time: u32, sapling_tree: CommitmentTree) -> Result<(), Self::Error>; + // + // fn put_transaction(transaction: Transaction, block_height: BlockHeight) -> Result; + // + // fn get_txref(txid: TxId) -> Result, Self::Error>; + // + // fn mark_spent(tx_ref: Self::TxRef, nullifier: Vec) -> Result<(), Self::Error>; + // + // fn put_note(output: WalletShieldedOutput, tx_ref: Self::TxRef, nullifier: Vec) -> Result<(), Self::Error>; + // + // fn get_note(tx_ref: Self::TxRef, output_index: i64) -> Result; + // + // fn prune_witnesses(to_height: BlockHeight) -> Result<(), Self::Error>; + // + // fn mark_expired_unspent(to_height: BlockHeight) -> Result<(), Self::Error>; + // + // fn put_sent_note(tx_ref: Self::TxRef, output: DecryptedOutput) -> Result<(), Self::Error>; + // + // fn put_received_note(tx_ref: Self::TxRef, output: DecryptedOutput) -> Result<(), Self::Error>; +} + +pub trait CacheOps { + type Error; + + // Validate the cached chain by applying a function that checks pairwise constraints + // (top_block :: &CompactBlock, next_block :: &CompactBlock) -> Result<(), Self::Error) + // beginning with the current maximum height walking backward through the chain, terminating + // with the block at `from_height`. Returns the hash of the block at height `from_height` + fn validate_chain( + &self, + from_height: BlockHeight, + validate: F, + ) -> Result, Self::Error> + where + F: Fn(&CompactBlock, &CompactBlock) -> Result<(), Self::Error>; +} diff --git a/zcash_client_backend/src/lib.rs b/zcash_client_backend/src/lib.rs index d6b276b62..fc26166f5 100644 --- a/zcash_client_backend/src/lib.rs +++ b/zcash_client_backend/src/lib.rs @@ -7,6 +7,7 @@ #![deny(intra_doc_link_resolution_failure)] pub mod address; +pub mod data_api; mod decrypt; pub mod encoding; pub mod keys; diff --git a/zcash_client_backend/src/proto/mod.rs b/zcash_client_backend/src/proto/mod.rs index f674dd622..2a4dbc939 100644 --- a/zcash_client_backend/src/proto/mod.rs +++ b/zcash_client_backend/src/proto/mod.rs @@ -46,6 +46,16 @@ impl compact_formats::CompactBlock { } } + /// Returns the [`BlockHeight`] value for this block + /// + /// # Panics + /// + /// This function will panic if [`CompactBlock.height`] is not + /// representable within a u32. + pub fn height(&self) -> BlockHeight { + self.height.try_into().unwrap() + } + /// Returns the [`BlockHeader`] for this block if present. /// /// A convenience method that parses [`CompactBlock.header`] if present. @@ -58,15 +68,6 @@ impl compact_formats::CompactBlock { BlockHeader::read(&self.header[..]).ok() } } - - /// Returns the [`BlockHeight`] for this block. - /// - /// A convenience method that wraps [`CompactBlock.height`] - /// - /// [`CompactBlock.height`]: #structfield.height - pub fn height(&self) -> BlockHeight { - BlockHeight::from(self.height) - } } impl compact_formats::CompactOutput { diff --git a/zcash_primitives/src/consensus.rs b/zcash_primitives/src/consensus.rs index da1c9fc21..e3b55a1b5 100644 --- a/zcash_primitives/src/consensus.rs +++ b/zcash_primitives/src/consensus.rs @@ -45,9 +45,23 @@ impl From for BlockHeight { } } -impl From for BlockHeight { - fn from(value: u64) -> Self { - BlockHeight(value as u32) +impl From for u32 { + fn from(value: BlockHeight) -> u32 { + value.0 + } +} + +impl TryFrom for BlockHeight { + type Error = std::num::TryFromIntError; + + fn try_from(value: u64) -> Result { + u32::try_from(value).map(BlockHeight) + } +} + +impl From for u64 { + fn from(value: BlockHeight) -> u64 { + value.0 as u64 } } @@ -67,18 +81,6 @@ impl TryFrom for BlockHeight { } } -impl From for u32 { - fn from(value: BlockHeight) -> u32 { - value.0 - } -} - -impl From for u64 { - fn from(value: BlockHeight) -> u64 { - value.0 as u64 - } -} - impl From for i64 { fn from(value: BlockHeight) -> i64 { value.0 as i64