2019-03-08 18:16:00 -08:00
|
|
|
//! *An SQLite-based Zcash light client.*
|
|
|
|
//!
|
2021-03-25 21:22:45 -07:00
|
|
|
//! `zcash_client_sqlite` contains complete SQLite-based implementations of the [`WalletRead`],
|
|
|
|
//! [`WalletWrite`], and [`BlockSource`] traits from the [`zcash_client_backend`] crate. In
|
|
|
|
//! combination with [`zcash_client_backend`], it provides a full implementation of a SQLite-backed
|
|
|
|
//! client for the Zcash network.
|
2019-03-08 18:16:00 -08:00
|
|
|
//!
|
|
|
|
//! # 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.
|
|
|
|
//!
|
2019-04-08 04:24:49 -07:00
|
|
|
//! # Features
|
|
|
|
//!
|
|
|
|
//! The `mainnet` feature configures the light client for use with the Zcash mainnet. By
|
|
|
|
//! default, the light client is configured for use with the Zcash testnet.
|
|
|
|
//!
|
2021-03-25 21:22:45 -07:00
|
|
|
//! [`WalletRead`]: zcash_client_backend::data_api::WalletRead
|
|
|
|
//! [`WalletWrite`]: zcash_client_backend::data_api::WalletWrite
|
2022-10-17 10:35:14 -07:00
|
|
|
//! [`BlockSource`]: zcash_client_backend::data_api::chain::BlockSource
|
2019-03-08 18:16:00 -08:00
|
|
|
//! [`CompactBlock`]: zcash_client_backend::proto::compact_formats::CompactBlock
|
2021-03-04 14:17:25 -08:00
|
|
|
//! [`init_cache_database`]: crate::chain::init::init_cache_database
|
2019-03-08 18:16:00 -08:00
|
|
|
|
2021-03-26 12:27:17 -07:00
|
|
|
// Catch documentation errors caused by code changes.
|
2022-07-29 14:56:44 -07:00
|
|
|
#![deny(rustdoc::broken_intra_doc_links)]
|
2021-03-26 12:27:17 -07:00
|
|
|
|
2023-04-03 12:53:43 -07:00
|
|
|
use either::Either;
|
2023-06-09 10:02:00 -07:00
|
|
|
use rusqlite::{self, Connection};
|
2022-09-13 15:43:04 -07:00
|
|
|
use secrecy::{ExposeSecret, SecretVec};
|
2023-04-03 12:53:43 -07:00
|
|
|
use std::{borrow::Borrow, collections::HashMap, convert::AsRef, fmt, io, ops::Range, path::Path};
|
2019-03-08 18:16:00 -08:00
|
|
|
|
2023-04-03 12:53:43 -07:00
|
|
|
use incrementalmerkletree::Position;
|
|
|
|
use shardtree::{ShardTree, ShardTreeError};
|
2020-08-05 13:27:40 -07:00
|
|
|
use zcash_primitives::{
|
2020-08-05 18:14:45 -07:00
|
|
|
block::BlockHash,
|
2020-08-05 13:27:40 -07:00
|
|
|
consensus::{self, BlockHeight},
|
2022-10-13 19:58:43 -07:00
|
|
|
legacy::TransparentAddress,
|
2023-04-24 15:00:43 -07:00
|
|
|
memo::{Memo, MemoBytes},
|
2023-06-07 14:03:20 -07:00
|
|
|
sapling,
|
2022-11-09 07:13:34 -08:00
|
|
|
transaction::{
|
|
|
|
components::{amount::Amount, OutPoint},
|
|
|
|
Transaction, TxId,
|
|
|
|
},
|
2022-09-12 11:42:12 -07:00
|
|
|
zip32::{AccountId, DiversifierIndex, ExtendedFullViewingKey},
|
2019-04-08 04:24:49 -07:00
|
|
|
};
|
|
|
|
|
2020-08-05 18:14:45 -07:00
|
|
|
use zcash_client_backend::{
|
2022-10-25 09:54:12 -07:00
|
|
|
address::{AddressMetadata, UnifiedAddress},
|
2021-03-09 17:10:44 -08:00
|
|
|
data_api::{
|
2023-07-01 17:16:23 -07:00
|
|
|
self, chain::BlockSource, BlockMetadata, DecryptedTransaction, NullifierQuery, PoolType,
|
|
|
|
Recipient, ScannedBlock, SentTransaction, WalletCommitmentTrees, WalletRead, WalletWrite,
|
|
|
|
SAPLING_SHARD_HEIGHT,
|
2021-03-09 17:10:44 -08:00
|
|
|
},
|
2022-09-13 15:43:04 -07:00
|
|
|
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
|
2020-08-05 18:14:45 -07:00
|
|
|
proto::compact_formats::CompactBlock,
|
2023-04-03 12:53:43 -07:00
|
|
|
wallet::{ReceivedSaplingNote, WalletTransparentOutput},
|
2023-04-24 15:00:43 -07:00
|
|
|
DecryptedOutput, TransferType,
|
2020-08-05 18:14:45 -07:00
|
|
|
};
|
|
|
|
|
2023-06-16 13:42:36 -07:00
|
|
|
use crate::{error::SqliteClientError, wallet::commitment_tree::SqliteShardStore};
|
2020-08-06 13:11:25 -07:00
|
|
|
|
2022-07-28 16:21:04 -07:00
|
|
|
#[cfg(feature = "unstable")]
|
|
|
|
use {
|
|
|
|
crate::chain::{fsblockdb_with_blocks, BlockMeta},
|
2023-04-03 12:53:43 -07:00
|
|
|
std::fs,
|
2022-07-28 16:21:04 -07:00
|
|
|
std::path::PathBuf,
|
|
|
|
};
|
|
|
|
|
2019-05-02 03:27:43 -07:00
|
|
|
pub mod chain;
|
2019-03-08 18:16:00 -08:00
|
|
|
pub mod error;
|
2023-04-03 12:53:43 -07:00
|
|
|
pub mod serialization;
|
2020-08-25 14:20:12 -07:00
|
|
|
pub mod wallet;
|
2019-03-08 18:16:00 -08:00
|
|
|
|
2021-10-01 10:58:23 -07:00
|
|
|
/// The maximum number of blocks the wallet is allowed to rewind. This is
|
|
|
|
/// consistent with the bound in zcashd, and allows block data deeper than
|
|
|
|
/// this delta from the chain tip to be pruned.
|
|
|
|
pub(crate) const PRUNING_HEIGHT: u32 = 100;
|
2021-04-13 10:02:35 -07:00
|
|
|
|
2023-07-01 17:16:23 -07:00
|
|
|
pub(crate) const SAPLING_TABLES_PREFIX: &str = "sapling";
|
2023-06-29 15:26:22 -07:00
|
|
|
|
2021-01-11 17:13:40 -08:00
|
|
|
/// A newtype wrapper for sqlite primary key values for the notes
|
|
|
|
/// table.
|
2022-10-17 10:35:14 -07:00
|
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
2021-01-19 11:27:48 -08:00
|
|
|
pub enum NoteId {
|
|
|
|
SentNoteId(i64),
|
|
|
|
ReceivedNoteId(i64),
|
|
|
|
}
|
2020-08-05 18:14:45 -07:00
|
|
|
|
2020-08-18 12:48:59 -07:00
|
|
|
impl fmt::Display for NoteId {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
2021-01-19 11:27:48 -08:00
|
|
|
match self {
|
|
|
|
NoteId::SentNoteId(id) => write!(f, "Sent Note {}", id),
|
|
|
|
NoteId::ReceivedNoteId(id) => write!(f, "Received Note {}", id),
|
|
|
|
}
|
2020-08-18 12:48:59 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-22 06:10:13 -08:00
|
|
|
/// A newtype wrapper for sqlite primary key values for the utxos
|
|
|
|
/// table.
|
2022-10-01 11:58:01 -07:00
|
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
2021-02-12 13:08:31 -08:00
|
|
|
pub struct UtxoId(pub i64);
|
2020-12-22 06:10:13 -08:00
|
|
|
|
2021-03-25 21:22:45 -07:00
|
|
|
/// A wrapper for the SQLite connection to the wallet database.
|
2023-06-09 10:02:00 -07:00
|
|
|
pub struct WalletDb<C, P> {
|
|
|
|
conn: C,
|
2021-01-12 17:24:18 -08:00
|
|
|
params: P,
|
|
|
|
}
|
2020-08-05 18:14:45 -07:00
|
|
|
|
2023-06-09 10:02:00 -07:00
|
|
|
/// A wrapper for a SQLite transaction affecting the wallet database.
|
2023-06-13 10:20:18 -07:00
|
|
|
pub struct SqlTransaction<'conn>(pub(crate) &'conn rusqlite::Transaction<'conn>);
|
2023-06-09 10:02:00 -07:00
|
|
|
|
2023-06-12 11:17:20 -07:00
|
|
|
impl Borrow<rusqlite::Connection> for SqlTransaction<'_> {
|
2023-06-09 10:02:00 -07:00
|
|
|
fn borrow(&self) -> &rusqlite::Connection {
|
2023-06-13 10:20:18 -07:00
|
|
|
self.0
|
2023-06-09 10:02:00 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<P: consensus::Parameters + Clone> WalletDb<Connection, P> {
|
2021-01-11 17:13:40 -08:00
|
|
|
/// Construct a connection to the wallet database stored at the specified path.
|
2021-01-12 17:24:18 -08:00
|
|
|
pub fn for_path<F: AsRef<Path>>(path: F, params: P) -> Result<Self, rusqlite::Error> {
|
2022-11-09 07:13:34 -08:00
|
|
|
Connection::open(path).and_then(move |conn| {
|
|
|
|
rusqlite::vtab::array::load_module(&conn)?;
|
|
|
|
Ok(WalletDb { conn, params })
|
|
|
|
})
|
2020-08-05 18:14:45 -07:00
|
|
|
}
|
2021-01-08 11:49:10 -08:00
|
|
|
|
2023-04-03 12:53:43 -07:00
|
|
|
pub fn transactionally<F, A, E: From<rusqlite::Error>>(&mut self, f: F) -> Result<A, E>
|
2023-06-09 10:02:00 -07:00
|
|
|
where
|
2023-04-03 12:53:43 -07:00
|
|
|
F: FnOnce(&mut WalletDb<SqlTransaction<'_>, P>) -> Result<A, E>,
|
2023-06-09 10:02:00 -07:00
|
|
|
{
|
2023-06-13 10:20:18 -07:00
|
|
|
let tx = self.conn.transaction()?;
|
2023-04-03 12:53:43 -07:00
|
|
|
let mut wdb = WalletDb {
|
2023-06-13 10:20:18 -07:00
|
|
|
conn: SqlTransaction(&tx),
|
2023-06-09 10:02:00 -07:00
|
|
|
params: self.params.clone(),
|
|
|
|
};
|
2023-04-03 12:53:43 -07:00
|
|
|
let result = f(&mut wdb)?;
|
2023-06-13 10:20:18 -07:00
|
|
|
tx.commit()?;
|
2023-06-09 10:02:00 -07:00
|
|
|
Ok(result)
|
2021-01-08 11:49:10 -08:00
|
|
|
}
|
2020-08-05 18:14:45 -07:00
|
|
|
}
|
|
|
|
|
2023-06-09 10:02:00 -07:00
|
|
|
impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for WalletDb<C, P> {
|
2021-01-13 14:20:11 -08:00
|
|
|
type Error = SqliteClientError;
|
2020-08-20 10:41:43 -07:00
|
|
|
type NoteRef = NoteId;
|
2020-08-26 14:47:47 -07:00
|
|
|
type TxRef = i64;
|
2020-08-05 18:14:45 -07:00
|
|
|
|
|
|
|
fn block_height_extrema(&self) -> Result<Option<(BlockHeight, BlockHeight)>, Self::Error> {
|
2023-06-09 10:02:00 -07:00
|
|
|
wallet::block_height_extrema(self.conn.borrow()).map_err(SqliteClientError::from)
|
2020-08-05 18:14:45 -07:00
|
|
|
}
|
|
|
|
|
2023-07-01 17:16:23 -07:00
|
|
|
fn block_metadata(&self, height: BlockHeight) -> Result<Option<BlockMetadata>, Self::Error> {
|
|
|
|
wallet::block_metadata(self.conn.borrow(), height)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn block_fully_scanned(&self) -> Result<Option<BlockMetadata>, Self::Error> {
|
|
|
|
wallet::block_fully_scanned(self.conn.borrow())
|
2023-04-03 12:53:43 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
fn suggest_scan_ranges(
|
|
|
|
&self,
|
|
|
|
_batch_size: usize,
|
|
|
|
_limit: usize,
|
|
|
|
) -> Result<Vec<Range<BlockHeight>>, Self::Error> {
|
|
|
|
todo!()
|
|
|
|
}
|
|
|
|
|
2023-04-26 15:27:56 -07:00
|
|
|
fn get_min_unspent_height(&self) -> Result<Option<BlockHeight>, Self::Error> {
|
2023-06-09 10:02:00 -07:00
|
|
|
wallet::get_min_unspent_height(self.conn.borrow()).map_err(SqliteClientError::from)
|
2023-04-26 15:27:56 -07:00
|
|
|
}
|
|
|
|
|
2020-08-05 18:14:45 -07:00
|
|
|
fn get_block_hash(&self, block_height: BlockHeight) -> Result<Option<BlockHash>, Self::Error> {
|
2023-06-09 10:02:00 -07:00
|
|
|
wallet::get_block_hash(self.conn.borrow(), block_height).map_err(SqliteClientError::from)
|
2020-08-05 18:14:45 -07:00
|
|
|
}
|
|
|
|
|
2020-08-25 14:02:44 -07:00
|
|
|
fn get_tx_height(&self, txid: TxId) -> Result<Option<BlockHeight>, Self::Error> {
|
2023-06-09 10:02:00 -07:00
|
|
|
wallet::get_tx_height(self.conn.borrow(), txid).map_err(SqliteClientError::from)
|
2020-08-25 14:02:44 -07:00
|
|
|
}
|
|
|
|
|
2023-07-01 17:16:23 -07:00
|
|
|
fn get_current_address(
|
|
|
|
&self,
|
|
|
|
account: AccountId,
|
|
|
|
) -> Result<Option<UnifiedAddress>, Self::Error> {
|
|
|
|
wallet::get_current_address(self.conn.borrow(), &self.params, account)
|
|
|
|
.map(|res| res.map(|(addr, _)| addr))
|
|
|
|
}
|
|
|
|
|
2022-06-13 17:57:20 -07:00
|
|
|
fn get_unified_full_viewing_keys(
|
2020-08-26 14:47:47 -07:00
|
|
|
&self,
|
2022-06-13 17:57:20 -07:00
|
|
|
) -> Result<HashMap<AccountId, UnifiedFullViewingKey>, Self::Error> {
|
2023-06-09 10:02:00 -07:00
|
|
|
wallet::get_unified_full_viewing_keys(self.conn.borrow(), &self.params)
|
2020-08-26 14:47:47 -07:00
|
|
|
}
|
|
|
|
|
2022-10-03 13:44:04 -07:00
|
|
|
fn get_account_for_ufvk(
|
|
|
|
&self,
|
|
|
|
ufvk: &UnifiedFullViewingKey,
|
|
|
|
) -> Result<Option<AccountId>, Self::Error> {
|
2023-06-09 10:02:00 -07:00
|
|
|
wallet::get_account_for_ufvk(self.conn.borrow(), &self.params, ufvk)
|
2022-10-03 13:44:04 -07:00
|
|
|
}
|
|
|
|
|
2021-01-12 17:24:18 -08:00
|
|
|
fn is_valid_account_extfvk(
|
2020-08-26 14:47:47 -07:00
|
|
|
&self,
|
|
|
|
account: AccountId,
|
|
|
|
extfvk: &ExtendedFullViewingKey,
|
|
|
|
) -> Result<bool, Self::Error> {
|
2023-06-09 10:02:00 -07:00
|
|
|
wallet::is_valid_account_extfvk(self.conn.borrow(), &self.params, account, extfvk)
|
2020-08-26 14:47:47 -07:00
|
|
|
}
|
|
|
|
|
2021-01-15 11:00:14 -08:00
|
|
|
fn get_balance_at(
|
2020-08-26 14:47:47 -07:00
|
|
|
&self,
|
|
|
|
account: AccountId,
|
|
|
|
anchor_height: BlockHeight,
|
|
|
|
) -> Result<Amount, Self::Error> {
|
2023-06-09 10:02:00 -07:00
|
|
|
wallet::get_balance_at(self.conn.borrow(), account, anchor_height)
|
2020-08-05 16:01:22 -07:00
|
|
|
}
|
2020-08-06 09:23:15 -07:00
|
|
|
|
2023-05-16 09:27:40 -07:00
|
|
|
fn get_memo(&self, id_note: Self::NoteRef) -> Result<Option<Memo>, Self::Error> {
|
2021-01-19 11:27:48 -08:00
|
|
|
match id_note {
|
2023-06-09 10:02:00 -07:00
|
|
|
NoteId::SentNoteId(id_note) => wallet::get_sent_memo(self.conn.borrow(), id_note),
|
|
|
|
NoteId::ReceivedNoteId(id_note) => {
|
|
|
|
wallet::get_received_memo(self.conn.borrow(), id_note)
|
|
|
|
}
|
2021-01-19 11:27:48 -08:00
|
|
|
}
|
2020-08-17 10:46:44 -07:00
|
|
|
}
|
|
|
|
|
2023-07-01 17:16:23 -07:00
|
|
|
fn get_transaction(&self, id_tx: i64) -> Result<Transaction, Self::Error> {
|
|
|
|
wallet::get_transaction(self.conn.borrow(), &self.params, id_tx)
|
|
|
|
}
|
|
|
|
|
2023-04-03 12:53:43 -07:00
|
|
|
fn get_sapling_nullifiers(
|
|
|
|
&self,
|
2023-04-03 12:53:43 -07:00
|
|
|
query: NullifierQuery,
|
2023-04-03 12:53:43 -07:00
|
|
|
) -> Result<Vec<(AccountId, sapling::Nullifier)>, Self::Error> {
|
|
|
|
match query {
|
2023-06-09 10:02:00 -07:00
|
|
|
NullifierQuery::Unspent => wallet::sapling::get_sapling_nullifiers(self.conn.borrow()),
|
|
|
|
NullifierQuery::All => wallet::sapling::get_all_sapling_nullifiers(self.conn.borrow()),
|
2023-04-03 12:53:43 -07:00
|
|
|
}
|
2021-04-14 10:20:56 -07:00
|
|
|
}
|
|
|
|
|
2021-09-03 16:32:40 -07:00
|
|
|
fn get_spendable_sapling_notes(
|
2021-01-12 19:33:53 -08:00
|
|
|
&self,
|
|
|
|
account: AccountId,
|
|
|
|
anchor_height: BlockHeight,
|
2022-11-09 07:13:34 -08:00
|
|
|
exclude: &[Self::NoteRef],
|
2023-04-03 12:53:43 -07:00
|
|
|
) -> Result<Vec<ReceivedSaplingNote<Self::NoteRef>>, Self::Error> {
|
2023-06-09 10:02:00 -07:00
|
|
|
wallet::sapling::get_spendable_sapling_notes(
|
|
|
|
self.conn.borrow(),
|
|
|
|
account,
|
|
|
|
anchor_height,
|
|
|
|
exclude,
|
|
|
|
)
|
2021-01-12 19:33:53 -08:00
|
|
|
}
|
|
|
|
|
2021-09-03 16:32:40 -07:00
|
|
|
fn select_spendable_sapling_notes(
|
2020-08-26 14:47:47 -07:00
|
|
|
&self,
|
|
|
|
account: AccountId,
|
|
|
|
target_value: Amount,
|
|
|
|
anchor_height: BlockHeight,
|
2022-11-09 07:13:34 -08:00
|
|
|
exclude: &[Self::NoteRef],
|
2023-04-03 12:53:43 -07:00
|
|
|
) -> Result<Vec<ReceivedSaplingNote<Self::NoteRef>>, Self::Error> {
|
2023-06-02 07:33:19 -07:00
|
|
|
wallet::sapling::select_spendable_sapling_notes(
|
2023-06-09 10:02:00 -07:00
|
|
|
self.conn.borrow(),
|
2022-11-09 07:13:34 -08:00
|
|
|
account,
|
|
|
|
target_value,
|
|
|
|
anchor_height,
|
|
|
|
exclude,
|
|
|
|
)
|
2020-12-22 06:10:13 -08:00
|
|
|
}
|
|
|
|
|
2022-09-08 11:48:06 -07:00
|
|
|
fn get_transparent_receivers(
|
|
|
|
&self,
|
2022-10-13 19:58:43 -07:00
|
|
|
_account: AccountId,
|
2022-10-25 09:54:12 -07:00
|
|
|
) -> Result<HashMap<TransparentAddress, AddressMetadata>, Self::Error> {
|
2022-10-13 19:58:43 -07:00
|
|
|
#[cfg(feature = "transparent-inputs")]
|
2023-06-09 10:02:00 -07:00
|
|
|
return wallet::get_transparent_receivers(self.conn.borrow(), &self.params, _account);
|
2022-10-13 19:58:43 -07:00
|
|
|
|
|
|
|
#[cfg(not(feature = "transparent-inputs"))]
|
2022-10-17 10:35:14 -07:00
|
|
|
panic!(
|
|
|
|
"The wallet must be compiled with the transparent-inputs feature to use this method."
|
|
|
|
);
|
2022-09-08 11:48:06 -07:00
|
|
|
}
|
|
|
|
|
2021-09-03 16:32:40 -07:00
|
|
|
fn get_unspent_transparent_outputs(
|
2020-12-22 06:10:13 -08:00
|
|
|
&self,
|
2022-10-13 19:58:43 -07:00
|
|
|
_address: &TransparentAddress,
|
|
|
|
_max_height: BlockHeight,
|
2022-11-09 07:13:34 -08:00
|
|
|
_exclude: &[OutPoint],
|
2020-12-22 06:10:13 -08:00
|
|
|
) -> Result<Vec<WalletTransparentOutput>, Self::Error> {
|
2022-10-13 19:58:43 -07:00
|
|
|
#[cfg(feature = "transparent-inputs")]
|
2023-06-09 10:02:00 -07:00
|
|
|
return wallet::get_unspent_transparent_outputs(
|
|
|
|
self.conn.borrow(),
|
|
|
|
&self.params,
|
|
|
|
_address,
|
|
|
|
_max_height,
|
|
|
|
_exclude,
|
|
|
|
);
|
2022-10-13 19:58:43 -07:00
|
|
|
|
|
|
|
#[cfg(not(feature = "transparent-inputs"))]
|
2022-10-17 10:35:14 -07:00
|
|
|
panic!(
|
|
|
|
"The wallet must be compiled with the transparent-inputs feature to use this method."
|
|
|
|
);
|
2020-08-26 14:47:47 -07:00
|
|
|
}
|
2022-10-25 11:04:02 -07:00
|
|
|
|
|
|
|
fn get_transparent_balances(
|
|
|
|
&self,
|
|
|
|
_account: AccountId,
|
|
|
|
_max_height: BlockHeight,
|
|
|
|
) -> Result<HashMap<TransparentAddress, Amount>, Self::Error> {
|
|
|
|
#[cfg(feature = "transparent-inputs")]
|
2023-06-09 10:02:00 -07:00
|
|
|
return wallet::get_transparent_balances(
|
|
|
|
self.conn.borrow(),
|
|
|
|
&self.params,
|
|
|
|
_account,
|
|
|
|
_max_height,
|
|
|
|
);
|
2022-10-25 11:04:02 -07:00
|
|
|
|
|
|
|
#[cfg(not(feature = "transparent-inputs"))]
|
2022-10-17 10:35:14 -07:00
|
|
|
panic!(
|
|
|
|
"The wallet must be compiled with the transparent-inputs feature to use this method."
|
|
|
|
);
|
2022-10-25 11:04:02 -07:00
|
|
|
}
|
2020-08-20 10:41:43 -07:00
|
|
|
}
|
|
|
|
|
2023-06-09 10:02:00 -07:00
|
|
|
impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P> {
|
2022-10-13 19:58:43 -07:00
|
|
|
type UtxoRef = UtxoId;
|
|
|
|
|
2022-09-13 15:43:04 -07:00
|
|
|
fn create_account(
|
|
|
|
&mut self,
|
2022-09-14 11:20:39 -07:00
|
|
|
seed: &SecretVec<u8>,
|
2022-09-13 15:43:04 -07:00
|
|
|
) -> Result<(AccountId, UnifiedSpendingKey), Self::Error> {
|
2023-06-09 10:02:00 -07:00
|
|
|
self.transactionally(|wdb| {
|
2023-06-13 10:20:18 -07:00
|
|
|
let account = wallet::get_max_account_id(wdb.conn.0)?
|
2022-09-13 15:43:04 -07:00
|
|
|
.map(|a| AccountId::from(u32::from(a) + 1))
|
|
|
|
.unwrap_or_else(|| AccountId::from(0));
|
|
|
|
|
2022-09-14 12:38:00 -07:00
|
|
|
if u32::from(account) >= 0x7FFFFFFF {
|
|
|
|
return Err(SqliteClientError::AccountIdOutOfRange);
|
|
|
|
}
|
|
|
|
|
2023-06-09 10:02:00 -07:00
|
|
|
let usk = UnifiedSpendingKey::from_seed(&wdb.params, seed.expose_secret(), account)
|
|
|
|
.map_err(|_| SqliteClientError::KeyDerivationError(account))?;
|
2022-09-13 15:43:04 -07:00
|
|
|
let ufvk = usk.to_unified_full_viewing_key();
|
|
|
|
|
2023-06-13 10:20:18 -07:00
|
|
|
wallet::add_account(wdb.conn.0, &wdb.params, account, &ufvk)?;
|
2022-09-13 15:43:04 -07:00
|
|
|
|
|
|
|
Ok((account, usk))
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-09-12 11:42:12 -07:00
|
|
|
fn get_next_available_address(
|
|
|
|
&mut self,
|
|
|
|
account: AccountId,
|
|
|
|
) -> Result<Option<UnifiedAddress>, Self::Error> {
|
2023-06-09 10:02:00 -07:00
|
|
|
self.transactionally(
|
|
|
|
|wdb| match wdb.get_unified_full_viewing_keys()?.get(&account) {
|
|
|
|
Some(ufvk) => {
|
|
|
|
let search_from =
|
2023-06-13 10:20:18 -07:00
|
|
|
match wallet::get_current_address(wdb.conn.0, &wdb.params, account)? {
|
2023-06-09 10:02:00 -07:00
|
|
|
Some((_, mut last_diversifier_index)) => {
|
|
|
|
last_diversifier_index
|
|
|
|
.increment()
|
|
|
|
.map_err(|_| SqliteClientError::DiversifierIndexOutOfRange)?;
|
|
|
|
last_diversifier_index
|
|
|
|
}
|
|
|
|
None => DiversifierIndex::default(),
|
|
|
|
};
|
2022-09-12 11:42:12 -07:00
|
|
|
|
2023-06-09 10:02:00 -07:00
|
|
|
let (addr, diversifier_index) = ufvk
|
|
|
|
.find_address(search_from)
|
|
|
|
.ok_or(SqliteClientError::DiversifierIndexOutOfRange)?;
|
|
|
|
|
|
|
|
wallet::insert_address(
|
2023-06-13 10:20:18 -07:00
|
|
|
wdb.conn.0,
|
2023-06-09 10:02:00 -07:00
|
|
|
&wdb.params,
|
|
|
|
account,
|
|
|
|
diversifier_index,
|
|
|
|
&addr,
|
|
|
|
)?;
|
|
|
|
|
|
|
|
Ok(Some(addr))
|
|
|
|
}
|
|
|
|
None => Ok(None),
|
|
|
|
},
|
|
|
|
)
|
2022-09-12 11:42:12 -07:00
|
|
|
}
|
|
|
|
|
2023-07-01 17:16:23 -07:00
|
|
|
#[tracing::instrument(skip_all, fields(height = u32::from(block.height())))]
|
2021-03-09 19:55:44 -08:00
|
|
|
#[allow(clippy::type_complexity)]
|
2023-06-14 14:34:28 -07:00
|
|
|
fn put_block(
|
2020-08-20 10:41:43 -07:00
|
|
|
&mut self,
|
2023-07-01 17:16:23 -07:00
|
|
|
block: ScannedBlock<sapling::Nullifier>,
|
2023-04-03 12:53:43 -07:00
|
|
|
) -> Result<Vec<Self::NoteRef>, Self::Error> {
|
2023-06-09 10:02:00 -07:00
|
|
|
self.transactionally(|wdb| {
|
2021-03-09 17:10:44 -08:00
|
|
|
// Insert the block into the database.
|
2023-06-14 15:49:16 -07:00
|
|
|
wallet::put_block(
|
2023-06-13 10:20:18 -07:00
|
|
|
wdb.conn.0,
|
2023-07-01 17:16:23 -07:00
|
|
|
block.height(),
|
|
|
|
block.block_hash(),
|
|
|
|
block.block_time(),
|
|
|
|
block.metadata().sapling_tree_size(),
|
2021-03-09 17:10:44 -08:00
|
|
|
)?;
|
|
|
|
|
2023-04-03 12:53:43 -07:00
|
|
|
let mut wallet_note_ids = vec![];
|
2023-07-01 17:16:23 -07:00
|
|
|
for tx in block.transactions() {
|
|
|
|
let tx_row = wallet::put_tx_meta(wdb.conn.0, tx, block.height())?;
|
2021-03-09 17:10:44 -08:00
|
|
|
|
|
|
|
// Mark notes as spent and remove them from the scanning cache
|
2023-04-03 12:31:45 -07:00
|
|
|
for spend in &tx.sapling_spends {
|
2023-06-13 10:20:18 -07:00
|
|
|
wallet::sapling::mark_sapling_note_spent(wdb.conn.0, tx_row, spend.nf())?;
|
2021-03-09 17:10:44 -08:00
|
|
|
}
|
2020-08-20 10:41:43 -07:00
|
|
|
|
2023-04-03 12:31:45 -07:00
|
|
|
for output in &tx.sapling_outputs {
|
2023-06-07 14:03:20 -07:00
|
|
|
let received_note_id =
|
2023-06-13 10:20:18 -07:00
|
|
|
wallet::sapling::put_received_note(wdb.conn.0, output, tx_row)?;
|
2020-08-26 16:52:21 -07:00
|
|
|
|
2021-03-09 17:10:44 -08:00
|
|
|
// Save witness for note.
|
2023-04-03 12:53:43 -07:00
|
|
|
wallet_note_ids.push(received_note_id);
|
2021-03-09 17:10:44 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-01 17:16:23 -07:00
|
|
|
let block_height = block.height();
|
|
|
|
let sapling_tree_size = block.metadata().sapling_tree_size();
|
|
|
|
let sapling_commitments_len = block.sapling_commitments().len();
|
|
|
|
let mut sapling_commitments = block.take_sapling_commitments().into_iter();
|
2023-04-03 12:53:43 -07:00
|
|
|
wdb.with_sapling_tree_mut::<_, _, SqliteClientError>(move |sapling_tree| {
|
2023-07-01 17:16:23 -07:00
|
|
|
let start_position = Position::from(u64::from(sapling_tree_size))
|
|
|
|
- u64::try_from(sapling_commitments_len).unwrap();
|
|
|
|
sapling_tree.batch_insert(start_position, &mut sapling_commitments)?;
|
2023-04-03 12:53:43 -07:00
|
|
|
Ok(())
|
|
|
|
})?;
|
2021-03-09 17:18:24 -08:00
|
|
|
|
|
|
|
// Update now-expired transactions that didn't get mined.
|
2023-07-01 17:16:23 -07:00
|
|
|
wallet::update_expired_notes(wdb.conn.0, block_height)?;
|
2021-03-09 17:18:24 -08:00
|
|
|
|
2023-04-03 12:53:43 -07:00
|
|
|
Ok(wallet_note_ids)
|
2021-03-09 17:10:44 -08:00
|
|
|
})
|
2020-08-25 14:02:44 -07:00
|
|
|
}
|
|
|
|
|
2021-03-29 13:35:18 -07:00
|
|
|
fn store_decrypted_tx(
|
2020-08-26 14:47:47 -07:00
|
|
|
&mut self,
|
2023-04-03 12:53:43 -07:00
|
|
|
d_tx: DecryptedTransaction,
|
2020-08-26 14:47:47 -07:00
|
|
|
) -> Result<Self::TxRef, Self::Error> {
|
2023-06-09 10:02:00 -07:00
|
|
|
self.transactionally(|wdb| {
|
2023-06-30 09:42:48 -07:00
|
|
|
let tx_ref = wallet::put_tx_data(wdb.conn.0, d_tx.tx, None, None)?;
|
|
|
|
|
|
|
|
let mut spending_account_id: Option<AccountId> = None;
|
|
|
|
for output in d_tx.sapling_outputs {
|
|
|
|
match output.transfer_type {
|
|
|
|
TransferType::Outgoing | TransferType::WalletInternal => {
|
|
|
|
let recipient = if output.transfer_type == TransferType::Outgoing {
|
|
|
|
Recipient::Sapling(output.note.recipient())
|
|
|
|
} else {
|
|
|
|
Recipient::InternalAccount(output.account, PoolType::Sapling)
|
|
|
|
};
|
2023-04-24 15:00:43 -07:00
|
|
|
|
2023-06-30 09:42:48 -07:00
|
|
|
wallet::put_sent_output(
|
|
|
|
wdb.conn.0,
|
|
|
|
&wdb.params,
|
|
|
|
output.account,
|
|
|
|
tx_ref,
|
|
|
|
output.index,
|
|
|
|
&recipient,
|
|
|
|
Amount::from_u64(output.note.value().inner()).map_err(|_| {
|
|
|
|
SqliteClientError::CorruptedData(
|
|
|
|
"Note value is not a valid Zcash amount.".to_string(),
|
|
|
|
)
|
|
|
|
})?,
|
|
|
|
Some(&output.memo),
|
|
|
|
)?;
|
|
|
|
|
|
|
|
if matches!(recipient, Recipient::InternalAccount(_, _)) {
|
|
|
|
wallet::sapling::put_received_note(wdb.conn.0, output, tx_ref)?;
|
|
|
|
}
|
2022-09-26 09:59:50 -07:00
|
|
|
}
|
2023-06-30 09:42:48 -07:00
|
|
|
TransferType::Incoming => {
|
|
|
|
match spending_account_id {
|
|
|
|
Some(id) => {
|
|
|
|
if id != output.account {
|
|
|
|
panic!("Unable to determine a unique account identifier for z->t spend.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None => {
|
|
|
|
spending_account_id = Some(output.account);
|
2021-03-29 13:55:15 -07:00
|
|
|
}
|
|
|
|
}
|
2023-06-09 10:02:00 -07:00
|
|
|
|
2023-06-30 09:42:48 -07:00
|
|
|
wallet::sapling::put_received_note(wdb.conn.0, output, tx_ref)?;
|
|
|
|
}
|
2021-03-09 17:10:44 -08:00
|
|
|
}
|
|
|
|
}
|
2020-08-20 10:41:43 -07:00
|
|
|
|
2023-04-03 12:53:43 -07:00
|
|
|
// If any of the utxos spent in the transaction are ours, mark them as spent.
|
|
|
|
#[cfg(feature = "transparent-inputs")]
|
|
|
|
for txin in d_tx.tx.transparent_bundle().iter().flat_map(|b| b.vin.iter()) {
|
2023-06-13 10:20:18 -07:00
|
|
|
wallet::mark_transparent_utxo_spent(wdb.conn.0, tx_ref, &txin.prevout)?;
|
2023-04-03 12:53:43 -07:00
|
|
|
}
|
2022-10-13 19:20:46 -07:00
|
|
|
|
2023-04-03 12:53:43 -07:00
|
|
|
// If we have some transparent outputs:
|
|
|
|
if d_tx.tx.transparent_bundle().iter().any(|b| !b.vout.is_empty()) {
|
|
|
|
let nullifiers = wdb.get_sapling_nullifiers(NullifierQuery::All)?;
|
|
|
|
// If the transaction contains shielded spends from our wallet, we will store z->t
|
|
|
|
// transactions we observe in the same way they would be stored by
|
|
|
|
// create_spend_to_address.
|
|
|
|
if let Some((account_id, _)) = nullifiers.iter().find(
|
|
|
|
|(_, nf)|
|
|
|
|
d_tx.tx.sapling_bundle().iter().flat_map(|b| b.shielded_spends().iter())
|
|
|
|
.any(|input| nf == input.nullifier())
|
|
|
|
) {
|
|
|
|
for (output_index, txout) in d_tx.tx.transparent_bundle().iter().flat_map(|b| b.vout.iter()).enumerate() {
|
|
|
|
if let Some(address) = txout.recipient_address() {
|
|
|
|
wallet::put_sent_output(
|
2023-06-13 10:20:18 -07:00
|
|
|
wdb.conn.0,
|
2023-04-03 12:53:43 -07:00
|
|
|
&wdb.params,
|
|
|
|
*account_id,
|
|
|
|
tx_ref,
|
|
|
|
output_index,
|
|
|
|
&Recipient::Transparent(address),
|
|
|
|
txout.value,
|
|
|
|
None
|
|
|
|
)?;
|
|
|
|
}
|
2021-03-29 13:55:15 -07:00
|
|
|
}
|
2021-03-29 13:35:18 -07:00
|
|
|
}
|
|
|
|
}
|
2023-06-30 09:42:48 -07:00
|
|
|
|
|
|
|
Ok(tx_ref)
|
2021-03-09 17:10:44 -08:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
fn store_sent_tx(&mut self, sent_tx: &SentTransaction) -> Result<Self::TxRef, Self::Error> {
|
2023-06-09 10:02:00 -07:00
|
|
|
self.transactionally(|wdb| {
|
2022-09-01 20:03:39 -07:00
|
|
|
let tx_ref = wallet::put_tx_data(
|
2023-06-13 10:20:18 -07:00
|
|
|
wdb.conn.0,
|
2022-09-01 20:03:39 -07:00
|
|
|
sent_tx.tx,
|
|
|
|
Some(sent_tx.fee_amount),
|
|
|
|
Some(sent_tx.created),
|
|
|
|
)?;
|
2021-03-09 17:10:44 -08:00
|
|
|
|
|
|
|
// Mark notes as spent.
|
|
|
|
//
|
|
|
|
// This locks the notes so they aren't selected again by a subsequent call to
|
|
|
|
// create_spend_to_address() before this transaction has been mined (at which point the notes
|
|
|
|
// get re-marked as spent).
|
|
|
|
//
|
|
|
|
// Assumes that create_spend_to_address() will never be called in parallel, which is a
|
|
|
|
// reasonable assumption for a light client such as a mobile phone.
|
2021-05-12 23:16:40 -07:00
|
|
|
if let Some(bundle) = sent_tx.tx.sapling_bundle() {
|
2022-12-10 14:27:09 -08:00
|
|
|
for spend in bundle.shielded_spends() {
|
2023-06-07 14:03:20 -07:00
|
|
|
wallet::sapling::mark_sapling_note_spent(
|
2023-06-13 10:20:18 -07:00
|
|
|
wdb.conn.0,
|
2023-06-07 14:03:20 -07:00
|
|
|
tx_ref,
|
|
|
|
spend.nullifier(),
|
|
|
|
)?;
|
2021-04-07 14:58:30 -07:00
|
|
|
}
|
2020-12-22 06:10:13 -08:00
|
|
|
}
|
|
|
|
|
2022-01-20 13:33:29 -08:00
|
|
|
#[cfg(feature = "transparent-inputs")]
|
2020-12-22 06:10:13 -08:00
|
|
|
for utxo_outpoint in &sent_tx.utxos_spent {
|
2023-06-13 10:20:18 -07:00
|
|
|
wallet::mark_transparent_utxo_spent(wdb.conn.0, tx_ref, utxo_outpoint)?;
|
2021-03-09 17:10:44 -08:00
|
|
|
}
|
|
|
|
|
2021-04-09 13:40:35 -07:00
|
|
|
for output in &sent_tx.outputs {
|
2023-06-07 14:41:52 -07:00
|
|
|
wallet::insert_sent_output(
|
2023-06-13 10:20:18 -07:00
|
|
|
wdb.conn.0,
|
2023-06-09 10:02:00 -07:00
|
|
|
&wdb.params,
|
2023-06-07 14:41:52 -07:00
|
|
|
tx_ref,
|
|
|
|
sent_tx.account,
|
|
|
|
output,
|
|
|
|
)?;
|
2023-04-24 15:00:43 -07:00
|
|
|
|
|
|
|
if let Some((account, note)) = output.sapling_change_to() {
|
2023-06-02 07:33:19 -07:00
|
|
|
wallet::sapling::put_received_note(
|
2023-06-13 10:20:18 -07:00
|
|
|
wdb.conn.0,
|
2023-04-24 15:00:43 -07:00
|
|
|
&DecryptedOutput {
|
|
|
|
index: output.output_index(),
|
|
|
|
note: note.clone(),
|
|
|
|
account: *account,
|
|
|
|
memo: output
|
|
|
|
.memo()
|
|
|
|
.map_or_else(MemoBytes::empty, |memo| memo.clone()),
|
|
|
|
transfer_type: TransferType::WalletInternal,
|
|
|
|
},
|
|
|
|
tx_ref,
|
|
|
|
)?;
|
|
|
|
}
|
2021-04-09 13:40:35 -07:00
|
|
|
}
|
2021-03-09 17:10:44 -08:00
|
|
|
|
|
|
|
// Return the row number of the transaction, so the caller can fetch it for sending.
|
|
|
|
Ok(tx_ref)
|
|
|
|
})
|
2020-08-20 10:41:43 -07:00
|
|
|
}
|
|
|
|
|
2023-04-26 15:27:56 -07:00
|
|
|
fn truncate_to_height(&mut self, block_height: BlockHeight) -> Result<(), Self::Error> {
|
2023-06-09 10:02:00 -07:00
|
|
|
self.transactionally(|wdb| {
|
2023-06-13 10:20:18 -07:00
|
|
|
wallet::truncate_to_height(wdb.conn.0, &wdb.params, block_height)
|
2023-06-09 10:02:00 -07:00
|
|
|
})
|
2020-08-20 10:41:43 -07:00
|
|
|
}
|
2022-01-31 15:22:20 -08:00
|
|
|
|
|
|
|
fn put_received_transparent_utxo(
|
|
|
|
&mut self,
|
2022-10-13 19:58:43 -07:00
|
|
|
_output: &WalletTransparentOutput,
|
2022-01-31 15:22:20 -08:00
|
|
|
) -> Result<Self::UtxoRef, Self::Error> {
|
2022-10-13 19:58:43 -07:00
|
|
|
#[cfg(feature = "transparent-inputs")]
|
2023-06-09 10:02:00 -07:00
|
|
|
return wallet::put_received_transparent_utxo(&self.conn, &self.params, _output);
|
2022-10-13 19:58:43 -07:00
|
|
|
|
|
|
|
#[cfg(not(feature = "transparent-inputs"))]
|
2022-10-17 10:35:14 -07:00
|
|
|
panic!(
|
|
|
|
"The wallet must be compiled with the transparent-inputs feature to use this method."
|
|
|
|
);
|
2022-01-31 15:22:20 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-03 12:53:43 -07:00
|
|
|
impl<P: consensus::Parameters> WalletCommitmentTrees for WalletDb<rusqlite::Connection, P> {
|
2023-04-03 12:53:43 -07:00
|
|
|
type Error = Either<io::Error, rusqlite::Error>;
|
2023-06-15 12:50:07 -07:00
|
|
|
type SaplingShardStore<'a> =
|
|
|
|
SqliteShardStore<&'a rusqlite::Transaction<'a>, sapling::Node, SAPLING_SHARD_HEIGHT>;
|
2023-04-03 12:53:43 -07:00
|
|
|
|
|
|
|
fn with_sapling_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
|
|
|
|
where
|
|
|
|
for<'a> F: FnMut(
|
|
|
|
&'a mut ShardTree<
|
|
|
|
Self::SaplingShardStore<'a>,
|
|
|
|
{ sapling::NOTE_COMMITMENT_TREE_DEPTH },
|
|
|
|
SAPLING_SHARD_HEIGHT,
|
|
|
|
>,
|
|
|
|
) -> Result<A, E>,
|
2023-04-03 12:53:43 -07:00
|
|
|
E: From<ShardTreeError<Either<io::Error, rusqlite::Error>>>,
|
2023-04-03 12:53:43 -07:00
|
|
|
{
|
2023-04-03 12:53:43 -07:00
|
|
|
let tx = self
|
|
|
|
.conn
|
|
|
|
.transaction()
|
|
|
|
.map_err(|e| ShardTreeError::Storage(Either::Right(e)))?;
|
2023-06-29 15:26:22 -07:00
|
|
|
let shard_store = SqliteShardStore::from_connection(&tx, SAPLING_TABLES_PREFIX)
|
2023-04-03 12:53:43 -07:00
|
|
|
.map_err(|e| ShardTreeError::Storage(Either::Right(e)))?;
|
2023-04-03 12:53:43 -07:00
|
|
|
let result = {
|
2023-06-29 15:26:22 -07:00
|
|
|
let mut shardtree = ShardTree::new(shard_store, PRUNING_HEIGHT.try_into().unwrap());
|
2023-04-03 12:53:43 -07:00
|
|
|
callback(&mut shardtree)?
|
|
|
|
};
|
2023-04-03 12:53:43 -07:00
|
|
|
tx.commit()
|
|
|
|
.map_err(|e| ShardTreeError::Storage(Either::Right(e)))?;
|
2023-04-03 12:53:43 -07:00
|
|
|
Ok(result)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'conn, P: consensus::Parameters> WalletCommitmentTrees for WalletDb<SqlTransaction<'conn>, P> {
|
2023-04-03 12:53:43 -07:00
|
|
|
type Error = Either<io::Error, rusqlite::Error>;
|
2023-06-15 12:50:07 -07:00
|
|
|
type SaplingShardStore<'a> =
|
|
|
|
SqliteShardStore<&'a rusqlite::Transaction<'a>, sapling::Node, SAPLING_SHARD_HEIGHT>;
|
2023-04-03 12:53:43 -07:00
|
|
|
|
|
|
|
fn with_sapling_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
|
|
|
|
where
|
|
|
|
for<'a> F: FnMut(
|
|
|
|
&'a mut ShardTree<
|
|
|
|
Self::SaplingShardStore<'a>,
|
|
|
|
{ sapling::NOTE_COMMITMENT_TREE_DEPTH },
|
|
|
|
SAPLING_SHARD_HEIGHT,
|
|
|
|
>,
|
|
|
|
) -> Result<A, E>,
|
2023-04-03 12:53:43 -07:00
|
|
|
E: From<ShardTreeError<Either<io::Error, rusqlite::Error>>>,
|
2023-04-03 12:53:43 -07:00
|
|
|
{
|
|
|
|
let mut shardtree = ShardTree::new(
|
2023-06-29 15:26:22 -07:00
|
|
|
SqliteShardStore::from_connection(self.conn.0, SAPLING_TABLES_PREFIX)
|
2023-04-03 12:53:43 -07:00
|
|
|
.map_err(|e| ShardTreeError::Storage(Either::Right(e)))?,
|
2023-06-29 15:26:22 -07:00
|
|
|
PRUNING_HEIGHT.try_into().unwrap(),
|
2023-04-03 12:53:43 -07:00
|
|
|
);
|
|
|
|
let result = callback(&mut shardtree)?;
|
|
|
|
|
|
|
|
Ok(result)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-28 16:21:04 -07:00
|
|
|
/// A handle for the SQLite block source.
|
2021-03-26 22:17:54 -07:00
|
|
|
pub struct BlockDb(Connection);
|
2020-08-05 18:14:45 -07:00
|
|
|
|
2021-03-26 22:17:54 -07:00
|
|
|
impl BlockDb {
|
2021-03-25 22:47:59 -07:00
|
|
|
/// Opens a connection to the wallet database stored at the specified path.
|
2020-08-05 18:14:45 -07:00
|
|
|
pub fn for_path<P: AsRef<Path>>(path: P) -> Result<Self, rusqlite::Error> {
|
2021-03-26 22:17:54 -07:00
|
|
|
Connection::open(path).map(BlockDb)
|
2020-08-05 18:14:45 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-26 22:17:54 -07:00
|
|
|
impl BlockSource for BlockDb {
|
2021-01-13 14:20:11 -08:00
|
|
|
type Error = SqliteClientError;
|
2020-08-05 18:14:45 -07:00
|
|
|
|
2023-07-01 17:16:23 -07:00
|
|
|
fn with_blocks<F, DbErrT>(
|
2020-08-18 15:33:34 -07:00
|
|
|
&self,
|
2022-12-08 18:46:32 -08:00
|
|
|
from_height: Option<BlockHeight>,
|
2020-08-18 15:33:34 -07:00
|
|
|
limit: Option<u32>,
|
|
|
|
with_row: F,
|
2023-07-01 17:16:23 -07:00
|
|
|
) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>
|
2020-08-18 15:33:34 -07:00
|
|
|
where
|
2023-07-01 17:16:23 -07:00
|
|
|
F: FnMut(CompactBlock) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>,
|
2020-08-18 15:33:34 -07:00
|
|
|
{
|
2022-07-28 16:21:04 -07:00
|
|
|
chain::blockdb_with_blocks(self, from_height, limit, with_row)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// A block source that reads block data from disk and block metadata from a SQLite database.
|
|
|
|
///
|
|
|
|
/// This block source expects each compact block to be stored on disk in the `blocks` subdirectory
|
|
|
|
/// of the `blockstore_root` path provided at the time of construction. Each block should be
|
|
|
|
/// written, as the serialized bytes of its protobuf representation, where the path for each block
|
|
|
|
/// has the pattern:
|
|
|
|
///
|
|
|
|
/// `<blockstore_root>/blocks/<block_height>-<block_hash>-compactblock`
|
|
|
|
///
|
|
|
|
/// where `<block_height>` is the decimal value of the height at which the block was mined, and
|
|
|
|
/// `<block_hash>` is the hexadecimal representation of the block hash, as produced by the
|
2022-10-17 10:35:14 -07:00
|
|
|
/// [`fmt::Display`] implementation for [`zcash_primitives::block::BlockHash`].
|
2022-07-28 16:21:04 -07:00
|
|
|
///
|
|
|
|
/// This block source is intended to be used with the following data flow:
|
|
|
|
/// * When the cache is being filled:
|
|
|
|
/// * The caller requests the current maximum height height at which cached data is available
|
|
|
|
/// using [`FsBlockDb::get_max_cached_height`]. If no cached data is available, the caller
|
|
|
|
/// can use the wallet's synced-to height for the following operations instead.
|
|
|
|
/// * (recommended for privacy) the caller should round the returned height down to some 100- /
|
|
|
|
/// 1000-block boundary.
|
|
|
|
/// * The caller uses the lightwalletd's `getblock` gRPC method to obtain a stream of blocks.
|
|
|
|
/// For each block returned, the caller writes the compact block to `blocks_dir` using the
|
|
|
|
/// path format specified above. It is fine to overwrite an existing block, since block hashes
|
|
|
|
/// are immutable and collision-resistant.
|
|
|
|
/// * Once a caller-determined number of blocks have been successfully written to disk, the
|
|
|
|
/// caller should invoke [`FsBlockDb::write_block_metadata`] with the metadata for each block
|
|
|
|
/// written to disk.
|
|
|
|
/// * The cache can then be scanned using the [`BlockSource`] implementation, providing the
|
|
|
|
/// wallet's synced-to-height as a starting point.
|
|
|
|
/// * When part of the cache is no longer needed:
|
|
|
|
/// * The caller determines some height `H` that is the earliest block data it needs to preserve.
|
|
|
|
/// This might be determined based on where the wallet is fully-synced to, or other heuristics.
|
|
|
|
/// * The caller searches the defined filesystem folder for all files beginning in `HEIGHT-*` where
|
|
|
|
/// `HEIGHT < H`, and deletes those files.
|
|
|
|
///
|
|
|
|
/// Note: This API is unstable, and may change in the future. In particular, the [`BlockSource`]
|
|
|
|
/// API and the above description currently assume that scanning is performed in linear block
|
|
|
|
/// order; this assumption is likely to be weakened and/or removed in a future update.
|
|
|
|
#[cfg(feature = "unstable")]
|
|
|
|
pub struct FsBlockDb {
|
|
|
|
conn: Connection,
|
|
|
|
blocks_dir: PathBuf,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Errors that can be generated by the filesystem/sqlite-backed
|
|
|
|
/// block source.
|
|
|
|
#[derive(Debug)]
|
|
|
|
#[cfg(feature = "unstable")]
|
|
|
|
pub enum FsBlockDbError {
|
2022-12-08 15:22:00 -08:00
|
|
|
Fs(io::Error),
|
|
|
|
Db(rusqlite::Error),
|
2022-10-17 10:35:14 -07:00
|
|
|
Protobuf(prost::DecodeError),
|
2022-12-09 01:47:20 -08:00
|
|
|
MissingBlockPath(PathBuf),
|
2022-07-28 16:21:04 -07:00
|
|
|
InvalidBlockstoreRoot(PathBuf),
|
|
|
|
InvalidBlockPath(PathBuf),
|
|
|
|
CorruptedData(String),
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(feature = "unstable")]
|
|
|
|
impl From<io::Error> for FsBlockDbError {
|
|
|
|
fn from(err: io::Error) -> Self {
|
2022-12-08 15:22:00 -08:00
|
|
|
FsBlockDbError::Fs(err)
|
2022-07-28 16:21:04 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(feature = "unstable")]
|
|
|
|
impl From<rusqlite::Error> for FsBlockDbError {
|
|
|
|
fn from(err: rusqlite::Error) -> Self {
|
2022-12-08 15:22:00 -08:00
|
|
|
FsBlockDbError::Db(err)
|
2022-07-28 16:21:04 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-17 10:35:14 -07:00
|
|
|
#[cfg(feature = "unstable")]
|
|
|
|
impl From<prost::DecodeError> for FsBlockDbError {
|
|
|
|
fn from(e: prost::DecodeError) -> Self {
|
|
|
|
FsBlockDbError::Protobuf(e)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-28 16:21:04 -07:00
|
|
|
#[cfg(feature = "unstable")]
|
|
|
|
impl FsBlockDb {
|
|
|
|
/// Creates a filesystem-backed block store at the given path.
|
|
|
|
///
|
|
|
|
/// This will construct or open a SQLite database at the path
|
|
|
|
/// `<fsblockdb_root>/blockmeta.sqlite` and will ensure that a directory exists at
|
|
|
|
/// `<fsblockdb_root>/blocks` where this block store will expect to find serialized block
|
|
|
|
/// files as described for [`FsBlockDb`].
|
2023-01-26 13:33:50 -08:00
|
|
|
///
|
|
|
|
/// An application using this constructor should ensure that they call
|
|
|
|
/// [`zcash_client_sqlite::chain::init::init_blockmetadb`] at application startup to ensure
|
|
|
|
/// that the resulting metadata database is properly initialized and has had all required
|
|
|
|
/// migrations applied before use.
|
2022-07-28 16:21:04 -07:00
|
|
|
pub fn for_path<P: AsRef<Path>>(fsblockdb_root: P) -> Result<Self, FsBlockDbError> {
|
2022-12-08 15:22:00 -08:00
|
|
|
let meta = fs::metadata(&fsblockdb_root).map_err(FsBlockDbError::Fs)?;
|
2022-07-28 16:21:04 -07:00
|
|
|
if meta.is_dir() {
|
|
|
|
let db_path = fsblockdb_root.as_ref().join("blockmeta.sqlite");
|
|
|
|
let blocks_dir = fsblockdb_root.as_ref().join("blocks");
|
|
|
|
fs::create_dir_all(&blocks_dir)?;
|
|
|
|
Ok(FsBlockDb {
|
2022-12-08 15:22:00 -08:00
|
|
|
conn: Connection::open(db_path).map_err(FsBlockDbError::Db)?,
|
2022-07-28 16:21:04 -07:00
|
|
|
blocks_dir,
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
Err(FsBlockDbError::InvalidBlockstoreRoot(
|
|
|
|
fsblockdb_root.as_ref().to_path_buf(),
|
|
|
|
))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns the maximum height of blocks known to the block metadata database.
|
|
|
|
pub fn get_max_cached_height(&self) -> Result<Option<BlockHeight>, FsBlockDbError> {
|
|
|
|
Ok(chain::blockmetadb_get_max_cached_height(&self.conn)?)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Adds a set of block metadata entries to the metadata database.
|
|
|
|
///
|
|
|
|
/// This will return an error if any block file corresponding to one of these metadata records
|
|
|
|
/// is absent from the blocks directory.
|
|
|
|
pub fn write_block_metadata(&self, block_meta: &[BlockMeta]) -> Result<(), FsBlockDbError> {
|
|
|
|
for m in block_meta {
|
|
|
|
let block_path = m.block_file_path(&self.blocks_dir);
|
2022-12-09 01:47:20 -08:00
|
|
|
match fs::metadata(&block_path) {
|
|
|
|
Err(e) => {
|
|
|
|
return Err(match e.kind() {
|
|
|
|
io::ErrorKind::NotFound => FsBlockDbError::MissingBlockPath(block_path),
|
|
|
|
_ => FsBlockDbError::Fs(e),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
Ok(meta) => {
|
|
|
|
if !meta.is_file() {
|
|
|
|
return Err(FsBlockDbError::InvalidBlockPath(block_path));
|
|
|
|
}
|
|
|
|
}
|
2022-07-28 16:21:04 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(chain::blockmetadb_insert(&self.conn, block_meta)?)
|
|
|
|
}
|
2023-01-24 15:30:04 -08:00
|
|
|
|
|
|
|
/// Returns the metadata for the block with the given height, if it exists in the
|
|
|
|
/// database.
|
|
|
|
pub fn find_block(&self, height: BlockHeight) -> Result<Option<BlockMeta>, FsBlockDbError> {
|
|
|
|
Ok(chain::blockmetadb_find_block(&self.conn, height)?)
|
|
|
|
}
|
|
|
|
|
2023-01-11 15:07:21 -08:00
|
|
|
/// Rewinds the BlockMeta Db to the `block_height` provided.
|
|
|
|
///
|
|
|
|
/// This doesn't delete any files referenced by the records
|
|
|
|
/// stored in BlockMeta.
|
|
|
|
///
|
|
|
|
/// If the requested height is greater than or equal to the height
|
|
|
|
/// of the last scanned block, or if the DB is empty, this function
|
|
|
|
/// does nothing.
|
2023-04-26 15:27:56 -07:00
|
|
|
pub fn truncate_to_height(&self, block_height: BlockHeight) -> Result<(), FsBlockDbError> {
|
|
|
|
Ok(chain::blockmetadb_truncate_to_height(
|
2023-01-11 15:07:21 -08:00
|
|
|
&self.conn,
|
|
|
|
block_height,
|
|
|
|
)?)
|
|
|
|
}
|
2022-07-28 16:21:04 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(feature = "unstable")]
|
|
|
|
impl BlockSource for FsBlockDb {
|
2022-10-17 10:35:14 -07:00
|
|
|
type Error = FsBlockDbError;
|
2022-07-28 16:21:04 -07:00
|
|
|
|
2023-07-01 17:16:23 -07:00
|
|
|
fn with_blocks<F, DbErrT>(
|
2022-07-28 16:21:04 -07:00
|
|
|
&self,
|
2022-12-08 18:46:32 -08:00
|
|
|
from_height: Option<BlockHeight>,
|
2022-07-28 16:21:04 -07:00
|
|
|
limit: Option<u32>,
|
|
|
|
with_row: F,
|
2023-07-01 17:16:23 -07:00
|
|
|
) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>
|
2022-07-28 16:21:04 -07:00
|
|
|
where
|
2023-07-01 17:16:23 -07:00
|
|
|
F: FnMut(CompactBlock) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>,
|
2022-07-28 16:21:04 -07:00
|
|
|
{
|
|
|
|
fsblockdb_with_blocks(self, from_height, limit, with_row)
|
2020-08-18 15:33:34 -07:00
|
|
|
}
|
2020-08-05 18:14:45 -07:00
|
|
|
}
|
2019-04-08 04:24:49 -07:00
|
|
|
|
2022-12-13 15:13:55 -08:00
|
|
|
#[cfg(feature = "unstable")]
|
|
|
|
impl std::fmt::Display for FsBlockDbError {
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
|
|
match self {
|
|
|
|
FsBlockDbError::Fs(io_error) => {
|
|
|
|
write!(f, "Failed to access the file system: {}", io_error)
|
|
|
|
}
|
|
|
|
FsBlockDbError::Db(e) => {
|
|
|
|
write!(f, "There was a problem with the sqlite db: {}", e)
|
|
|
|
}
|
|
|
|
FsBlockDbError::Protobuf(e) => {
|
|
|
|
write!(f, "Failed to parse protobuf-encoded record: {}", e)
|
|
|
|
}
|
|
|
|
FsBlockDbError::MissingBlockPath(block_path) => {
|
|
|
|
write!(
|
|
|
|
f,
|
|
|
|
"CompactBlock file expected but not found at {}",
|
|
|
|
block_path.display(),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
FsBlockDbError::InvalidBlockstoreRoot(fsblockdb_root) => {
|
|
|
|
write!(
|
|
|
|
f,
|
|
|
|
"The block storage root {} is not a directory",
|
|
|
|
fsblockdb_root.display(),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
FsBlockDbError::InvalidBlockPath(block_path) => {
|
|
|
|
write!(
|
|
|
|
f,
|
|
|
|
"CompactBlock path {} is not a file",
|
|
|
|
block_path.display(),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
FsBlockDbError::CorruptedData(e) => {
|
|
|
|
write!(
|
|
|
|
f,
|
|
|
|
"The block cache has corrupted data and this caused an error: {}",
|
|
|
|
e,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-17 10:35:14 -07:00
|
|
|
#[cfg(test)]
|
|
|
|
#[macro_use]
|
|
|
|
extern crate assert_matches;
|
|
|
|
|
2019-03-08 19:12:31 -08:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
2022-11-01 17:42:41 -07:00
|
|
|
use prost::Message;
|
2019-03-08 19:12:31 -08:00
|
|
|
use rand_core::{OsRng, RngCore};
|
2021-01-12 20:10:34 -08:00
|
|
|
use rusqlite::params;
|
2022-06-27 12:48:25 -07:00
|
|
|
use std::collections::HashMap;
|
2020-08-05 13:27:40 -07:00
|
|
|
|
2023-01-25 07:29:22 -08:00
|
|
|
#[cfg(feature = "unstable")]
|
|
|
|
use std::{fs::File, path::Path};
|
|
|
|
|
2021-10-04 13:09:02 -07:00
|
|
|
#[cfg(feature = "transparent-inputs")]
|
2022-01-26 12:24:56 -08:00
|
|
|
use zcash_primitives::{legacy, legacy::keys::IncomingViewingKey};
|
2021-10-04 13:09:02 -07:00
|
|
|
|
2023-01-10 05:43:09 -08:00
|
|
|
use zcash_note_encryption::Domain;
|
2019-03-08 19:12:31 -08:00
|
|
|
use zcash_primitives::{
|
|
|
|
block::BlockHash,
|
2020-08-05 13:27:40 -07:00
|
|
|
consensus::{BlockHeight, Network, NetworkUpgrade, Parameters},
|
2021-03-31 14:59:36 -07:00
|
|
|
legacy::TransparentAddress,
|
2020-10-29 09:48:26 -07:00
|
|
|
memo::MemoBytes,
|
2021-03-04 13:45:41 -08:00
|
|
|
sapling::{
|
2023-01-10 05:43:09 -08:00
|
|
|
note_encryption::{sapling_note_encryption, SaplingDomain},
|
|
|
|
util::generate_random_rseed,
|
|
|
|
value::NoteValue,
|
|
|
|
Note, Nullifier, PaymentAddress,
|
2021-03-04 13:45:41 -08:00
|
|
|
},
|
2019-03-08 19:12:31 -08:00
|
|
|
transaction::components::Amount,
|
2022-11-04 12:42:11 -07:00
|
|
|
zip32::{sapling::DiversifiableFullViewingKey, DiversifierIndex},
|
2019-03-08 19:12:31 -08:00
|
|
|
};
|
|
|
|
|
2022-09-12 11:42:12 -07:00
|
|
|
use zcash_client_backend::{
|
|
|
|
data_api::{WalletRead, WalletWrite},
|
|
|
|
keys::{sapling, UnifiedFullViewingKey},
|
|
|
|
proto::compact_formats::{
|
2023-07-01 17:16:23 -07:00
|
|
|
self as compact, CompactBlock, CompactSaplingOutput, CompactSaplingSpend, CompactTx,
|
2022-09-12 11:42:12 -07:00
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
use crate::{
|
|
|
|
wallet::init::{init_accounts_table, init_wallet_db},
|
|
|
|
AccountId, WalletDb,
|
|
|
|
};
|
2021-03-31 14:59:36 -07:00
|
|
|
|
2021-03-26 22:17:54 -07:00
|
|
|
use super::BlockDb;
|
2020-08-05 18:14:45 -07:00
|
|
|
|
2023-01-25 07:29:22 -08:00
|
|
|
#[cfg(feature = "unstable")]
|
|
|
|
use super::{
|
|
|
|
chain::{init::init_blockmeta_db, BlockMeta},
|
|
|
|
FsBlockDb,
|
|
|
|
};
|
|
|
|
|
2020-08-05 13:27:40 -07:00
|
|
|
#[cfg(feature = "mainnet")]
|
|
|
|
pub(crate) fn network() -> Network {
|
|
|
|
Network::MainNetwork
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(not(feature = "mainnet"))]
|
|
|
|
pub(crate) fn network() -> Network {
|
|
|
|
Network::TestNetwork
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(feature = "mainnet")]
|
|
|
|
pub(crate) fn sapling_activation_height() -> BlockHeight {
|
|
|
|
Network::MainNetwork
|
|
|
|
.activation_height(NetworkUpgrade::Sapling)
|
|
|
|
.unwrap()
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(not(feature = "mainnet"))]
|
|
|
|
pub(crate) fn sapling_activation_height() -> BlockHeight {
|
|
|
|
Network::TestNetwork
|
|
|
|
.activation_height(NetworkUpgrade::Sapling)
|
|
|
|
.unwrap()
|
|
|
|
}
|
|
|
|
|
2022-01-20 13:33:29 -08:00
|
|
|
#[cfg(test)]
|
2021-03-31 14:59:36 -07:00
|
|
|
pub(crate) fn init_test_accounts_table(
|
2023-06-09 10:02:00 -07:00
|
|
|
db_data: &mut WalletDb<rusqlite::Connection, Network>,
|
2022-06-13 19:27:55 -07:00
|
|
|
) -> (DiversifiableFullViewingKey, Option<TransparentAddress>) {
|
2022-09-08 11:48:06 -07:00
|
|
|
let (ufvk, taddr) = init_test_accounts_table_ufvk(db_data);
|
|
|
|
(ufvk.sapling().unwrap().clone(), taddr)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
pub(crate) fn init_test_accounts_table_ufvk(
|
2023-06-09 10:02:00 -07:00
|
|
|
db_data: &mut WalletDb<rusqlite::Connection, Network>,
|
2022-09-08 11:48:06 -07:00
|
|
|
) -> (UnifiedFullViewingKey, Option<TransparentAddress>) {
|
2021-10-04 13:09:02 -07:00
|
|
|
let seed = [0u8; 32];
|
2022-02-10 08:47:42 -08:00
|
|
|
let account = AccountId::from(0);
|
2022-02-01 10:37:43 -08:00
|
|
|
let extsk = sapling::spending_key(&seed, network().coin_type(), account);
|
2022-10-31 13:54:33 -07:00
|
|
|
let dfvk = extsk.to_diversifiable_full_viewing_key();
|
2021-10-04 13:09:02 -07:00
|
|
|
|
|
|
|
#[cfg(feature = "transparent-inputs")]
|
2022-02-01 10:37:43 -08:00
|
|
|
let (tkey, taddr) = {
|
|
|
|
let tkey = legacy::keys::AccountPrivKey::from_seed(&network(), &seed, account)
|
|
|
|
.unwrap()
|
|
|
|
.to_account_pubkey();
|
|
|
|
let taddr = tkey.derive_external_ivk().unwrap().default_address().0;
|
|
|
|
(Some(tkey), Some(taddr))
|
|
|
|
};
|
2021-10-04 13:09:02 -07:00
|
|
|
|
|
|
|
#[cfg(not(feature = "transparent-inputs"))]
|
2022-02-01 10:37:43 -08:00
|
|
|
let taddr = None;
|
|
|
|
|
|
|
|
let ufvk = UnifiedFullViewingKey::new(
|
|
|
|
#[cfg(feature = "transparent-inputs")]
|
|
|
|
tkey,
|
2022-09-08 11:48:06 -07:00
|
|
|
Some(dfvk),
|
2022-06-13 19:41:01 -07:00
|
|
|
None,
|
2022-02-01 10:37:43 -08:00
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
|
2022-09-08 11:48:06 -07:00
|
|
|
let ufvks = HashMap::from([(account, ufvk.clone())]);
|
2022-06-27 12:48:25 -07:00
|
|
|
init_accounts_table(db_data, &ufvks).unwrap();
|
2022-02-01 10:37:43 -08:00
|
|
|
|
2022-09-08 11:48:06 -07:00
|
|
|
(ufvk, taddr)
|
2021-03-31 14:59:36 -07:00
|
|
|
}
|
|
|
|
|
2022-11-04 12:42:11 -07:00
|
|
|
#[allow(dead_code)]
|
|
|
|
pub(crate) enum AddressType {
|
|
|
|
DefaultExternal,
|
|
|
|
DiversifiedExternal(DiversifierIndex),
|
|
|
|
Internal,
|
|
|
|
}
|
|
|
|
|
2019-03-08 19:12:31 -08:00
|
|
|
/// Create a fake CompactBlock at the given height, containing a single output paying
|
2022-11-04 12:42:11 -07:00
|
|
|
/// an address. Returns the CompactBlock and the nullifier for the new note.
|
2019-03-08 19:12:31 -08:00
|
|
|
pub(crate) fn fake_compact_block(
|
2020-08-05 13:08:58 -07:00
|
|
|
height: BlockHeight,
|
2019-03-08 19:12:31 -08:00
|
|
|
prev_hash: BlockHash,
|
2022-06-13 19:27:55 -07:00
|
|
|
dfvk: &DiversifiableFullViewingKey,
|
2022-11-04 12:42:11 -07:00
|
|
|
req: AddressType,
|
2019-03-08 19:12:31 -08:00
|
|
|
value: Amount,
|
2023-04-03 12:53:43 -07:00
|
|
|
initial_sapling_tree_size: u32,
|
2021-01-11 17:13:40 -08:00
|
|
|
) -> (CompactBlock, Nullifier) {
|
2022-11-04 12:42:11 -07:00
|
|
|
let to = match req {
|
|
|
|
AddressType::DefaultExternal => dfvk.default_address().1,
|
|
|
|
AddressType::DiversifiedExternal(idx) => dfvk.find_address(idx).unwrap().1,
|
|
|
|
AddressType::Internal => dfvk.change_address().1,
|
|
|
|
};
|
2019-03-08 19:12:31 -08:00
|
|
|
|
|
|
|
// Create a fake Note for the account
|
|
|
|
let mut rng = OsRng;
|
2020-08-05 13:27:40 -07:00
|
|
|
let rseed = generate_random_rseed(&network(), height, &mut rng);
|
2023-01-09 15:25:49 -08:00
|
|
|
let note = Note::from_parts(to, NoteValue::from_raw(value.into()), rseed);
|
2021-03-17 17:22:21 -07:00
|
|
|
let encryptor = sapling_note_encryption::<_, Network>(
|
2022-06-13 19:27:55 -07:00
|
|
|
Some(dfvk.fvk().ovk),
|
2019-03-08 19:12:31 -08:00
|
|
|
note.clone(),
|
2021-03-17 00:47:11 -07:00
|
|
|
MemoBytes::empty(),
|
2020-08-05 21:47:35 -07:00
|
|
|
&mut rng,
|
2019-03-08 19:12:31 -08:00
|
|
|
);
|
2023-01-06 09:01:05 -08:00
|
|
|
let cmu = note.cmu().to_bytes().to_vec();
|
2023-01-10 05:43:09 -08:00
|
|
|
let ephemeral_key = SaplingDomain::<Network>::epk_bytes(encryptor.epk())
|
|
|
|
.0
|
|
|
|
.to_vec();
|
2019-03-08 19:12:31 -08:00
|
|
|
let enc_ciphertext = encryptor.encrypt_note_plaintext();
|
|
|
|
|
|
|
|
// Create a fake CompactBlock containing the note
|
2022-11-01 17:42:41 -07:00
|
|
|
let cout = CompactSaplingOutput {
|
|
|
|
cmu,
|
|
|
|
ephemeral_key,
|
|
|
|
ciphertext: enc_ciphertext.as_ref()[..52].to_vec(),
|
|
|
|
};
|
|
|
|
let mut ctx = CompactTx::default();
|
2019-03-08 19:12:31 -08:00
|
|
|
let mut txid = vec![0; 32];
|
|
|
|
rng.fill_bytes(&mut txid);
|
2022-11-01 17:42:41 -07:00
|
|
|
ctx.hash = txid;
|
2019-03-08 19:12:31 -08:00
|
|
|
ctx.outputs.push(cout);
|
2022-11-02 22:02:39 -07:00
|
|
|
let mut cb = CompactBlock {
|
|
|
|
hash: {
|
|
|
|
let mut hash = vec![0; 32];
|
|
|
|
rng.fill_bytes(&mut hash);
|
|
|
|
hash
|
|
|
|
},
|
|
|
|
height: height.into(),
|
|
|
|
..Default::default()
|
|
|
|
};
|
2022-11-01 17:42:41 -07:00
|
|
|
cb.prev_hash.extend_from_slice(&prev_hash.0);
|
2019-03-08 19:12:31 -08:00
|
|
|
cb.vtx.push(ctx);
|
2023-07-01 17:16:23 -07:00
|
|
|
cb.block_metadata = Some(compact::BlockMetadata {
|
2023-06-29 13:28:12 -07:00
|
|
|
sapling_commitment_tree_size: initial_sapling_tree_size
|
|
|
|
+ cb.vtx.iter().map(|tx| tx.outputs.len() as u32).sum::<u32>(),
|
|
|
|
..Default::default()
|
|
|
|
});
|
2022-07-25 19:12:45 -07:00
|
|
|
(cb, note.nf(&dfvk.fvk().vk.nk, 0))
|
2019-03-08 19:12:31 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Create a fake CompactBlock at the given height, spending a single note from the
|
|
|
|
/// given address.
|
|
|
|
pub(crate) fn fake_compact_block_spending(
|
2020-08-05 13:08:58 -07:00
|
|
|
height: BlockHeight,
|
2019-03-08 19:12:31 -08:00
|
|
|
prev_hash: BlockHash,
|
2021-01-11 17:13:40 -08:00
|
|
|
(nf, in_value): (Nullifier, Amount),
|
2022-06-13 19:27:55 -07:00
|
|
|
dfvk: &DiversifiableFullViewingKey,
|
2020-07-01 13:26:54 -07:00
|
|
|
to: PaymentAddress,
|
2019-03-08 19:12:31 -08:00
|
|
|
value: Amount,
|
2023-04-03 12:53:43 -07:00
|
|
|
initial_sapling_tree_size: u32,
|
2019-03-08 19:12:31 -08:00
|
|
|
) -> CompactBlock {
|
|
|
|
let mut rng = OsRng;
|
2020-08-05 13:27:40 -07:00
|
|
|
let rseed = generate_random_rseed(&network(), height, &mut rng);
|
2019-03-08 19:12:31 -08:00
|
|
|
|
|
|
|
// Create a fake CompactBlock containing the note
|
2022-11-02 22:02:39 -07:00
|
|
|
let cspend = CompactSaplingSpend { nf: nf.to_vec() };
|
2022-11-01 17:42:41 -07:00
|
|
|
let mut ctx = CompactTx::default();
|
2019-03-08 19:12:31 -08:00
|
|
|
let mut txid = vec![0; 32];
|
|
|
|
rng.fill_bytes(&mut txid);
|
2022-11-01 17:42:41 -07:00
|
|
|
ctx.hash = txid;
|
2019-03-08 19:12:31 -08:00
|
|
|
ctx.spends.push(cspend);
|
|
|
|
|
|
|
|
// Create a fake Note for the payment
|
|
|
|
ctx.outputs.push({
|
2023-01-09 15:25:49 -08:00
|
|
|
let note = Note::from_parts(to, NoteValue::from_raw(value.into()), rseed);
|
2021-03-17 17:22:21 -07:00
|
|
|
let encryptor = sapling_note_encryption::<_, Network>(
|
2022-06-13 19:27:55 -07:00
|
|
|
Some(dfvk.fvk().ovk),
|
2020-08-05 21:47:35 -07:00
|
|
|
note.clone(),
|
2021-03-17 00:47:11 -07:00
|
|
|
MemoBytes::empty(),
|
2020-08-05 21:47:35 -07:00
|
|
|
&mut rng,
|
|
|
|
);
|
2023-01-06 09:01:05 -08:00
|
|
|
let cmu = note.cmu().to_bytes().to_vec();
|
2023-01-10 05:43:09 -08:00
|
|
|
let ephemeral_key = SaplingDomain::<Network>::epk_bytes(encryptor.epk())
|
|
|
|
.0
|
|
|
|
.to_vec();
|
2019-03-08 19:12:31 -08:00
|
|
|
let enc_ciphertext = encryptor.encrypt_note_plaintext();
|
|
|
|
|
2022-11-01 17:42:41 -07:00
|
|
|
CompactSaplingOutput {
|
|
|
|
cmu,
|
|
|
|
ephemeral_key,
|
|
|
|
ciphertext: enc_ciphertext.as_ref()[..52].to_vec(),
|
|
|
|
}
|
2019-03-08 19:12:31 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
// Create a fake Note for the change
|
|
|
|
ctx.outputs.push({
|
2022-06-13 19:27:55 -07:00
|
|
|
let change_addr = dfvk.default_address().1;
|
2020-08-05 13:27:40 -07:00
|
|
|
let rseed = generate_random_rseed(&network(), height, &mut rng);
|
2023-01-09 15:25:49 -08:00
|
|
|
let note = Note::from_parts(
|
|
|
|
change_addr,
|
|
|
|
NoteValue::from_raw((in_value - value).unwrap().into()),
|
2020-07-30 09:44:23 -07:00
|
|
|
rseed,
|
2023-01-09 15:25:49 -08:00
|
|
|
);
|
2021-03-17 17:22:21 -07:00
|
|
|
let encryptor = sapling_note_encryption::<_, Network>(
|
2022-06-13 19:27:55 -07:00
|
|
|
Some(dfvk.fvk().ovk),
|
2019-03-08 19:12:31 -08:00
|
|
|
note.clone(),
|
2021-03-17 00:47:11 -07:00
|
|
|
MemoBytes::empty(),
|
2020-08-05 21:47:35 -07:00
|
|
|
&mut rng,
|
2019-03-08 19:12:31 -08:00
|
|
|
);
|
2023-01-06 09:01:05 -08:00
|
|
|
let cmu = note.cmu().to_bytes().to_vec();
|
2023-01-10 05:43:09 -08:00
|
|
|
let ephemeral_key = SaplingDomain::<Network>::epk_bytes(encryptor.epk())
|
|
|
|
.0
|
|
|
|
.to_vec();
|
2019-03-08 19:12:31 -08:00
|
|
|
let enc_ciphertext = encryptor.encrypt_note_plaintext();
|
|
|
|
|
2022-11-01 17:42:41 -07:00
|
|
|
CompactSaplingOutput {
|
|
|
|
cmu,
|
|
|
|
ephemeral_key,
|
|
|
|
ciphertext: enc_ciphertext.as_ref()[..52].to_vec(),
|
|
|
|
}
|
2019-03-08 19:12:31 -08:00
|
|
|
});
|
|
|
|
|
2022-11-02 22:02:39 -07:00
|
|
|
let mut cb = CompactBlock {
|
|
|
|
hash: {
|
|
|
|
let mut hash = vec![0; 32];
|
|
|
|
rng.fill_bytes(&mut hash);
|
|
|
|
hash
|
|
|
|
},
|
|
|
|
height: height.into(),
|
|
|
|
..Default::default()
|
|
|
|
};
|
2022-11-01 17:42:41 -07:00
|
|
|
cb.prev_hash.extend_from_slice(&prev_hash.0);
|
2019-03-08 19:12:31 -08:00
|
|
|
cb.vtx.push(ctx);
|
2023-07-01 17:16:23 -07:00
|
|
|
cb.block_metadata = Some(compact::BlockMetadata {
|
2023-06-29 13:28:12 -07:00
|
|
|
sapling_commitment_tree_size: initial_sapling_tree_size
|
|
|
|
+ cb.vtx.iter().map(|tx| tx.outputs.len() as u32).sum::<u32>(),
|
|
|
|
..Default::default()
|
|
|
|
});
|
2019-03-08 19:12:31 -08:00
|
|
|
cb
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Insert a fake CompactBlock into the cache DB.
|
2021-03-26 22:17:54 -07:00
|
|
|
pub(crate) fn insert_into_cache(db_cache: &BlockDb, cb: &CompactBlock) {
|
2022-11-01 17:42:41 -07:00
|
|
|
let cb_bytes = cb.encode_to_vec();
|
2020-08-05 18:14:45 -07:00
|
|
|
db_cache
|
|
|
|
.0
|
2019-03-08 19:12:31 -08:00
|
|
|
.prepare("INSERT INTO compactblocks (height, data) VALUES (?, ?)")
|
|
|
|
.unwrap()
|
2021-01-12 17:24:18 -08:00
|
|
|
.execute(params![u32::from(cb.height()), cb_bytes,])
|
2019-03-08 19:12:31 -08:00
|
|
|
.unwrap();
|
|
|
|
}
|
2022-08-29 14:32:18 -07:00
|
|
|
|
2023-01-25 07:29:22 -08:00
|
|
|
#[cfg(feature = "unstable")]
|
|
|
|
pub(crate) fn store_in_fsblockdb<P: AsRef<Path>>(
|
|
|
|
fsblockdb_root: P,
|
|
|
|
cb: &CompactBlock,
|
|
|
|
) -> BlockMeta {
|
|
|
|
use std::io::Write;
|
|
|
|
|
|
|
|
let meta = BlockMeta {
|
|
|
|
height: cb.height(),
|
|
|
|
block_hash: cb.hash(),
|
|
|
|
block_time: cb.time,
|
|
|
|
sapling_outputs_count: cb.vtx.iter().map(|tx| tx.outputs.len() as u32).sum(),
|
|
|
|
orchard_actions_count: cb.vtx.iter().map(|tx| tx.actions.len() as u32).sum(),
|
|
|
|
};
|
|
|
|
|
|
|
|
let blocks_dir = fsblockdb_root.as_ref().join("blocks");
|
|
|
|
let block_path = meta.block_file_path(&blocks_dir);
|
|
|
|
|
|
|
|
File::create(block_path)
|
|
|
|
.unwrap()
|
|
|
|
.write_all(&cb.encode_to_vec())
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
meta
|
|
|
|
}
|
|
|
|
|
2022-09-12 11:42:12 -07:00
|
|
|
#[test]
|
|
|
|
pub(crate) fn get_next_available_address() {
|
|
|
|
use tempfile::NamedTempFile;
|
|
|
|
|
|
|
|
let data_file = NamedTempFile::new().unwrap();
|
|
|
|
let mut db_data = WalletDb::for_path(data_file.path(), network()).unwrap();
|
|
|
|
|
|
|
|
let account = AccountId::from(0);
|
|
|
|
init_wallet_db(&mut db_data, None).unwrap();
|
2023-06-09 10:02:00 -07:00
|
|
|
init_test_accounts_table_ufvk(&mut db_data);
|
2022-09-12 11:42:12 -07:00
|
|
|
|
|
|
|
let current_addr = db_data.get_current_address(account).unwrap();
|
|
|
|
assert!(current_addr.is_some());
|
|
|
|
|
2023-06-09 10:02:00 -07:00
|
|
|
let addr2 = db_data.get_next_available_address(account).unwrap();
|
2022-09-12 11:42:12 -07:00
|
|
|
assert!(addr2.is_some());
|
|
|
|
assert_ne!(current_addr, addr2);
|
|
|
|
|
|
|
|
let addr2_cur = db_data.get_current_address(account).unwrap();
|
|
|
|
assert_eq!(addr2, addr2_cur);
|
|
|
|
}
|
|
|
|
|
2022-09-08 11:48:06 -07:00
|
|
|
#[cfg(feature = "transparent-inputs")]
|
|
|
|
#[test]
|
|
|
|
fn transparent_receivers() {
|
|
|
|
use secrecy::Secret;
|
|
|
|
use tempfile::NamedTempFile;
|
|
|
|
|
|
|
|
use crate::{chain::init::init_cache_database, wallet::init::init_wallet_db};
|
|
|
|
|
|
|
|
let cache_file = NamedTempFile::new().unwrap();
|
|
|
|
let db_cache = BlockDb::for_path(cache_file.path()).unwrap();
|
|
|
|
init_cache_database(&db_cache).unwrap();
|
|
|
|
|
|
|
|
let data_file = NamedTempFile::new().unwrap();
|
|
|
|
let mut db_data = WalletDb::for_path(data_file.path(), network()).unwrap();
|
|
|
|
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
|
|
|
|
|
|
|
|
// Add an account to the wallet.
|
2023-06-09 10:02:00 -07:00
|
|
|
let (ufvk, taddr) = init_test_accounts_table_ufvk(&mut db_data);
|
2022-09-08 11:48:06 -07:00
|
|
|
let taddr = taddr.unwrap();
|
|
|
|
|
|
|
|
let receivers = db_data.get_transparent_receivers(0.into()).unwrap();
|
|
|
|
|
|
|
|
// The receiver for the default UA should be in the set.
|
2022-10-25 09:54:12 -07:00
|
|
|
assert!(receivers.contains_key(ufvk.default_address().0.transparent().unwrap()));
|
2022-09-08 11:48:06 -07:00
|
|
|
|
|
|
|
// The default t-addr should be in the set.
|
2022-10-25 09:54:12 -07:00
|
|
|
assert!(receivers.contains_key(&taddr));
|
2022-09-08 11:48:06 -07:00
|
|
|
}
|
|
|
|
|
2023-01-25 07:29:22 -08:00
|
|
|
#[cfg(feature = "unstable")]
|
|
|
|
#[test]
|
|
|
|
pub(crate) fn fsblockdb_api() {
|
|
|
|
// Initialise a BlockMeta DB in a new directory.
|
|
|
|
let fsblockdb_root = tempfile::tempdir().unwrap();
|
|
|
|
let mut db_meta = FsBlockDb::for_path(&fsblockdb_root).unwrap();
|
|
|
|
init_blockmeta_db(&mut db_meta).unwrap();
|
|
|
|
|
|
|
|
// The BlockMeta DB starts off empty.
|
|
|
|
assert_eq!(db_meta.get_max_cached_height().unwrap(), None);
|
|
|
|
|
|
|
|
// Generate some fake CompactBlocks.
|
|
|
|
let seed = [0u8; 32];
|
|
|
|
let account = AccountId::from(0);
|
|
|
|
let extsk = sapling::spending_key(&seed, network().coin_type(), account);
|
|
|
|
let dfvk = extsk.to_diversifiable_full_viewing_key();
|
|
|
|
let (cb1, _) = fake_compact_block(
|
|
|
|
BlockHeight::from_u32(1),
|
|
|
|
BlockHash([1; 32]),
|
|
|
|
&dfvk,
|
|
|
|
AddressType::DefaultExternal,
|
|
|
|
Amount::from_u64(5).unwrap(),
|
2023-04-03 12:53:43 -07:00
|
|
|
0,
|
2023-01-25 07:29:22 -08:00
|
|
|
);
|
|
|
|
let (cb2, _) = fake_compact_block(
|
|
|
|
BlockHeight::from_u32(2),
|
|
|
|
BlockHash([2; 32]),
|
|
|
|
&dfvk,
|
|
|
|
AddressType::DefaultExternal,
|
|
|
|
Amount::from_u64(10).unwrap(),
|
2023-04-03 12:53:43 -07:00
|
|
|
1,
|
2023-01-25 07:29:22 -08:00
|
|
|
);
|
|
|
|
|
|
|
|
// Write the CompactBlocks to the BlockMeta DB's corresponding disk storage.
|
|
|
|
let meta1 = store_in_fsblockdb(&fsblockdb_root, &cb1);
|
|
|
|
let meta2 = store_in_fsblockdb(&fsblockdb_root, &cb2);
|
|
|
|
|
|
|
|
// The BlockMeta DB is not updated until we do so explicitly.
|
|
|
|
assert_eq!(db_meta.get_max_cached_height().unwrap(), None);
|
|
|
|
|
|
|
|
// Inform the BlockMeta DB about the newly-persisted CompactBlocks.
|
|
|
|
db_meta.write_block_metadata(&[meta1, meta2]).unwrap();
|
|
|
|
|
|
|
|
// The BlockMeta DB now sees blocks up to height 2.
|
|
|
|
assert_eq!(
|
|
|
|
db_meta.get_max_cached_height().unwrap(),
|
|
|
|
Some(BlockHeight::from_u32(2)),
|
|
|
|
);
|
|
|
|
assert_eq!(
|
|
|
|
db_meta.find_block(BlockHeight::from_u32(1)).unwrap(),
|
|
|
|
Some(meta1),
|
|
|
|
);
|
|
|
|
assert_eq!(
|
|
|
|
db_meta.find_block(BlockHeight::from_u32(2)).unwrap(),
|
|
|
|
Some(meta2),
|
|
|
|
);
|
|
|
|
assert_eq!(db_meta.find_block(BlockHeight::from_u32(3)).unwrap(), None);
|
|
|
|
|
|
|
|
// Rewinding to height 1 should cause the metadata for height 2 to be deleted.
|
2023-04-26 15:27:56 -07:00
|
|
|
db_meta
|
|
|
|
.truncate_to_height(BlockHeight::from_u32(1))
|
|
|
|
.unwrap();
|
2023-01-25 07:29:22 -08:00
|
|
|
assert_eq!(
|
|
|
|
db_meta.get_max_cached_height().unwrap(),
|
|
|
|
Some(BlockHeight::from_u32(1)),
|
|
|
|
);
|
|
|
|
assert_eq!(
|
|
|
|
db_meta.find_block(BlockHeight::from_u32(1)).unwrap(),
|
|
|
|
Some(meta1),
|
|
|
|
);
|
|
|
|
assert_eq!(db_meta.find_block(BlockHeight::from_u32(2)).unwrap(), None);
|
|
|
|
assert_eq!(db_meta.find_block(BlockHeight::from_u32(3)).unwrap(), None);
|
|
|
|
}
|
2019-03-08 19:12:31 -08:00
|
|
|
}
|