Initial skeleton of low-level database access API.

This commit is contained in:
Kris Nuttycombe 2020-07-22 20:44:06 -06:00
parent 7ced7f3c56
commit a437df191e
6 changed files with 339 additions and 24 deletions

View File

@ -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<Error = Error<E>>,
D: DBOps<Error = Error<E>>,
>(
parameters: &P,
cache: &C,
data: &D,
) -> Result<(), Error<E>> {
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::<E>(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(())
}
}
}

View File

@ -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<DbError> {
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<E>(at_height: BlockHeight) -> Error<E> {
Error::InvalidChain(at_height, ChainInvalid::PrevHashMismatch)
}
pub fn block_height_mismatch<E>(at_height: BlockHeight, found: BlockHeight) -> Error<E> {
Error::InvalidChain(at_height, ChainInvalid::BlockHeightMismatch(found))
}
}
impl<E: fmt::Display> fmt::Display for Error<E> {
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<E: error::Error + 'static> error::Error for Error<E> {
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<E> From<bech32::Error> for Error<E> {
fn from(e: bech32::Error) -> Self {
Error::Bech32(e)
}
}
impl<E> From<bs58::decode::Error> for Error<E> {
fn from(e: bs58::decode::Error) -> Self {
Error::Base58(e)
}
}
impl<E> From<builder::Error> for Error<E> {
fn from(e: builder::Error) -> Self {
Error::Builder(e)
}
}
impl<E> From<std::io::Error> for Error<E> {
fn from(e: std::io::Error) -> Self {
Error::Io(e)
}
}
impl<E> From<protobuf::ProtobufError> for Error<E> {
fn from(e: protobuf::ProtobufError) -> Self {
Error::Protobuf(e)
}
}

View File

@ -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<Option<(BlockHeight, BlockHeight)>, Self::Error>;
fn get_block_hash(&self, block_height: BlockHeight) -> Result<Option<BlockHash>, Self::Error>;
fn rewind_to_height<P: consensus::Parameters>(
&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<String, Self::Error>;
//
// fn get_balance(account: Account) -> Result<Amount, Self::Error>;
//
// fn get_verified_balance(account: Account) -> Result<Amount, Self::Error>;
//
// fn get_received_memo_as_utf8(id_note: i64) -> Result<Option<String>, Self::Error>;
//
// fn get_extended_full_viewing_keys() -> Result<Box<dyn Iterator<Item = ExtendedFullViewingKey>>, Self::Error>;
//
// fn get_commitment_tree(block_height: BlockHeight) -> Result<Option<CommitmentTree<Node>>, Self::Error>;
//
// fn get_witnesses(block_height: BlockHeight) -> Result<Box<dyn Iterator<Item = IncrementalWitness<Node>>>, Self::Error>;
//
// fn get_nullifiers() -> Result<(Vec<u8>, Account), Self::Error>;
//
// fn create_block(block_height: BlockHeight, hash: BlockHash, time: u32, sapling_tree: CommitmentTree<Node>) -> Result<(), Self::Error>;
//
// fn put_transaction(transaction: Transaction, block_height: BlockHeight) -> Result<Self::TxRef, Self::Error>;
//
// fn get_txref(txid: TxId) -> Result<Option<Self::TxRef>, Self::Error>;
//
// fn mark_spent(tx_ref: Self::TxRef, nullifier: Vec<u8>) -> Result<(), Self::Error>;
//
// fn put_note(output: WalletShieldedOutput, tx_ref: Self::TxRef, nullifier: Vec<u8>) -> Result<(), Self::Error>;
//
// fn get_note(tx_ref: Self::TxRef, output_index: i64) -> Result<Self::NoteRef, Self::Error>;
//
// 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<F>(
&self,
from_height: BlockHeight,
validate: F,
) -> Result<Option<BlockHash>, Self::Error>
where
F: Fn(&CompactBlock, &CompactBlock) -> Result<(), Self::Error>;
}

View File

@ -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;

View File

@ -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 {

View File

@ -45,9 +45,23 @@ impl From<u32> for BlockHeight {
}
}
impl From<u64> for BlockHeight {
fn from(value: u64) -> Self {
BlockHeight(value as u32)
impl From<BlockHeight> for u32 {
fn from(value: BlockHeight) -> u32 {
value.0
}
}
impl TryFrom<u64> for BlockHeight {
type Error = std::num::TryFromIntError;
fn try_from(value: u64) -> Result<Self, Self::Error> {
u32::try_from(value).map(BlockHeight)
}
}
impl From<BlockHeight> for u64 {
fn from(value: BlockHeight) -> u64 {
value.0 as u64
}
}
@ -67,18 +81,6 @@ impl TryFrom<i64> for BlockHeight {
}
}
impl From<BlockHeight> for u32 {
fn from(value: BlockHeight) -> u32 {
value.0
}
}
impl From<BlockHeight> for u64 {
fn from(value: BlockHeight) -> u64 {
value.0 as u64
}
}
impl From<BlockHeight> for i64 {
fn from(value: BlockHeight) -> i64 {
value.0 as i64