Merge pull request #246 from str4d/zcash_client_sqlite
zcash_client_sqlite crate
This commit is contained in:
commit
d380a8c8d2
|
@ -11,7 +11,7 @@ jobs:
|
|||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: 1.39.0
|
||||
toolchain: 1.40.0
|
||||
override: true
|
||||
|
||||
# cargo fmt does not build the code, and running it in a fresh clone of
|
||||
|
@ -48,8 +48,23 @@ jobs:
|
|||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: 1.39.0
|
||||
toolchain: 1.40.0
|
||||
override: true
|
||||
|
||||
- name: Fetch path to Zcash parameters
|
||||
working-directory: ./zcash_proofs
|
||||
run: echo "::set-env name=ZCASH_PARAMS::$(cargo run --release --example get-params-path --features directories)"
|
||||
- name: Cache Zcash parameters
|
||||
id: cache-params
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ env.ZCASH_PARAMS }}
|
||||
key: ${{ runner.os }}-params
|
||||
- name: Fetch Zcash parameters
|
||||
if: steps.cache-params.outputs.cache-hit != 'true'
|
||||
working-directory: ./zcash_proofs
|
||||
run: cargo run --release --example download-params --features download-params
|
||||
|
||||
- name: cargo fetch
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
|
|
19
.travis.yml
19
.travis.yml
|
@ -1,19 +0,0 @@
|
|||
language: rust
|
||||
rust:
|
||||
- 1.39.0
|
||||
|
||||
cache: cargo
|
||||
|
||||
before_script:
|
||||
- rustup component add rustfmt
|
||||
|
||||
script:
|
||||
- cargo build --verbose --release --all
|
||||
- cargo fmt --all -- --check
|
||||
- cargo test --verbose --release --all
|
||||
- cargo test --verbose --release --all -- --ignored
|
||||
|
||||
before_cache:
|
||||
- rm -rf "$TRAVIS_HOME/.cargo/registry/src"
|
||||
- cargo install cargo-update || echo "cargo-update already installed"
|
||||
- cargo install-update -a # update outdated cached binaries
|
|
@ -5,6 +5,7 @@ members = [
|
|||
"group",
|
||||
"pairing",
|
||||
"zcash_client_backend",
|
||||
"zcash_client_sqlite",
|
||||
"zcash_history",
|
||||
"zcash_primitives",
|
||||
"zcash_proofs",
|
||||
|
|
|
@ -1 +1 @@
|
|||
1.39.0
|
||||
1.40.0
|
||||
|
|
|
@ -15,6 +15,8 @@ pub struct DecryptedOutput {
|
|||
pub index: usize,
|
||||
/// The note within the output.
|
||||
pub note: Note<Bls12>,
|
||||
/// The account that decrypted the note.
|
||||
pub account: usize,
|
||||
/// The address the note was sent to.
|
||||
pub to: PaymentAddress<Bls12>,
|
||||
/// The memo included with the note.
|
||||
|
@ -46,7 +48,7 @@ pub fn decrypt_transaction(
|
|||
None => continue,
|
||||
};
|
||||
|
||||
for (ivk, ovk) in &vks {
|
||||
for (account, (ivk, ovk)) in vks.iter().enumerate() {
|
||||
let ((note, to, memo), outgoing) =
|
||||
match try_sapling_note_decryption(ivk, &epk, &output.cmu, &output.enc_ciphertext) {
|
||||
Some(ret) => (ret, false),
|
||||
|
@ -65,6 +67,7 @@ pub fn decrypt_transaction(
|
|||
decrypted.push(DecryptedOutput {
|
||||
index,
|
||||
note,
|
||||
account,
|
||||
to,
|
||||
memo,
|
||||
outgoing,
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
[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]
|
||||
bech32 = "0.7"
|
||||
bs58 = { version = "0.3", features = ["check"] }
|
||||
ff = { version = "0.6", path = "../ff" }
|
||||
pairing = { version = "0.16", path = "../pairing" }
|
||||
protobuf = "2"
|
||||
rand_core = "0.5.1"
|
||||
rusqlite = { version = "0.23", features = ["bundled"] }
|
||||
time = "0.1"
|
||||
zcash_client_backend = { version = "0.2", path = "../zcash_client_backend" }
|
||||
zcash_primitives = { version = "0.2", path = "../zcash_primitives" }
|
||||
|
||||
[dev-dependencies]
|
||||
rand_core = "0.5.1"
|
||||
tempfile = "3"
|
||||
zcash_proofs = { version = "0.2", path = "../zcash_proofs" }
|
||||
|
||||
[features]
|
||||
mainnet = []
|
|
@ -0,0 +1,60 @@
|
|||
# Security Disclaimer
|
||||
|
||||
#### :warning: WARNING: This is an *early preview*
|
||||
|
||||
----
|
||||
|
||||
In the spirit of transparency, we provide this as a window into what we are actively
|
||||
developing. This is an alpha build, not yet intended for 3rd party use. Please be advised
|
||||
of the following:
|
||||
|
||||
* 🛑 This code currently is not audited. 🛑
|
||||
* ❌ This is a public, active branch with **no support**.
|
||||
* ❌ The code **does not have** documentation that is reviewed and approved by our Documentation team.
|
||||
* ❌ The code **does not have** adequate unit tests, acceptance tests and stress tests.
|
||||
* ❌ The code **does not have** automated tests that use the officially supported CI system.
|
||||
* ❌ The code **has not been subjected to thorough review** by engineers at the Electric Coin Company.
|
||||
* :warning: This library **is** compatible with the latest version of zcashd, but there **is no** automated testing of this.
|
||||
* :heavy_check_mark: The library **is not** majorly broken in some way.
|
||||
* :heavy_check_mark: The library **does run** on mainnet and testnet.
|
||||
* ❌ We **are actively rebasing** this branch and adding features where/when needed.
|
||||
* ❌ We **do not** undertake appropriate security coverage (threat models, review, response, etc.).
|
||||
* :heavy_check_mark: There is a product manager for this library.
|
||||
* :heavy_check_mark: Electric Coin Company maintains the library as we discover bugs and do network upgrades/minor releases.
|
||||
* :heavy_check_mark: Users can expect to get a response within a few weeks after submitting an issue.
|
||||
* ❌ The User Support team **has not yet been briefed** on the features provided to users and the functionality of the associated test-framework.
|
||||
* ❌ The code is **not fully-documented**.
|
||||
|
||||
|
||||
### 🛑 Use of this code may lead to a loss of funds 🛑
|
||||
|
||||
Use of this code in its current form or with modifications may lead to loss of funds, loss
|
||||
of "expected" privacy, or denial of service for a large portion of users, or a bug which
|
||||
could leverage any of those kinds of attacks (especially a "0 day" where we suspect few
|
||||
people know about the vulnerability).
|
||||
|
||||
### :eyes: At this time, this is for preview purposes only. :eyes:
|
||||
|
||||
----
|
||||
|
||||
# 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.
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
//! Structs for handling supported address types.
|
||||
|
||||
use pairing::bls12_381::Bls12;
|
||||
use zcash_client_backend::encoding::{
|
||||
decode_payment_address, decode_transparent_address, encode_payment_address,
|
||||
encode_transparent_address,
|
||||
};
|
||||
use zcash_primitives::{legacy::TransparentAddress, primitives::PaymentAddress};
|
||||
|
||||
#[cfg(feature = "mainnet")]
|
||||
use zcash_client_backend::constants::mainnet::{
|
||||
B58_PUBKEY_ADDRESS_PREFIX, B58_SCRIPT_ADDRESS_PREFIX, HRP_SAPLING_PAYMENT_ADDRESS,
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "mainnet"))]
|
||||
use zcash_client_backend::constants::testnet::{
|
||||
B58_PUBKEY_ADDRESS_PREFIX, B58_SCRIPT_ADDRESS_PREFIX, HRP_SAPLING_PAYMENT_ADDRESS,
|
||||
};
|
||||
|
||||
/// An address that funds can be sent to.
|
||||
pub enum RecipientAddress {
|
||||
Shielded(PaymentAddress<Bls12>),
|
||||
Transparent(TransparentAddress),
|
||||
}
|
||||
|
||||
impl From<PaymentAddress<Bls12>> for RecipientAddress {
|
||||
fn from(addr: PaymentAddress<Bls12>) -> Self {
|
||||
RecipientAddress::Shielded(addr)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TransparentAddress> for RecipientAddress {
|
||||
fn from(addr: TransparentAddress) -> Self {
|
||||
RecipientAddress::Transparent(addr)
|
||||
}
|
||||
}
|
||||
|
||||
impl RecipientAddress {
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
if let Ok(Some(pa)) = decode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS, s) {
|
||||
Some(pa.into())
|
||||
} else if let Ok(Some(addr)) =
|
||||
decode_transparent_address(&B58_PUBKEY_ADDRESS_PREFIX, &B58_SCRIPT_ADDRESS_PREFIX, s)
|
||||
{
|
||||
Some(addr.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> String {
|
||||
match self {
|
||||
RecipientAddress::Shielded(pa) => {
|
||||
encode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS, pa)
|
||||
}
|
||||
RecipientAddress::Transparent(addr) => encode_transparent_address(
|
||||
&B58_PUBKEY_ADDRESS_PREFIX,
|
||||
&B58_SCRIPT_ADDRESS_PREFIX,
|
||||
addr,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,480 @@
|
|||
//! Functions for enforcing chain validity and handling chain reorgs.
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```
|
||||
//! use zcash_client_sqlite::{
|
||||
//! chain::{rewind_to_height, validate_combined_chain},
|
||||
//! error::ErrorKind,
|
||||
//! scan::scan_cached_blocks,
|
||||
//! };
|
||||
//!
|
||||
//! let db_cache = "/path/to/cache.db";
|
||||
//! let db_data = "/path/to/data.db";
|
||||
//!
|
||||
//! // 1) Download new CompactBlocks into db_cache.
|
||||
//!
|
||||
//! // 2) Run the chain validator on the received blocks.
|
||||
//! //
|
||||
//! // Given that we assume the server always gives us correct-at-the-time blocks, any
|
||||
//! // errors are in the blocks we have previously cached or scanned.
|
||||
//! if let Err(e) = validate_combined_chain(&db_cache, &db_data) {
|
||||
//! match e.kind() {
|
||||
//! ErrorKind::InvalidChain(upper_bound, _) => {
|
||||
//! // a) Pick a height to rewind to.
|
||||
//! //
|
||||
//! // This might be informed by some external chain reorg information, or
|
||||
//! // heuristics such as the platform, available bandwidth, size of recent
|
||||
//! // CompactBlocks, etc.
|
||||
//! let rewind_height = upper_bound - 10;
|
||||
//!
|
||||
//! // b) Rewind scanned block information.
|
||||
//! rewind_to_height(&db_data, rewind_height);
|
||||
//!
|
||||
//! // c) Delete cached blocks from rewind_height onwards.
|
||||
//! //
|
||||
//! // This does imply that assumed-valid blocks will be re-downloaded, but it
|
||||
//! // is also possible that in the intervening time, a chain reorg has
|
||||
//! // occurred that orphaned some of those blocks.
|
||||
//!
|
||||
//! // d) If there is some separate thread or service downloading
|
||||
//! // CompactBlocks, tell it to go back and download from rewind_height
|
||||
//! // onwards.
|
||||
//! }
|
||||
//! _ => {
|
||||
//! // Handle other errors.
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! // 3) Scan (any remaining) cached blocks.
|
||||
//! //
|
||||
//! // At this point, the cache and scanned data are locally consistent (though not
|
||||
//! // necessarily consistent with the latest chain tip - this would be discovered the
|
||||
//! // next time this codepath is executed after new blocks are received).
|
||||
//! scan_cached_blocks(&db_cache, &db_data, None);
|
||||
//! ```
|
||||
|
||||
use protobuf::parse_from_bytes;
|
||||
use rusqlite::{Connection, NO_PARAMS};
|
||||
use std::path::Path;
|
||||
use zcash_client_backend::proto::compact_formats::CompactBlock;
|
||||
|
||||
use crate::{
|
||||
error::{Error, ErrorKind},
|
||||
SAPLING_ACTIVATION_HEIGHT,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ChainInvalidCause {
|
||||
PrevHashMismatch,
|
||||
/// (expected_height, actual_height)
|
||||
HeightMismatch(i32, i32),
|
||||
}
|
||||
|
||||
struct CompactBlockRow {
|
||||
height: i32,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
/// 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<P: AsRef<Path>, Q: AsRef<Path>>(
|
||||
db_cache: P,
|
||||
db_data: Q,
|
||||
) -> Result<(), Error> {
|
||||
let cache = Connection::open(db_cache)?;
|
||||
let data = Connection::open(db_data)?;
|
||||
|
||||
// Recall where we synced up to previously.
|
||||
// If we have never synced, use Sapling activation height to select all cached CompactBlocks.
|
||||
let (have_scanned, last_scanned_height) =
|
||||
data.query_row("SELECT MAX(height) FROM blocks", NO_PARAMS, |row| {
|
||||
row.get(0)
|
||||
.map(|h| (true, h))
|
||||
.or(Ok((false, SAPLING_ACTIVATION_HEIGHT - 1)))
|
||||
})?;
|
||||
|
||||
// Fetch the CompactBlocks we need to validate
|
||||
let mut stmt_blocks = cache
|
||||
.prepare("SELECT height, data FROM compactblocks WHERE height > ? ORDER BY height DESC")?;
|
||||
let mut rows = stmt_blocks.query_map(&[last_scanned_height], |row| {
|
||||
Ok(CompactBlockRow {
|
||||
height: row.get(0)?,
|
||||
data: row.get(1)?,
|
||||
})
|
||||
})?;
|
||||
|
||||
// Take the highest cached block as accurate.
|
||||
let (mut last_height, mut last_prev_hash) = {
|
||||
let assumed_correct = match rows.next() {
|
||||
Some(row) => row?,
|
||||
None => {
|
||||
// No cached blocks, and we've already validated the blocks we've scanned,
|
||||
// so there's nothing to validate.
|
||||
// TODO: Maybe we still want to check if there are cached blocks that are
|
||||
// at heights we previously scanned? Check scanning flow again.
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let block: CompactBlock = parse_from_bytes(&assumed_correct.data)?;
|
||||
(block.height as i32, block.prev_hash())
|
||||
};
|
||||
|
||||
for row in rows {
|
||||
let row = row?;
|
||||
|
||||
// Scanned blocks MUST be height-sequential.
|
||||
if row.height != (last_height - 1) {
|
||||
return Err(Error(ErrorKind::InvalidChain(
|
||||
last_height - 1,
|
||||
ChainInvalidCause::HeightMismatch(last_height - 1, row.height),
|
||||
)));
|
||||
}
|
||||
last_height = row.height;
|
||||
|
||||
let block: CompactBlock = parse_from_bytes(&row.data)?;
|
||||
|
||||
// Cached blocks MUST be hash-chained.
|
||||
if block.hash() != last_prev_hash {
|
||||
return Err(Error(ErrorKind::InvalidChain(
|
||||
last_height,
|
||||
ChainInvalidCause::PrevHashMismatch,
|
||||
)));
|
||||
}
|
||||
last_prev_hash = block.prev_hash();
|
||||
}
|
||||
|
||||
if have_scanned {
|
||||
// Cached blocks MUST hash-chain to the last scanned block.
|
||||
let last_scanned_hash = data.query_row(
|
||||
"SELECT hash FROM blocks WHERE height = ?",
|
||||
&[last_scanned_height],
|
||||
|row| row.get::<_, Vec<_>>(0),
|
||||
)?;
|
||||
if &last_scanned_hash[..] != &last_prev_hash.0[..] {
|
||||
return Err(Error(ErrorKind::InvalidChain(
|
||||
last_scanned_height,
|
||||
ChainInvalidCause::PrevHashMismatch,
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// All good!
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Rewinds the data database to the given height.
|
||||
///
|
||||
/// If the requested height is greater than or equal to the height of the last scanned
|
||||
/// block, this function does nothing.
|
||||
pub fn rewind_to_height<P: AsRef<Path>>(db_data: P, height: i32) -> Result<(), Error> {
|
||||
let data = Connection::open(db_data)?;
|
||||
|
||||
// Recall where we synced up to previously.
|
||||
// If we have never synced, use Sapling activation height.
|
||||
let last_scanned_height =
|
||||
data.query_row("SELECT MAX(height) FROM blocks", NO_PARAMS, |row| {
|
||||
row.get(0).or(Ok(SAPLING_ACTIVATION_HEIGHT - 1))
|
||||
})?;
|
||||
|
||||
if height >= last_scanned_height {
|
||||
// Nothing to do.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Start an SQL transaction for rewinding.
|
||||
data.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
|
||||
|
||||
// Decrement witnesses.
|
||||
data.execute("DELETE FROM sapling_witnesses WHERE block > ?", &[height])?;
|
||||
|
||||
// Un-mine transactions.
|
||||
data.execute(
|
||||
"UPDATE transactions SET block = NULL, tx_index = NULL WHERE block > ?",
|
||||
&[height],
|
||||
)?;
|
||||
|
||||
// Now that they aren't depended on, delete scanned blocks.
|
||||
data.execute("DELETE FROM blocks WHERE height > ?", &[height])?;
|
||||
|
||||
// Commit the SQL transaction, rewinding atomically.
|
||||
data.execute("COMMIT", NO_PARAMS)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tempfile::NamedTempFile;
|
||||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
transaction::components::Amount,
|
||||
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
|
||||
};
|
||||
|
||||
use super::{rewind_to_height, validate_combined_chain};
|
||||
use crate::{
|
||||
error::ErrorKind,
|
||||
init::{init_accounts_table, init_cache_database, init_data_database},
|
||||
query::get_balance,
|
||||
scan::scan_cached_blocks,
|
||||
tests::{fake_compact_block, insert_into_cache},
|
||||
SAPLING_ACTIVATION_HEIGHT,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn valid_chain_states() {
|
||||
let cache_file = NamedTempFile::new().unwrap();
|
||||
let db_cache = cache_file.path();
|
||||
init_cache_database(&db_cache).unwrap();
|
||||
|
||||
let data_file = NamedTempFile::new().unwrap();
|
||||
let db_data = data_file.path();
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let extsk = ExtendedSpendingKey::master(&[]);
|
||||
let extfvk = ExtendedFullViewingKey::from(&extsk);
|
||||
init_accounts_table(&db_data, &[extfvk.clone()]).unwrap();
|
||||
|
||||
// Empty chain should be valid
|
||||
validate_combined_chain(db_cache, db_data).unwrap();
|
||||
|
||||
// Create a fake CompactBlock sending value to the address
|
||||
let (cb, _) = fake_compact_block(
|
||||
SAPLING_ACTIVATION_HEIGHT,
|
||||
BlockHash([0; 32]),
|
||||
extfvk.clone(),
|
||||
Amount::from_u64(5).unwrap(),
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
|
||||
// Cache-only chain should be valid
|
||||
validate_combined_chain(db_cache, db_data).unwrap();
|
||||
|
||||
// Scan the cache
|
||||
scan_cached_blocks(db_cache, db_data, None).unwrap();
|
||||
|
||||
// Data-only chain should be valid
|
||||
validate_combined_chain(db_cache, db_data).unwrap();
|
||||
|
||||
// Create a second fake CompactBlock sending more value to the address
|
||||
let (cb2, _) = fake_compact_block(
|
||||
SAPLING_ACTIVATION_HEIGHT + 1,
|
||||
cb.hash(),
|
||||
extfvk,
|
||||
Amount::from_u64(7).unwrap(),
|
||||
);
|
||||
insert_into_cache(db_cache, &cb2);
|
||||
|
||||
// Data+cache chain should be valid
|
||||
validate_combined_chain(db_cache, db_data).unwrap();
|
||||
|
||||
// Scan the cache again
|
||||
scan_cached_blocks(db_cache, db_data, None).unwrap();
|
||||
|
||||
// Data-only chain should be valid
|
||||
validate_combined_chain(db_cache, db_data).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_chain_cache_disconnected() {
|
||||
let cache_file = NamedTempFile::new().unwrap();
|
||||
let db_cache = cache_file.path();
|
||||
init_cache_database(&db_cache).unwrap();
|
||||
|
||||
let data_file = NamedTempFile::new().unwrap();
|
||||
let db_data = data_file.path();
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let extsk = ExtendedSpendingKey::master(&[]);
|
||||
let extfvk = ExtendedFullViewingKey::from(&extsk);
|
||||
init_accounts_table(&db_data, &[extfvk.clone()]).unwrap();
|
||||
|
||||
// Create some fake CompactBlocks
|
||||
let (cb, _) = fake_compact_block(
|
||||
SAPLING_ACTIVATION_HEIGHT,
|
||||
BlockHash([0; 32]),
|
||||
extfvk.clone(),
|
||||
Amount::from_u64(5).unwrap(),
|
||||
);
|
||||
let (cb2, _) = fake_compact_block(
|
||||
SAPLING_ACTIVATION_HEIGHT + 1,
|
||||
cb.hash(),
|
||||
extfvk.clone(),
|
||||
Amount::from_u64(7).unwrap(),
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
insert_into_cache(db_cache, &cb2);
|
||||
|
||||
// Scan the cache
|
||||
scan_cached_blocks(db_cache, db_data, None).unwrap();
|
||||
|
||||
// Data-only chain should be valid
|
||||
validate_combined_chain(db_cache, db_data).unwrap();
|
||||
|
||||
// Create more fake CompactBlocks that don't connect to the scanned ones
|
||||
let (cb3, _) = fake_compact_block(
|
||||
SAPLING_ACTIVATION_HEIGHT + 2,
|
||||
BlockHash([1; 32]),
|
||||
extfvk.clone(),
|
||||
Amount::from_u64(8).unwrap(),
|
||||
);
|
||||
let (cb4, _) = fake_compact_block(
|
||||
SAPLING_ACTIVATION_HEIGHT + 3,
|
||||
cb3.hash(),
|
||||
extfvk.clone(),
|
||||
Amount::from_u64(3).unwrap(),
|
||||
);
|
||||
insert_into_cache(db_cache, &cb3);
|
||||
insert_into_cache(db_cache, &cb4);
|
||||
|
||||
// Data+cache chain should be invalid at the data/cache boundary
|
||||
match validate_combined_chain(db_cache, db_data) {
|
||||
Err(e) => match e.kind() {
|
||||
ErrorKind::InvalidChain(upper_bound, _) => {
|
||||
assert_eq!(*upper_bound, SAPLING_ACTIVATION_HEIGHT + 1)
|
||||
}
|
||||
_ => panic!(),
|
||||
},
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_chain_cache_reorg() {
|
||||
let cache_file = NamedTempFile::new().unwrap();
|
||||
let db_cache = cache_file.path();
|
||||
init_cache_database(&db_cache).unwrap();
|
||||
|
||||
let data_file = NamedTempFile::new().unwrap();
|
||||
let db_data = data_file.path();
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let extsk = ExtendedSpendingKey::master(&[]);
|
||||
let extfvk = ExtendedFullViewingKey::from(&extsk);
|
||||
init_accounts_table(&db_data, &[extfvk.clone()]).unwrap();
|
||||
|
||||
// Create some fake CompactBlocks
|
||||
let (cb, _) = fake_compact_block(
|
||||
SAPLING_ACTIVATION_HEIGHT,
|
||||
BlockHash([0; 32]),
|
||||
extfvk.clone(),
|
||||
Amount::from_u64(5).unwrap(),
|
||||
);
|
||||
let (cb2, _) = fake_compact_block(
|
||||
SAPLING_ACTIVATION_HEIGHT + 1,
|
||||
cb.hash(),
|
||||
extfvk.clone(),
|
||||
Amount::from_u64(7).unwrap(),
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
insert_into_cache(db_cache, &cb2);
|
||||
|
||||
// Scan the cache
|
||||
scan_cached_blocks(db_cache, db_data, None).unwrap();
|
||||
|
||||
// Data-only chain should be valid
|
||||
validate_combined_chain(db_cache, db_data).unwrap();
|
||||
|
||||
// Create more fake CompactBlocks that contain a reorg
|
||||
let (cb3, _) = fake_compact_block(
|
||||
SAPLING_ACTIVATION_HEIGHT + 2,
|
||||
cb2.hash(),
|
||||
extfvk.clone(),
|
||||
Amount::from_u64(8).unwrap(),
|
||||
);
|
||||
let (cb4, _) = fake_compact_block(
|
||||
SAPLING_ACTIVATION_HEIGHT + 3,
|
||||
BlockHash([1; 32]),
|
||||
extfvk.clone(),
|
||||
Amount::from_u64(3).unwrap(),
|
||||
);
|
||||
insert_into_cache(db_cache, &cb3);
|
||||
insert_into_cache(db_cache, &cb4);
|
||||
|
||||
// Data+cache chain should be invalid inside the cache
|
||||
match validate_combined_chain(db_cache, db_data) {
|
||||
Err(e) => match e.kind() {
|
||||
ErrorKind::InvalidChain(upper_bound, _) => {
|
||||
assert_eq!(*upper_bound, SAPLING_ACTIVATION_HEIGHT + 2)
|
||||
}
|
||||
_ => panic!(),
|
||||
},
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_db_rewinding() {
|
||||
let cache_file = NamedTempFile::new().unwrap();
|
||||
let db_cache = cache_file.path();
|
||||
init_cache_database(&db_cache).unwrap();
|
||||
|
||||
let data_file = NamedTempFile::new().unwrap();
|
||||
let db_data = data_file.path();
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let extsk = ExtendedSpendingKey::master(&[]);
|
||||
let extfvk = ExtendedFullViewingKey::from(&extsk);
|
||||
init_accounts_table(&db_data, &[extfvk.clone()]).unwrap();
|
||||
|
||||
// Account balance should be zero
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), Amount::zero());
|
||||
|
||||
// Create fake CompactBlocks sending value to the address
|
||||
let value = Amount::from_u64(5).unwrap();
|
||||
let value2 = Amount::from_u64(7).unwrap();
|
||||
let (cb, _) = fake_compact_block(
|
||||
SAPLING_ACTIVATION_HEIGHT,
|
||||
BlockHash([0; 32]),
|
||||
extfvk.clone(),
|
||||
value,
|
||||
);
|
||||
let (cb2, _) = fake_compact_block(SAPLING_ACTIVATION_HEIGHT + 1, cb.hash(), extfvk, value2);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
insert_into_cache(db_cache, &cb2);
|
||||
|
||||
// Scan the cache
|
||||
scan_cached_blocks(db_cache, db_data, None).unwrap();
|
||||
|
||||
// Account balance should reflect both received notes
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), value + value2);
|
||||
|
||||
// "Rewind" to height of last scanned block
|
||||
rewind_to_height(db_data, SAPLING_ACTIVATION_HEIGHT + 1).unwrap();
|
||||
|
||||
// Account balance should be unaltered
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), value + value2);
|
||||
|
||||
// Rewind so that one block is dropped
|
||||
rewind_to_height(db_data, SAPLING_ACTIVATION_HEIGHT).unwrap();
|
||||
|
||||
// Account balance should only contain the first received note
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), value);
|
||||
|
||||
// Scan the cache again
|
||||
scan_cached_blocks(db_cache, db_data, None).unwrap();
|
||||
|
||||
// Account balance should again reflect both received notes
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), value + value2);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
use std::error;
|
||||
use std::fmt;
|
||||
use zcash_primitives::{
|
||||
sapling::Node,
|
||||
transaction::{builder, TxId},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ErrorKind {
|
||||
CorruptedData(&'static str),
|
||||
IncorrectHRPExtFVK,
|
||||
InsufficientBalance(u64, u64),
|
||||
InvalidChain(i32, crate::chain::ChainInvalidCause),
|
||||
InvalidExtSK(u32),
|
||||
InvalidHeight(i32, i32),
|
||||
InvalidMemo(std::str::Utf8Error),
|
||||
InvalidNewWitnessAnchor(usize, TxId, i32, Node),
|
||||
InvalidNote,
|
||||
InvalidWitnessAnchor(i64, i32),
|
||||
ScanRequired,
|
||||
TableNotEmpty,
|
||||
Bech32(bech32::Error),
|
||||
Base58(bs58::decode::Error),
|
||||
Builder(builder::Error),
|
||||
Database(rusqlite::Error),
|
||||
Io(std::io::Error),
|
||||
Protobuf(protobuf::ProtobufError),
|
||||
}
|
||||
|
||||
#[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::CorruptedData(reason) => write!(f, "Data DB is corrupted: {}", reason),
|
||||
ErrorKind::IncorrectHRPExtFVK => write!(f, "Incorrect HRP for extfvk"),
|
||||
ErrorKind::InsufficientBalance(have, need) => write!(
|
||||
f,
|
||||
"Insufficient balance (have {}, need {} including fee)",
|
||||
have, need
|
||||
),
|
||||
ErrorKind::InvalidChain(upper_bound, cause) => {
|
||||
write!(f, "Invalid chain (upper bound: {}): {:?}", upper_bound, cause)
|
||||
}
|
||||
ErrorKind::InvalidExtSK(account) => {
|
||||
write!(f, "Incorrect ExtendedSpendingKey for account {}", account)
|
||||
}
|
||||
ErrorKind::InvalidHeight(expected, actual) => write!(
|
||||
f,
|
||||
"Expected height of next CompactBlock to be {}, but was {}",
|
||||
expected, actual
|
||||
),
|
||||
ErrorKind::InvalidMemo(e) => write!(f, "{}", e),
|
||||
ErrorKind::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,
|
||||
),
|
||||
ErrorKind::InvalidNote => write!(f, "Invalid note"),
|
||||
ErrorKind::InvalidWitnessAnchor(id_note, last_height) => write!(
|
||||
f,
|
||||
"Witness for note {} has incorrect anchor after scanning block {}",
|
||||
id_note, last_height
|
||||
),
|
||||
ErrorKind::ScanRequired => write!(f, "Must scan blocks first"),
|
||||
ErrorKind::TableNotEmpty => write!(f, "Table is not empty"),
|
||||
ErrorKind::Bech32(e) => write!(f, "{}", e),
|
||||
ErrorKind::Base58(e) => write!(f, "{}", e),
|
||||
ErrorKind::Builder(e) => write!(f, "{:?}", e),
|
||||
ErrorKind::Database(e) => write!(f, "{}", e),
|
||||
ErrorKind::Io(e) => write!(f, "{}", e),
|
||||
ErrorKind::Protobuf(e) => write!(f, "{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for Error {
|
||||
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
||||
match &self.0 {
|
||||
ErrorKind::InvalidMemo(e) => Some(e),
|
||||
ErrorKind::Bech32(e) => Some(e),
|
||||
ErrorKind::Builder(e) => Some(e),
|
||||
ErrorKind::Database(e) => Some(e),
|
||||
ErrorKind::Io(e) => Some(e),
|
||||
ErrorKind::Protobuf(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bech32::Error> for Error {
|
||||
fn from(e: bech32::Error) -> Self {
|
||||
Error(ErrorKind::Bech32(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bs58::decode::Error> for Error {
|
||||
fn from(e: bs58::decode::Error) -> Self {
|
||||
Error(ErrorKind::Base58(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<builder::Error> for Error {
|
||||
fn from(e: builder::Error) -> Self {
|
||||
Error(ErrorKind::Builder(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<rusqlite::Error> for Error {
|
||||
fn from(e: rusqlite::Error) -> Self {
|
||||
Error(ErrorKind::Database(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
Error(ErrorKind::Io(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<protobuf::ProtobufError> for Error {
|
||||
fn from(e: protobuf::ProtobufError) -> Self {
|
||||
Error(ErrorKind::Protobuf(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn kind(&self) -> &ErrorKind {
|
||||
&self.0
|
||||
}
|
||||
}
|
|
@ -0,0 +1,303 @@
|
|||
//! Functions for initializing the various databases.
|
||||
|
||||
use rusqlite::{types::ToSql, Connection, NO_PARAMS};
|
||||
use std::path::Path;
|
||||
use zcash_client_backend::encoding::encode_extended_full_viewing_key;
|
||||
use zcash_primitives::{block::BlockHash, zip32::ExtendedFullViewingKey};
|
||||
|
||||
use crate::{
|
||||
address_from_extfvk,
|
||||
error::{Error, ErrorKind},
|
||||
HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY,
|
||||
};
|
||||
|
||||
/// 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 INTEGER 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_client_backend::encoding::decode_payment_address;
|
||||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
|
||||
};
|
||||
|
||||
use super::{init_accounts_table, init_blocks_table, init_data_database};
|
||||
use crate::{query::get_address, HRP_SAPLING_PAYMENT_ADDRESS};
|
||||
|
||||
#[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();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_accounts_table_stores_correct_address() {
|
||||
let data_file = NamedTempFile::new().unwrap();
|
||||
let db_data = data_file.path();
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let extsk = ExtendedSpendingKey::master(&[]);
|
||||
let extfvks = [ExtendedFullViewingKey::from(&extsk)];
|
||||
init_accounts_table(&db_data, &extfvks).unwrap();
|
||||
|
||||
// The account's address should be in the data DB
|
||||
let addr = get_address(&db_data, 0).unwrap();
|
||||
let pa = decode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS, &addr).unwrap();
|
||||
assert_eq!(pa.unwrap(), extsk.default_address().unwrap().1);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,258 @@
|
|||
//! *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.
|
||||
//!
|
||||
//! # 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.
|
||||
//!
|
||||
//! [`CompactBlock`]: zcash_client_backend::proto::compact_formats::CompactBlock
|
||||
//! [`init_cache_database`]: crate::init::init_cache_database
|
||||
|
||||
use rusqlite::{Connection, NO_PARAMS};
|
||||
use std::cmp;
|
||||
use zcash_client_backend::encoding::encode_payment_address;
|
||||
use zcash_primitives::zip32::ExtendedFullViewingKey;
|
||||
|
||||
#[cfg(feature = "mainnet")]
|
||||
use zcash_client_backend::constants::mainnet::{
|
||||
HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, HRP_SAPLING_PAYMENT_ADDRESS,
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "mainnet"))]
|
||||
use zcash_client_backend::constants::testnet::{
|
||||
HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, HRP_SAPLING_PAYMENT_ADDRESS,
|
||||
};
|
||||
|
||||
pub mod address;
|
||||
pub mod chain;
|
||||
pub mod error;
|
||||
pub mod init;
|
||||
pub mod query;
|
||||
pub mod scan;
|
||||
pub mod transact;
|
||||
|
||||
const ANCHOR_OFFSET: u32 = 10;
|
||||
|
||||
#[cfg(feature = "mainnet")]
|
||||
const SAPLING_ACTIVATION_HEIGHT: i32 = 419_200;
|
||||
|
||||
#[cfg(not(feature = "mainnet"))]
|
||||
const SAPLING_ACTIVATION_HEIGHT: i32 = 280_000;
|
||||
|
||||
fn address_from_extfvk(extfvk: &ExtendedFullViewingKey) -> String {
|
||||
let addr = extfvk.default_address().unwrap().1;
|
||||
encode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS, &addr)
|
||||
}
|
||||
|
||||
/// Determines the target height for a transaction, and the height from which to
|
||||
/// select anchors, based on the current synchronised block chain.
|
||||
fn get_target_and_anchor_heights(data: &Connection) -> Result<(u32, u32), error::Error> {
|
||||
data.query_row_and_then(
|
||||
"SELECT MIN(height), MAX(height) FROM blocks",
|
||||
NO_PARAMS,
|
||||
|row| match (row.get::<_, u32>(0), row.get::<_, u32>(1)) {
|
||||
// If there are no blocks, the query returns NULL.
|
||||
(Err(rusqlite::Error::InvalidColumnType(_, _, _)), _)
|
||||
| (_, Err(rusqlite::Error::InvalidColumnType(_, _, _))) => {
|
||||
Err(error::Error(error::ErrorKind::ScanRequired))
|
||||
}
|
||||
(Err(e), _) | (_, Err(e)) => Err(e.into()),
|
||||
(Ok(min_height), Ok(max_height)) => {
|
||||
let target_height = max_height + 1;
|
||||
|
||||
// Select an anchor ANCHOR_OFFSET back from the target block,
|
||||
// unless that would be before the earliest block we have.
|
||||
let anchor_height =
|
||||
cmp::max(target_height.saturating_sub(ANCHOR_OFFSET), min_height);
|
||||
|
||||
Ok((target_height, anchor_height))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ff::{Field, PrimeField};
|
||||
use pairing::bls12_381::Bls12;
|
||||
use protobuf::Message;
|
||||
use rand_core::{OsRng, RngCore};
|
||||
use rusqlite::{types::ToSql, Connection};
|
||||
use std::path::Path;
|
||||
use zcash_client_backend::proto::compact_formats::{
|
||||
CompactBlock, CompactOutput, CompactSpend, CompactTx,
|
||||
};
|
||||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
jubjub::fs::Fs,
|
||||
note_encryption::{Memo, SaplingNoteEncryption},
|
||||
primitives::{Note, PaymentAddress},
|
||||
transaction::components::Amount,
|
||||
zip32::ExtendedFullViewingKey,
|
||||
JUBJUB,
|
||||
};
|
||||
|
||||
/// Create a fake CompactBlock at the given height, containing a single output paying
|
||||
/// the given address. Returns the CompactBlock and the nullifier for the new note.
|
||||
pub(crate) fn fake_compact_block(
|
||||
height: i32,
|
||||
prev_hash: BlockHash,
|
||||
extfvk: ExtendedFullViewingKey,
|
||||
value: Amount,
|
||||
) -> (CompactBlock, Vec<u8>) {
|
||||
let to = extfvk.default_address().unwrap().1;
|
||||
|
||||
// Create a fake Note for the account
|
||||
let mut rng = OsRng;
|
||||
let note = Note {
|
||||
g_d: to.diversifier().g_d::<Bls12>(&JUBJUB).unwrap(),
|
||||
pk_d: to.pk_d().clone(),
|
||||
value: value.into(),
|
||||
r: Fs::random(&mut rng),
|
||||
};
|
||||
let encryptor = SaplingNoteEncryption::new(
|
||||
extfvk.fvk.ovk,
|
||||
note.clone(),
|
||||
to.clone(),
|
||||
Memo::default(),
|
||||
&mut rng,
|
||||
);
|
||||
let cmu = note.cm(&JUBJUB).to_repr().as_ref().to_vec();
|
||||
let mut epk = vec![];
|
||||
encryptor.epk().write(&mut epk).unwrap();
|
||||
let enc_ciphertext = encryptor.encrypt_note_plaintext();
|
||||
|
||||
// Create a fake CompactBlock containing the note
|
||||
let mut cout = CompactOutput::new();
|
||||
cout.set_cmu(cmu);
|
||||
cout.set_epk(epk);
|
||||
cout.set_ciphertext(enc_ciphertext[..52].to_vec());
|
||||
let mut ctx = CompactTx::new();
|
||||
let mut txid = vec![0; 32];
|
||||
rng.fill_bytes(&mut txid);
|
||||
ctx.set_hash(txid);
|
||||
ctx.outputs.push(cout);
|
||||
let mut cb = CompactBlock::new();
|
||||
cb.set_height(height as u64);
|
||||
cb.hash.resize(32, 0);
|
||||
rng.fill_bytes(&mut cb.hash);
|
||||
cb.prevHash.extend_from_slice(&prev_hash.0);
|
||||
cb.vtx.push(ctx);
|
||||
(cb, note.nf(&extfvk.fvk.vk, 0, &JUBJUB))
|
||||
}
|
||||
|
||||
/// Create a fake CompactBlock at the given height, spending a single note from the
|
||||
/// given address.
|
||||
pub(crate) fn fake_compact_block_spending(
|
||||
height: i32,
|
||||
prev_hash: BlockHash,
|
||||
(nf, in_value): (Vec<u8>, Amount),
|
||||
extfvk: ExtendedFullViewingKey,
|
||||
to: PaymentAddress<Bls12>,
|
||||
value: Amount,
|
||||
) -> CompactBlock {
|
||||
let mut rng = OsRng;
|
||||
|
||||
// Create a fake CompactBlock containing the note
|
||||
let mut cspend = CompactSpend::new();
|
||||
cspend.set_nf(nf);
|
||||
let mut ctx = CompactTx::new();
|
||||
let mut txid = vec![0; 32];
|
||||
rng.fill_bytes(&mut txid);
|
||||
ctx.set_hash(txid);
|
||||
ctx.spends.push(cspend);
|
||||
|
||||
// Create a fake Note for the payment
|
||||
ctx.outputs.push({
|
||||
let note = Note {
|
||||
g_d: to.diversifier().g_d::<Bls12>(&JUBJUB).unwrap(),
|
||||
pk_d: to.pk_d().clone(),
|
||||
value: value.into(),
|
||||
r: Fs::random(&mut rng),
|
||||
};
|
||||
let encryptor = SaplingNoteEncryption::new(
|
||||
extfvk.fvk.ovk,
|
||||
note.clone(),
|
||||
to,
|
||||
Memo::default(),
|
||||
&mut rng,
|
||||
);
|
||||
let cmu = note.cm(&JUBJUB).to_repr().as_ref().to_vec();
|
||||
let mut epk = vec![];
|
||||
encryptor.epk().write(&mut epk).unwrap();
|
||||
let enc_ciphertext = encryptor.encrypt_note_plaintext();
|
||||
|
||||
let mut cout = CompactOutput::new();
|
||||
cout.set_cmu(cmu);
|
||||
cout.set_epk(epk);
|
||||
cout.set_ciphertext(enc_ciphertext[..52].to_vec());
|
||||
cout
|
||||
});
|
||||
|
||||
// Create a fake Note for the change
|
||||
ctx.outputs.push({
|
||||
let change_addr = extfvk.default_address().unwrap().1;
|
||||
let note = Note {
|
||||
g_d: change_addr.diversifier().g_d::<Bls12>(&JUBJUB).unwrap(),
|
||||
pk_d: change_addr.pk_d().clone(),
|
||||
value: (in_value - value).into(),
|
||||
r: Fs::random(&mut rng),
|
||||
};
|
||||
let encryptor = SaplingNoteEncryption::new(
|
||||
extfvk.fvk.ovk,
|
||||
note.clone(),
|
||||
change_addr,
|
||||
Memo::default(),
|
||||
&mut rng,
|
||||
);
|
||||
let cmu = note.cm(&JUBJUB).to_repr().as_ref().to_vec();
|
||||
let mut epk = vec![];
|
||||
encryptor.epk().write(&mut epk).unwrap();
|
||||
let enc_ciphertext = encryptor.encrypt_note_plaintext();
|
||||
|
||||
let mut cout = CompactOutput::new();
|
||||
cout.set_cmu(cmu);
|
||||
cout.set_epk(epk);
|
||||
cout.set_ciphertext(enc_ciphertext[..52].to_vec());
|
||||
cout
|
||||
});
|
||||
|
||||
let mut cb = CompactBlock::new();
|
||||
cb.set_height(height as u64);
|
||||
cb.hash.resize(32, 0);
|
||||
rng.fill_bytes(&mut cb.hash);
|
||||
cb.prevHash.extend_from_slice(&prev_hash.0);
|
||||
cb.vtx.push(ctx);
|
||||
cb
|
||||
}
|
||||
|
||||
/// Insert a fake CompactBlock into the cache DB.
|
||||
pub(crate) fn insert_into_cache<P: AsRef<Path>>(db_cache: P, cb: &CompactBlock) {
|
||||
let cb_bytes = cb.write_to_bytes().unwrap();
|
||||
let cache = Connection::open(&db_cache).unwrap();
|
||||
cache
|
||||
.prepare("INSERT INTO compactblocks (height, data) VALUES (?, ?)")
|
||||
.unwrap()
|
||||
.execute(&[
|
||||
(cb.height as i32).to_sql().unwrap(),
|
||||
cb_bytes.to_sql().unwrap(),
|
||||
])
|
||||
.unwrap();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,206 @@
|
|||
//! Functions for querying information in the data database.
|
||||
|
||||
use rusqlite::Connection;
|
||||
use std::path::Path;
|
||||
use zcash_primitives::{note_encryption::Memo, transaction::components::Amount};
|
||||
|
||||
use crate::{
|
||||
error::{Error, ErrorKind},
|
||||
get_target_and_anchor_heights,
|
||||
};
|
||||
|
||||
/// Returns the address for the account.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use zcash_client_sqlite::query::get_address;
|
||||
///
|
||||
/// let addr = get_address("/path/to/data.db", 0);
|
||||
/// ```
|
||||
pub fn get_address<P: AsRef<Path>>(db_data: P, account: u32) -> Result<String, Error> {
|
||||
let data = Connection::open(db_data)?;
|
||||
|
||||
let addr = data.query_row(
|
||||
"SELECT address FROM accounts
|
||||
WHERE account = ?",
|
||||
&[account],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
Ok(addr)
|
||||
}
|
||||
|
||||
/// Returns the balance for the account, including all mined unspent notes that we know
|
||||
/// about.
|
||||
///
|
||||
/// WARNING: This balance is potentially unreliable, as mined notes may become unmined due
|
||||
/// to chain reorgs. You should generally not show this balance to users without some
|
||||
/// caveat. Use [`get_verified_balance`] where you need a more reliable indication of the
|
||||
/// wallet balance.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use zcash_client_sqlite::query::get_balance;
|
||||
///
|
||||
/// let addr = get_balance("/path/to/data.db", 0);
|
||||
/// ```
|
||||
pub fn get_balance<P: AsRef<Path>>(db_data: P, account: u32) -> Result<Amount, Error> {
|
||||
let data = Connection::open(db_data)?;
|
||||
|
||||
let balance = data.query_row(
|
||||
"SELECT SUM(value) FROM received_notes
|
||||
INNER JOIN transactions ON transactions.id_tx = received_notes.tx
|
||||
WHERE account = ? AND spent IS NULL AND transactions.block IS NOT NULL",
|
||||
&[account],
|
||||
|row| row.get(0).or(Ok(0)),
|
||||
)?;
|
||||
|
||||
match Amount::from_i64(balance) {
|
||||
Ok(amount) if !amount.is_negative() => Ok(amount),
|
||||
_ => Err(Error(ErrorKind::CorruptedData(
|
||||
"Sum of values in received_notes is out of range",
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the verified balance for the account, which ignores notes that have been
|
||||
/// received too recently and are not yet deemed spendable.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use zcash_client_sqlite::query::get_verified_balance;
|
||||
///
|
||||
/// let addr = get_verified_balance("/path/to/data.db", 0);
|
||||
/// ```
|
||||
pub fn get_verified_balance<P: AsRef<Path>>(db_data: P, account: u32) -> Result<Amount, Error> {
|
||||
let data = Connection::open(db_data)?;
|
||||
|
||||
let (_, anchor_height) = get_target_and_anchor_heights(&data)?;
|
||||
|
||||
let balance = data.query_row(
|
||||
"SELECT SUM(value) FROM received_notes
|
||||
INNER JOIN transactions ON transactions.id_tx = received_notes.tx
|
||||
WHERE account = ? AND spent IS NULL AND transactions.block <= ?",
|
||||
&[account, anchor_height],
|
||||
|row| row.get(0).or(Ok(0)),
|
||||
)?;
|
||||
|
||||
match Amount::from_i64(balance) {
|
||||
Ok(amount) if !amount.is_negative() => Ok(amount),
|
||||
_ => Err(Error(ErrorKind::CorruptedData(
|
||||
"Sum of values in received_notes is out of range",
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the memo for a received note, if it is known and a valid UTF-8 string.
|
||||
///
|
||||
/// The note is identified by its row index in the `received_notes` table within the data
|
||||
/// database.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use zcash_client_sqlite::query::get_received_memo_as_utf8;
|
||||
///
|
||||
/// let memo = get_received_memo_as_utf8("/path/to/data.db", 27);
|
||||
pub fn get_received_memo_as_utf8<P: AsRef<Path>>(
|
||||
db_data: P,
|
||||
id_note: i64,
|
||||
) -> Result<Option<String>, Error> {
|
||||
let data = Connection::open(db_data)?;
|
||||
|
||||
let memo: Vec<_> = data.query_row(
|
||||
"SELECT memo FROM received_notes
|
||||
WHERE id_note = ?",
|
||||
&[id_note],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
match Memo::from_bytes(&memo) {
|
||||
Some(memo) => match memo.to_utf8() {
|
||||
Some(Ok(res)) => Ok(Some(res)),
|
||||
Some(Err(e)) => Err(Error(ErrorKind::InvalidMemo(e))),
|
||||
None => Ok(None),
|
||||
},
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the memo for a sent note, if it is known and a valid UTF-8 string.
|
||||
///
|
||||
/// The note is identified by its row index in the `sent_notes` table within the data
|
||||
/// database.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use zcash_client_sqlite::query::get_sent_memo_as_utf8;
|
||||
///
|
||||
/// let memo = get_sent_memo_as_utf8("/path/to/data.db", 12);
|
||||
pub fn get_sent_memo_as_utf8<P: AsRef<Path>>(
|
||||
db_data: P,
|
||||
id_note: i64,
|
||||
) -> Result<Option<String>, Error> {
|
||||
let data = Connection::open(db_data)?;
|
||||
|
||||
let memo: Vec<_> = data.query_row(
|
||||
"SELECT memo FROM sent_notes
|
||||
WHERE id_note = ?",
|
||||
&[id_note],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
match Memo::from_bytes(&memo) {
|
||||
Some(memo) => match memo.to_utf8() {
|
||||
Some(Ok(res)) => Ok(Some(res)),
|
||||
Some(Err(e)) => Err(Error(ErrorKind::InvalidMemo(e))),
|
||||
None => Ok(None),
|
||||
},
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tempfile::NamedTempFile;
|
||||
use zcash_primitives::{
|
||||
transaction::components::Amount,
|
||||
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
|
||||
};
|
||||
|
||||
use super::{get_address, get_balance, get_verified_balance};
|
||||
use crate::{
|
||||
error::ErrorKind,
|
||||
init::{init_accounts_table, init_data_database},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn empty_database_has_no_balance() {
|
||||
let data_file = NamedTempFile::new().unwrap();
|
||||
let db_data = data_file.path();
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let extsk = ExtendedSpendingKey::master(&[]);
|
||||
let extfvks = [ExtendedFullViewingKey::from(&extsk)];
|
||||
init_accounts_table(&db_data, &extfvks).unwrap();
|
||||
|
||||
// The account should be empty
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), Amount::zero());
|
||||
|
||||
// The account should have no verified balance, as we haven't scanned any blocks
|
||||
let e = get_verified_balance(db_data, 0).unwrap_err();
|
||||
match e.kind() {
|
||||
ErrorKind::ScanRequired => (),
|
||||
_ => panic!("Unexpected error: {:?}", e),
|
||||
}
|
||||
|
||||
// An invalid account has zero balance
|
||||
assert!(get_address(db_data, 1).is_err());
|
||||
assert_eq!(get_balance(db_data, 1).unwrap(), Amount::zero());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,673 @@
|
|||
//! Functions for scanning the chain and extracting relevant information.
|
||||
|
||||
use ff::PrimeField;
|
||||
use protobuf::parse_from_bytes;
|
||||
use rusqlite::{types::ToSql, Connection, NO_PARAMS};
|
||||
use std::path::Path;
|
||||
use zcash_client_backend::{
|
||||
decrypt_transaction, encoding::decode_extended_full_viewing_key,
|
||||
proto::compact_formats::CompactBlock, welding_rig::scan_block,
|
||||
};
|
||||
use zcash_primitives::{
|
||||
merkle_tree::{CommitmentTree, IncrementalWitness},
|
||||
sapling::Node,
|
||||
transaction::Transaction,
|
||||
JUBJUB,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
address::RecipientAddress,
|
||||
error::{Error, ErrorKind},
|
||||
HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, SAPLING_ACTIVATION_HEIGHT,
|
||||
};
|
||||
|
||||
struct CompactBlockRow {
|
||||
height: i32,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct WitnessRow {
|
||||
id_note: i64,
|
||||
witness: IncrementalWitness<Node>,
|
||||
}
|
||||
|
||||
/// Scans at most `limit` new blocks added to the cache for any transactions received by
|
||||
/// the tracked accounts.
|
||||
///
|
||||
/// This function will return without error after scanning at most `limit` new blocks, to
|
||||
/// enable the caller to update their UI with scanning progress. Repeatedly calling this
|
||||
/// function will process sequential ranges of blocks, and is equivalent to calling
|
||||
/// `scan_cached_blocks` and passing `None` for the optional `limit` value.
|
||||
///
|
||||
/// This function pays attention only to cached blocks with heights greater than the
|
||||
/// highest scanned block in `db_data`. Cached blocks with lower heights are not verified
|
||||
/// against previously-scanned blocks. In particular, this function **assumes** that the
|
||||
/// caller is handling rollbacks.
|
||||
///
|
||||
/// For brand-new light client databases, this function starts scanning from the Sapling
|
||||
/// activation height. This height can be fast-forwarded to a more recent block by calling
|
||||
/// [`init_blocks_table`] before this function.
|
||||
///
|
||||
/// Scanned blocks are required to be height-sequential. If a block is missing from the
|
||||
/// cache, an error will be returned with kind [`ErrorKind::InvalidHeight`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use zcash_client_sqlite::scan::scan_cached_blocks;
|
||||
///
|
||||
/// scan_cached_blocks("/path/to/cache.db", "/path/to/data.db", None);
|
||||
/// ```
|
||||
///
|
||||
/// [`init_blocks_table`]: crate::init::init_blocks_table
|
||||
pub fn scan_cached_blocks<P: AsRef<Path>, Q: AsRef<Path>>(
|
||||
db_cache: P,
|
||||
db_data: Q,
|
||||
limit: Option<i32>,
|
||||
) -> Result<(), Error> {
|
||||
let cache = Connection::open(db_cache)?;
|
||||
let data = Connection::open(db_data)?;
|
||||
|
||||
// Recall where we synced up to previously.
|
||||
// If we have never synced, use sapling activation height to select all cached CompactBlocks.
|
||||
let mut last_height = data.query_row("SELECT MAX(height) FROM blocks", NO_PARAMS, |row| {
|
||||
row.get(0).or(Ok(SAPLING_ACTIVATION_HEIGHT - 1))
|
||||
})?;
|
||||
|
||||
// Fetch the CompactBlocks we need to scan
|
||||
let mut stmt_blocks = cache.prepare(
|
||||
"SELECT height, data FROM compactblocks WHERE height > ? ORDER BY height ASC LIMIT ?",
|
||||
)?;
|
||||
let rows = stmt_blocks.query_map(&[last_height, limit.unwrap_or(i32::max_value())], |row| {
|
||||
Ok(CompactBlockRow {
|
||||
height: row.get(0)?,
|
||||
data: row.get(1)?,
|
||||
})
|
||||
})?;
|
||||
|
||||
// Fetch the ExtendedFullViewingKeys we are tracking
|
||||
let mut stmt_fetch_accounts =
|
||||
data.prepare("SELECT extfvk FROM accounts ORDER BY account ASC")?;
|
||||
let extfvks = stmt_fetch_accounts.query_map(NO_PARAMS, |row| {
|
||||
row.get(0).map(|extfvk: String| {
|
||||
decode_extended_full_viewing_key(HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, &extfvk)
|
||||
})
|
||||
})?;
|
||||
// Raise SQL errors from the query, IO errors from parsing, and incorrect HRP errors.
|
||||
let extfvks: Vec<_> = extfvks
|
||||
.collect::<Result<Result<Option<_>, _>, _>>()??
|
||||
.ok_or(Error(ErrorKind::IncorrectHRPExtFVK))?;
|
||||
|
||||
// Get the most recent CommitmentTree
|
||||
let mut stmt_fetch_tree = data.prepare("SELECT sapling_tree FROM blocks WHERE height = ?")?;
|
||||
let mut tree = stmt_fetch_tree
|
||||
.query_row(&[last_height], |row| {
|
||||
row.get(0).map(|data: Vec<_>| {
|
||||
CommitmentTree::read(&data[..]).unwrap_or_else(|_| CommitmentTree::new())
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|_| CommitmentTree::new());
|
||||
|
||||
// Get most recent incremental witnesses for the notes we are tracking
|
||||
let mut stmt_fetch_witnesses =
|
||||
data.prepare("SELECT note, witness FROM sapling_witnesses WHERE block = ?")?;
|
||||
let witnesses = stmt_fetch_witnesses.query_map(&[last_height], |row| {
|
||||
let id_note = row.get(0)?;
|
||||
let data: Vec<_> = row.get(1)?;
|
||||
Ok(IncrementalWitness::read(&data[..]).map(|witness| WitnessRow { id_note, witness }))
|
||||
})?;
|
||||
let mut witnesses: Vec<_> = witnesses.collect::<Result<Result<_, _>, _>>()??;
|
||||
|
||||
// Get the nullifiers for the notes we are tracking
|
||||
let mut stmt_fetch_nullifiers =
|
||||
data.prepare("SELECT id_note, nf, account FROM received_notes WHERE spent IS NULL")?;
|
||||
let nullifiers = stmt_fetch_nullifiers.query_map(NO_PARAMS, |row| {
|
||||
let nf: Vec<_> = row.get(1)?;
|
||||
let account: i64 = row.get(2)?;
|
||||
Ok((nf, account as usize))
|
||||
})?;
|
||||
let mut nullifiers: Vec<_> = nullifiers.collect::<Result<_, _>>()?;
|
||||
|
||||
// Prepare per-block SQL statements
|
||||
let mut stmt_insert_block = data.prepare(
|
||||
"INSERT INTO blocks (height, hash, time, sapling_tree)
|
||||
VALUES (?, ?, ?, ?)",
|
||||
)?;
|
||||
let mut stmt_update_tx = data.prepare(
|
||||
"UPDATE transactions
|
||||
SET block = ?, tx_index = ? WHERE txid = ?",
|
||||
)?;
|
||||
let mut stmt_insert_tx = data.prepare(
|
||||
"INSERT INTO transactions (txid, block, tx_index)
|
||||
VALUES (?, ?, ?)",
|
||||
)?;
|
||||
let mut stmt_select_tx = data.prepare("SELECT id_tx FROM transactions WHERE txid = ?")?;
|
||||
let mut stmt_mark_spent_note =
|
||||
data.prepare("UPDATE received_notes SET spent = ? WHERE nf = ?")?;
|
||||
let mut stmt_update_note = data.prepare(
|
||||
"UPDATE received_notes
|
||||
SET account = ?, diversifier = ?, value = ?, rcm = ?, nf = ?, is_change = ?
|
||||
WHERE tx = ? AND output_index = ?",
|
||||
)?;
|
||||
let mut stmt_insert_note = data.prepare(
|
||||
"INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, nf, is_change)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
)?;
|
||||
let mut stmt_select_note =
|
||||
data.prepare("SELECT id_note FROM received_notes WHERE tx = ? AND output_index = ?")?;
|
||||
let mut stmt_insert_witness = data.prepare(
|
||||
"INSERT INTO sapling_witnesses (note, block, witness)
|
||||
VALUES (?, ?, ?)",
|
||||
)?;
|
||||
let mut stmt_prune_witnesses = data.prepare("DELETE FROM sapling_witnesses WHERE block < ?")?;
|
||||
let mut stmt_update_expired = data.prepare(
|
||||
"UPDATE received_notes SET spent = NULL WHERE EXISTS (
|
||||
SELECT id_tx FROM transactions
|
||||
WHERE id_tx = received_notes.spent AND block IS NULL AND expiry_height < ?
|
||||
)",
|
||||
)?;
|
||||
|
||||
for row in rows {
|
||||
let row = row?;
|
||||
|
||||
// Start an SQL transaction for this block.
|
||||
data.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
|
||||
|
||||
// Scanned blocks MUST be height-sequential.
|
||||
if row.height != (last_height + 1) {
|
||||
return Err(Error(ErrorKind::InvalidHeight(last_height + 1, row.height)));
|
||||
}
|
||||
last_height = row.height;
|
||||
|
||||
let block: CompactBlock = parse_from_bytes(&row.data)?;
|
||||
let block_hash = block.hash.clone();
|
||||
let block_time = block.time;
|
||||
|
||||
let txs = {
|
||||
let nf_refs: Vec<_> = nullifiers.iter().map(|(nf, acc)| (&nf[..], *acc)).collect();
|
||||
let mut witness_refs: Vec<_> = witnesses.iter_mut().map(|w| &mut w.witness).collect();
|
||||
scan_block(
|
||||
block,
|
||||
&extfvks[..],
|
||||
&nf_refs,
|
||||
&mut tree,
|
||||
&mut witness_refs[..],
|
||||
)
|
||||
};
|
||||
|
||||
// Enforce that all roots match. This is slow, so only include in debug builds.
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let cur_root = tree.root();
|
||||
for row in &witnesses {
|
||||
if row.witness.root() != cur_root {
|
||||
return Err(Error(ErrorKind::InvalidWitnessAnchor(
|
||||
row.id_note,
|
||||
last_height,
|
||||
)));
|
||||
}
|
||||
}
|
||||
for tx in &txs {
|
||||
for output in tx.shielded_outputs.iter() {
|
||||
if output.witness.root() != cur_root {
|
||||
return Err(Error(ErrorKind::InvalidNewWitnessAnchor(
|
||||
output.index,
|
||||
tx.txid,
|
||||
last_height,
|
||||
output.witness.root(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the block into the database.
|
||||
let mut encoded_tree = Vec::new();
|
||||
tree.write(&mut encoded_tree)
|
||||
.expect("Should be able to write to a Vec");
|
||||
stmt_insert_block.execute(&[
|
||||
row.height.to_sql()?,
|
||||
block_hash.to_sql()?,
|
||||
block_time.to_sql()?,
|
||||
encoded_tree.to_sql()?,
|
||||
])?;
|
||||
|
||||
for tx in txs {
|
||||
// First try update an existing transaction in the database.
|
||||
let txid = tx.txid.0.to_vec();
|
||||
let tx_row = if stmt_update_tx.execute(&[
|
||||
row.height.to_sql()?,
|
||||
(tx.index as i64).to_sql()?,
|
||||
txid.to_sql()?,
|
||||
])? == 0
|
||||
{
|
||||
// It isn't there, so insert our transaction into the database.
|
||||
stmt_insert_tx.execute(&[
|
||||
txid.to_sql()?,
|
||||
row.height.to_sql()?,
|
||||
(tx.index as i64).to_sql()?,
|
||||
])?;
|
||||
data.last_insert_rowid()
|
||||
} else {
|
||||
// It was there, so grab its row number.
|
||||
stmt_select_tx.query_row(&[txid], |row| row.get(0))?
|
||||
};
|
||||
|
||||
// Mark notes as spent and remove them from the scanning cache
|
||||
for spend in &tx.shielded_spends {
|
||||
stmt_mark_spent_note.execute(&[tx_row.to_sql()?, spend.nf.to_sql()?])?;
|
||||
}
|
||||
nullifiers = nullifiers
|
||||
.into_iter()
|
||||
.filter(|(nf, _acc)| {
|
||||
tx.shielded_spends
|
||||
.iter()
|
||||
.find(|spend| &spend.nf == nf)
|
||||
.is_none()
|
||||
})
|
||||
.collect();
|
||||
|
||||
for output in tx.shielded_outputs {
|
||||
let rcm = output.note.r.to_repr();
|
||||
let nf = output.note.nf(
|
||||
&extfvks[output.account].fvk.vk,
|
||||
output.witness.position() as u64,
|
||||
&JUBJUB,
|
||||
);
|
||||
|
||||
// Assumptions:
|
||||
// - A transaction will not contain more than 2^63 shielded outputs.
|
||||
// - A note value will never exceed 2^63 zatoshis.
|
||||
|
||||
// First try updating an existing received note into the database.
|
||||
let note_row = if stmt_update_note.execute(&[
|
||||
(output.account as i64).to_sql()?,
|
||||
output.to.diversifier().0.to_sql()?,
|
||||
(output.note.value as i64).to_sql()?,
|
||||
rcm.as_ref().to_sql()?,
|
||||
nf.to_sql()?,
|
||||
output.is_change.to_sql()?,
|
||||
tx_row.to_sql()?,
|
||||
(output.index as i64).to_sql()?,
|
||||
])? == 0
|
||||
{
|
||||
// It isn't there, so insert our note into the database.
|
||||
stmt_insert_note.execute(&[
|
||||
tx_row.to_sql()?,
|
||||
(output.index as i64).to_sql()?,
|
||||
(output.account as i64).to_sql()?,
|
||||
output.to.diversifier().0.to_sql()?,
|
||||
(output.note.value as i64).to_sql()?,
|
||||
rcm.as_ref().to_sql()?,
|
||||
nf.to_sql()?,
|
||||
output.is_change.to_sql()?,
|
||||
])?;
|
||||
data.last_insert_rowid()
|
||||
} else {
|
||||
// It was there, so grab its row number.
|
||||
stmt_select_note.query_row(
|
||||
&[tx_row.to_sql()?, (output.index as i64).to_sql()?],
|
||||
|row| row.get(0),
|
||||
)?
|
||||
};
|
||||
|
||||
// Save witness for note.
|
||||
witnesses.push(WitnessRow {
|
||||
id_note: note_row,
|
||||
witness: output.witness,
|
||||
});
|
||||
|
||||
// Cache nullifier for note (to detect subsequent spends in this scan).
|
||||
nullifiers.push((nf, output.account));
|
||||
}
|
||||
}
|
||||
|
||||
// Insert current witnesses into the database.
|
||||
let mut encoded = Vec::new();
|
||||
for witness_row in witnesses.iter() {
|
||||
encoded.clear();
|
||||
witness_row
|
||||
.witness
|
||||
.write(&mut encoded)
|
||||
.expect("Should be able to write to a Vec");
|
||||
stmt_insert_witness.execute(&[
|
||||
witness_row.id_note.to_sql()?,
|
||||
last_height.to_sql()?,
|
||||
encoded.to_sql()?,
|
||||
])?;
|
||||
}
|
||||
|
||||
// Prune the stored witnesses (we only expect rollbacks of at most 100 blocks).
|
||||
stmt_prune_witnesses.execute(&[last_height - 100])?;
|
||||
|
||||
// Update now-expired transactions that didn't get mined.
|
||||
stmt_update_expired.execute(&[last_height])?;
|
||||
|
||||
// Commit the SQL transaction, writing this block's data atomically.
|
||||
data.execute("COMMIT", NO_PARAMS)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Scans a [`Transaction`] for any information that can be decrypted by the accounts in
|
||||
/// the wallet, and saves it to the wallet.
|
||||
pub fn decrypt_and_store_transaction<P: AsRef<Path>>(
|
||||
db_data: P,
|
||||
tx: &Transaction,
|
||||
) -> Result<(), Error> {
|
||||
let data = Connection::open(db_data)?;
|
||||
|
||||
// Fetch the ExtendedFullViewingKeys we are tracking
|
||||
let mut stmt_fetch_accounts =
|
||||
data.prepare("SELECT extfvk FROM accounts ORDER BY account ASC")?;
|
||||
let extfvks = stmt_fetch_accounts.query_map(NO_PARAMS, |row| {
|
||||
row.get(0).map(|extfvk: String| {
|
||||
decode_extended_full_viewing_key(HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, &extfvk)
|
||||
})
|
||||
})?;
|
||||
// Raise SQL errors from the query, IO errors from parsing, and incorrect HRP errors.
|
||||
let extfvks: Vec<_> = extfvks
|
||||
.collect::<Result<Result<Option<_>, _>, _>>()??
|
||||
.ok_or(Error(ErrorKind::IncorrectHRPExtFVK))?;
|
||||
|
||||
let outputs = decrypt_transaction(tx, &extfvks);
|
||||
|
||||
if outputs.is_empty() {
|
||||
// Nothing to see here
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut stmt_update_tx = data.prepare(
|
||||
"UPDATE transactions
|
||||
SET expiry_height = ?, raw = ? WHERE txid = ?",
|
||||
)?;
|
||||
let mut stmt_insert_tx = data.prepare(
|
||||
"INSERT INTO transactions (txid, expiry_height, raw)
|
||||
VALUES (?, ?, ?)",
|
||||
)?;
|
||||
let mut stmt_select_tx = data.prepare("SELECT id_tx FROM transactions WHERE txid = ?")?;
|
||||
let mut stmt_update_sent_note = data.prepare(
|
||||
"UPDATE sent_notes
|
||||
SET from_account = ?, address = ?, value = ?, memo = ?
|
||||
WHERE tx = ? AND output_index = ?",
|
||||
)?;
|
||||
let mut stmt_insert_sent_note = data.prepare(
|
||||
"INSERT INTO sent_notes (tx, output_index, from_account, address, value, memo)
|
||||
VALUES (?, ?, ?, ?, ?, ?)",
|
||||
)?;
|
||||
let mut stmt_update_received_note = data.prepare(
|
||||
"UPDATE received_notes
|
||||
SET account = ?, diversifier = ?, value = ?, rcm = ?, memo = ?
|
||||
WHERE tx = ? AND output_index = ?",
|
||||
)?;
|
||||
let mut stmt_insert_received_note = data.prepare(
|
||||
"INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, memo)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
)?;
|
||||
|
||||
// Update the database atomically, to ensure the result is internally consistent.
|
||||
data.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
|
||||
|
||||
// First try update an existing transaction in the database.
|
||||
let txid = tx.txid().0.to_vec();
|
||||
let mut raw_tx = vec![];
|
||||
tx.write(&mut raw_tx)?;
|
||||
let tx_row = if stmt_update_tx.execute(&[
|
||||
tx.expiry_height.to_sql()?,
|
||||
raw_tx.to_sql()?,
|
||||
txid.to_sql()?,
|
||||
])? == 0
|
||||
{
|
||||
// It isn't there, so insert our transaction into the database.
|
||||
stmt_insert_tx.execute(&[txid.to_sql()?, tx.expiry_height.to_sql()?, raw_tx.to_sql()?])?;
|
||||
data.last_insert_rowid()
|
||||
} else {
|
||||
// It was there, so grab its row number.
|
||||
stmt_select_tx.query_row(&[txid], |row| row.get(0))?
|
||||
};
|
||||
|
||||
for output in outputs {
|
||||
let output_index = output.index as i64;
|
||||
let account = output.account as i64;
|
||||
let value = output.note.value as i64;
|
||||
|
||||
if output.outgoing {
|
||||
let to_str = RecipientAddress::from(output.to).to_string();
|
||||
|
||||
// Try updating an existing sent note.
|
||||
if stmt_update_sent_note.execute(&[
|
||||
account.to_sql()?,
|
||||
to_str.to_sql()?,
|
||||
value.to_sql()?,
|
||||
output.memo.as_bytes().to_sql()?,
|
||||
tx_row.to_sql()?,
|
||||
output_index.to_sql()?,
|
||||
])? == 0
|
||||
{
|
||||
// It isn't there, so insert.
|
||||
stmt_insert_sent_note.execute(&[
|
||||
tx_row.to_sql()?,
|
||||
output_index.to_sql()?,
|
||||
account.to_sql()?,
|
||||
to_str.to_sql()?,
|
||||
value.to_sql()?,
|
||||
output.memo.as_bytes().to_sql()?,
|
||||
])?;
|
||||
}
|
||||
} else {
|
||||
let rcm = output.note.r.to_repr();
|
||||
|
||||
// Try updating an existing received note.
|
||||
if stmt_update_received_note.execute(&[
|
||||
account.to_sql()?,
|
||||
output.to.diversifier().0.to_sql()?,
|
||||
value.to_sql()?,
|
||||
rcm.as_ref().to_sql()?,
|
||||
output.memo.as_bytes().to_sql()?,
|
||||
tx_row.to_sql()?,
|
||||
output_index.to_sql()?,
|
||||
])? == 0
|
||||
{
|
||||
// It isn't there, so insert.
|
||||
stmt_insert_received_note.execute(&[
|
||||
tx_row.to_sql()?,
|
||||
output_index.to_sql()?,
|
||||
account.to_sql()?,
|
||||
output.to.diversifier().0.to_sql()?,
|
||||
value.to_sql()?,
|
||||
rcm.as_ref().to_sql()?,
|
||||
output.memo.as_bytes().to_sql()?,
|
||||
])?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data.execute("COMMIT", NO_PARAMS)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tempfile::NamedTempFile;
|
||||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
transaction::components::Amount,
|
||||
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
|
||||
};
|
||||
|
||||
use super::scan_cached_blocks;
|
||||
use crate::{
|
||||
init::{init_accounts_table, init_cache_database, init_data_database},
|
||||
query::get_balance,
|
||||
tests::{fake_compact_block, fake_compact_block_spending, insert_into_cache},
|
||||
SAPLING_ACTIVATION_HEIGHT,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn scan_cached_blocks_requires_sequential_blocks() {
|
||||
let cache_file = NamedTempFile::new().unwrap();
|
||||
let db_cache = cache_file.path();
|
||||
init_cache_database(&db_cache).unwrap();
|
||||
|
||||
let data_file = NamedTempFile::new().unwrap();
|
||||
let db_data = data_file.path();
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let extsk = ExtendedSpendingKey::master(&[]);
|
||||
let extfvk = ExtendedFullViewingKey::from(&extsk);
|
||||
init_accounts_table(&db_data, &[extfvk.clone()]).unwrap();
|
||||
|
||||
// Create a block with height SAPLING_ACTIVATION_HEIGHT
|
||||
let value = Amount::from_u64(50000).unwrap();
|
||||
let (cb1, _) = fake_compact_block(
|
||||
SAPLING_ACTIVATION_HEIGHT,
|
||||
BlockHash([0; 32]),
|
||||
extfvk.clone(),
|
||||
value,
|
||||
);
|
||||
insert_into_cache(db_cache, &cb1);
|
||||
scan_cached_blocks(db_cache, db_data, None).unwrap();
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), value);
|
||||
|
||||
// We cannot scan a block of height SAPLING_ACTIVATION_HEIGHT + 2 next
|
||||
let (cb2, _) = fake_compact_block(
|
||||
SAPLING_ACTIVATION_HEIGHT + 1,
|
||||
cb1.hash(),
|
||||
extfvk.clone(),
|
||||
value,
|
||||
);
|
||||
let (cb3, _) = fake_compact_block(
|
||||
SAPLING_ACTIVATION_HEIGHT + 2,
|
||||
cb2.hash(),
|
||||
extfvk.clone(),
|
||||
value,
|
||||
);
|
||||
insert_into_cache(db_cache, &cb3);
|
||||
match scan_cached_blocks(db_cache, db_data, None) {
|
||||
Ok(_) => panic!("Should have failed"),
|
||||
Err(e) => assert_eq!(
|
||||
e.to_string(),
|
||||
format!(
|
||||
"Expected height of next CompactBlock to be {}, but was {}",
|
||||
SAPLING_ACTIVATION_HEIGHT + 1,
|
||||
SAPLING_ACTIVATION_HEIGHT + 2
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
// If we add a block of height SAPLING_ACTIVATION_HEIGHT + 1, we can now scan both
|
||||
insert_into_cache(db_cache, &cb2);
|
||||
scan_cached_blocks(db_cache, db_data, None).unwrap();
|
||||
assert_eq!(
|
||||
get_balance(db_data, 0).unwrap(),
|
||||
Amount::from_u64(150_000).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scan_cached_blocks_finds_received_notes() {
|
||||
let cache_file = NamedTempFile::new().unwrap();
|
||||
let db_cache = cache_file.path();
|
||||
init_cache_database(&db_cache).unwrap();
|
||||
|
||||
let data_file = NamedTempFile::new().unwrap();
|
||||
let db_data = data_file.path();
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let extsk = ExtendedSpendingKey::master(&[]);
|
||||
let extfvk = ExtendedFullViewingKey::from(&extsk);
|
||||
init_accounts_table(&db_data, &[extfvk.clone()]).unwrap();
|
||||
|
||||
// Account balance should be zero
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), Amount::zero());
|
||||
|
||||
// Create a fake CompactBlock sending value to the address
|
||||
let value = Amount::from_u64(5).unwrap();
|
||||
let (cb, _) = fake_compact_block(
|
||||
SAPLING_ACTIVATION_HEIGHT,
|
||||
BlockHash([0; 32]),
|
||||
extfvk.clone(),
|
||||
value,
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
|
||||
// Scan the cache
|
||||
scan_cached_blocks(db_cache, db_data, None).unwrap();
|
||||
|
||||
// Account balance should reflect the received note
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), value);
|
||||
|
||||
// Create a second fake CompactBlock sending more value to the address
|
||||
let value2 = Amount::from_u64(7).unwrap();
|
||||
let (cb2, _) = fake_compact_block(SAPLING_ACTIVATION_HEIGHT + 1, cb.hash(), extfvk, value2);
|
||||
insert_into_cache(db_cache, &cb2);
|
||||
|
||||
// Scan the cache again
|
||||
scan_cached_blocks(db_cache, db_data, None).unwrap();
|
||||
|
||||
// Account balance should reflect both received notes
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), value + value2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scan_cached_blocks_finds_change_notes() {
|
||||
let cache_file = NamedTempFile::new().unwrap();
|
||||
let db_cache = cache_file.path();
|
||||
init_cache_database(&db_cache).unwrap();
|
||||
|
||||
let data_file = NamedTempFile::new().unwrap();
|
||||
let db_data = data_file.path();
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let extsk = ExtendedSpendingKey::master(&[]);
|
||||
let extfvk = ExtendedFullViewingKey::from(&extsk);
|
||||
init_accounts_table(&db_data, &[extfvk.clone()]).unwrap();
|
||||
|
||||
// Account balance should be zero
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), Amount::zero());
|
||||
|
||||
// Create a fake CompactBlock sending value to the address
|
||||
let value = Amount::from_u64(5).unwrap();
|
||||
let (cb, nf) = fake_compact_block(
|
||||
SAPLING_ACTIVATION_HEIGHT,
|
||||
BlockHash([0; 32]),
|
||||
extfvk.clone(),
|
||||
value,
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
|
||||
// Scan the cache
|
||||
scan_cached_blocks(db_cache, db_data, None).unwrap();
|
||||
|
||||
// Account balance should reflect the received note
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), value);
|
||||
|
||||
// Create a second fake CompactBlock spending value from the address
|
||||
let extsk2 = ExtendedSpendingKey::master(&[0]);
|
||||
let to2 = extsk2.default_address().unwrap().1;
|
||||
let value2 = Amount::from_u64(2).unwrap();
|
||||
insert_into_cache(
|
||||
db_cache,
|
||||
&fake_compact_block_spending(
|
||||
SAPLING_ACTIVATION_HEIGHT + 1,
|
||||
cb.hash(),
|
||||
(nf, value),
|
||||
extfvk,
|
||||
to2,
|
||||
value2,
|
||||
),
|
||||
);
|
||||
|
||||
// Scan the cache again
|
||||
scan_cached_blocks(db_cache, db_data, None).unwrap();
|
||||
|
||||
// Account balance should equal the change
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), value - value2);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,848 @@
|
|||
//! Functions for creating transactions.
|
||||
|
||||
use ff::PrimeField;
|
||||
use pairing::bls12_381::Bls12;
|
||||
use rand_core::{OsRng, RngCore};
|
||||
use rusqlite::{types::ToSql, Connection, NO_PARAMS};
|
||||
use std::convert::TryInto;
|
||||
use std::path::Path;
|
||||
use zcash_client_backend::encoding::encode_extended_full_viewing_key;
|
||||
use zcash_primitives::{
|
||||
consensus,
|
||||
jubjub::fs::{Fs, FsRepr},
|
||||
keys::OutgoingViewingKey,
|
||||
merkle_tree::{IncrementalWitness, MerklePath},
|
||||
note_encryption::Memo,
|
||||
primitives::{Diversifier, Note},
|
||||
prover::TxProver,
|
||||
sapling::Node,
|
||||
transaction::{
|
||||
builder::Builder,
|
||||
components::{amount::DEFAULT_FEE, Amount},
|
||||
},
|
||||
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
|
||||
JUBJUB,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
address::RecipientAddress,
|
||||
error::{Error, ErrorKind},
|
||||
get_target_and_anchor_heights, HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY,
|
||||
};
|
||||
|
||||
/// Describes a policy for which outgoing viewing key should be able to decrypt
|
||||
/// transaction outputs.
|
||||
///
|
||||
/// For details on what transaction information is visible to the holder of an outgoing
|
||||
/// viewing key, refer to [ZIP 310].
|
||||
///
|
||||
/// [ZIP 310]: https://zips.z.cash/zip-0310
|
||||
pub enum OvkPolicy {
|
||||
/// Use the outgoing viewing key from the sender's [`ExtendedFullViewingKey`].
|
||||
///
|
||||
/// Transaction outputs will be decryptable by the sender, in addition to the
|
||||
/// recipients.
|
||||
Sender,
|
||||
|
||||
/// Use a custom outgoing viewing key. This might for instance be derived from a
|
||||
/// separate seed than the wallet's spending keys.
|
||||
///
|
||||
/// Transaction outputs will be decryptable by the recipients, and whoever controls
|
||||
/// the provided outgoing viewing key.
|
||||
Custom(OutgoingViewingKey),
|
||||
|
||||
/// Use no outgoing viewing key. Transaction outputs will be decryptable by their
|
||||
/// recipients, but not by the sender.
|
||||
Discard,
|
||||
}
|
||||
|
||||
struct SelectedNoteRow {
|
||||
diversifier: Diversifier,
|
||||
note: Note<Bls12>,
|
||||
merkle_path: MerklePath<Node>,
|
||||
}
|
||||
|
||||
/// Creates a transaction paying the specified address from the given account.
|
||||
///
|
||||
/// Returns the row index of the newly-created transaction in the `transactions` table
|
||||
/// within the data database. The caller can read the raw transaction bytes from the `raw`
|
||||
/// column in order to broadcast the transaction to the network.
|
||||
///
|
||||
/// Do not call this multiple times in parallel, or you will generate transactions that
|
||||
/// double-spend the same notes.
|
||||
///
|
||||
/// # Transaction privacy
|
||||
///
|
||||
/// `ovk_policy` specifies the desired policy for which outgoing viewing key should be
|
||||
/// able to decrypt the outputs of this transaction. This is primarily relevant to
|
||||
/// wallet recovery from backup; in particular, [`OvkPolicy::Discard`] will prevent the
|
||||
/// recipient's address, and the contents of `memo`, from ever being recovered from the
|
||||
/// block chain. (The total value sent can always be inferred by the sender from the spent
|
||||
/// notes and received change.)
|
||||
///
|
||||
/// Regardless of the specified policy, `create_to_address` saves `to`, `value`, and
|
||||
/// `memo` in `db_data`. This can be deleted independently of `ovk_policy`.
|
||||
///
|
||||
/// For details on what transaction information is visible to the holder of a full or
|
||||
/// outgoing viewing key, refer to [ZIP 310].
|
||||
///
|
||||
/// [ZIP 310]: https://zips.z.cash/zip-0310
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use zcash_client_backend::{
|
||||
/// constants::testnet::COIN_TYPE,
|
||||
/// keys::spending_key,
|
||||
/// };
|
||||
/// use zcash_client_sqlite::transact::{create_to_address, OvkPolicy};
|
||||
/// use zcash_primitives::{consensus, transaction::components::Amount};
|
||||
/// use zcash_proofs::prover::LocalTxProver;
|
||||
///
|
||||
/// let tx_prover = match LocalTxProver::with_default_location() {
|
||||
/// Some(tx_prover) => tx_prover,
|
||||
/// None => {
|
||||
/// panic!("Cannot locate the Zcash parameters. Please run zcash-fetch-params or fetch-params.sh to download the parameters, and then re-run the tests.");
|
||||
/// }
|
||||
/// };
|
||||
///
|
||||
/// let account = 0;
|
||||
/// let extsk = spending_key(&[0; 32][..], COIN_TYPE, account);
|
||||
/// let to = extsk.default_address().unwrap().1.into();
|
||||
/// match create_to_address(
|
||||
/// "/path/to/data.db",
|
||||
/// consensus::BranchId::Sapling,
|
||||
/// tx_prover,
|
||||
/// (account, &extsk),
|
||||
/// &to,
|
||||
/// Amount::from_u64(1).unwrap(),
|
||||
/// None,
|
||||
/// OvkPolicy::Sender,
|
||||
/// ) {
|
||||
/// Ok(tx_row) => (),
|
||||
/// Err(e) => (),
|
||||
/// }
|
||||
/// ```
|
||||
pub fn create_to_address<P: AsRef<Path>>(
|
||||
db_data: P,
|
||||
consensus_branch_id: consensus::BranchId,
|
||||
prover: impl TxProver,
|
||||
(account, extsk): (u32, &ExtendedSpendingKey),
|
||||
to: &RecipientAddress,
|
||||
value: Amount,
|
||||
memo: Option<Memo>,
|
||||
ovk_policy: OvkPolicy,
|
||||
) -> Result<i64, Error> {
|
||||
let data = Connection::open(db_data)?;
|
||||
|
||||
// Check that the ExtendedSpendingKey we have been given corresponds to the
|
||||
// ExtendedFullViewingKey for the account we are spending from.
|
||||
let extfvk = ExtendedFullViewingKey::from(extsk);
|
||||
if !data
|
||||
.prepare("SELECT * FROM accounts WHERE account = ? AND extfvk = ?")?
|
||||
.exists(&[
|
||||
account.to_sql()?,
|
||||
encode_extended_full_viewing_key(HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, &extfvk)
|
||||
.to_sql()?,
|
||||
])?
|
||||
{
|
||||
return Err(Error(ErrorKind::InvalidExtSK(account)));
|
||||
}
|
||||
|
||||
// Apply the outgoing viewing key policy.
|
||||
let ovk = match ovk_policy {
|
||||
OvkPolicy::Sender => extfvk.fvk.ovk,
|
||||
OvkPolicy::Custom(ovk) => ovk,
|
||||
OvkPolicy::Discard => {
|
||||
// Generate a random outgoing viewing key that the caller does not know.
|
||||
// The probability of this colliding with a legitimate outgoing viewing
|
||||
// key is negligible.
|
||||
let mut ovk = [0; 32];
|
||||
OsRng.fill_bytes(&mut ovk);
|
||||
OutgoingViewingKey(ovk)
|
||||
}
|
||||
};
|
||||
|
||||
// Target the next block, assuming we are up-to-date.
|
||||
let (height, anchor_height) = {
|
||||
let (target_height, anchor_height) = get_target_and_anchor_heights(&data)?;
|
||||
(target_height, i64::from(anchor_height))
|
||||
};
|
||||
|
||||
// The goal of this SQL statement is to select the oldest notes until the required
|
||||
// value has been reached, and then fetch the witnesses at the desired height for the
|
||||
// selected notes. This is achieved in several steps:
|
||||
//
|
||||
// 1) Use a window function to create a view of all notes, ordered from oldest to
|
||||
// newest, with an additional column containing a running sum:
|
||||
// - Unspent notes accumulate the values of all unspent notes in that note's
|
||||
// account, up to itself.
|
||||
// - Spent notes accumulate the values of all notes in the transaction they were
|
||||
// spent in, up to itself.
|
||||
//
|
||||
// 2) Select all unspent notes in the desired account, along with their running sum.
|
||||
//
|
||||
// 3) Select all notes for which the running sum was less than the required value, as
|
||||
// well as a single note for which the sum was greater than or equal to the
|
||||
// required value, bringing the sum of all selected notes across the threshold.
|
||||
//
|
||||
// 4) Match the selected notes against the witnesses at the desired height.
|
||||
let target_value = i64::from(value + DEFAULT_FEE);
|
||||
let mut stmt_select_notes = data.prepare(
|
||||
"WITH selected AS (
|
||||
WITH eligible AS (
|
||||
SELECT id_note, diversifier, value, rcm,
|
||||
SUM(value) OVER
|
||||
(PARTITION BY account, spent ORDER BY id_note) AS so_far
|
||||
FROM received_notes
|
||||
INNER JOIN transactions ON transactions.id_tx = received_notes.tx
|
||||
WHERE account = ? AND spent IS NULL AND transactions.block <= ?
|
||||
)
|
||||
SELECT * FROM eligible WHERE so_far < ?
|
||||
UNION
|
||||
SELECT * FROM (SELECT * FROM eligible WHERE so_far >= ? LIMIT 1)
|
||||
), witnesses AS (
|
||||
SELECT note, witness FROM sapling_witnesses
|
||||
WHERE block = ?
|
||||
)
|
||||
SELECT selected.diversifier, selected.value, selected.rcm, witnesses.witness
|
||||
FROM selected
|
||||
INNER JOIN witnesses ON selected.id_note = witnesses.note",
|
||||
)?;
|
||||
|
||||
// Select notes
|
||||
let notes = stmt_select_notes.query_and_then::<_, Error, _, _>(
|
||||
&[
|
||||
i64::from(account),
|
||||
anchor_height,
|
||||
target_value,
|
||||
target_value,
|
||||
anchor_height,
|
||||
],
|
||||
|row| {
|
||||
let diversifier = {
|
||||
let d: Vec<_> = row.get(0)?;
|
||||
if d.len() != 11 {
|
||||
return Err(Error(ErrorKind::CorruptedData(
|
||||
"Invalid diversifier length",
|
||||
)));
|
||||
}
|
||||
let mut tmp = [0; 11];
|
||||
tmp.copy_from_slice(&d);
|
||||
Diversifier(tmp)
|
||||
};
|
||||
|
||||
let note_value: i64 = row.get(1)?;
|
||||
|
||||
let rcm = {
|
||||
let d: Vec<_> = row.get(2)?;
|
||||
let tmp = FsRepr(
|
||||
d[..]
|
||||
.try_into()
|
||||
.map_err(|_| Error(ErrorKind::InvalidNote))?,
|
||||
);
|
||||
Fs::from_repr(tmp).ok_or(Error(ErrorKind::InvalidNote))?
|
||||
};
|
||||
|
||||
let from = extfvk
|
||||
.fvk
|
||||
.vk
|
||||
.to_payment_address(diversifier, &JUBJUB)
|
||||
.unwrap();
|
||||
let note = from.create_note(note_value as u64, rcm, &JUBJUB).unwrap();
|
||||
|
||||
let merkle_path = {
|
||||
let d: Vec<_> = row.get(3)?;
|
||||
IncrementalWitness::read(&d[..])?
|
||||
.path()
|
||||
.expect("the tree is not empty")
|
||||
};
|
||||
|
||||
Ok(SelectedNoteRow {
|
||||
diversifier,
|
||||
note,
|
||||
merkle_path,
|
||||
})
|
||||
},
|
||||
)?;
|
||||
let notes: Vec<SelectedNoteRow> = notes.collect::<Result<_, _>>()?;
|
||||
|
||||
// Confirm we were able to select sufficient value
|
||||
let selected_value = notes
|
||||
.iter()
|
||||
.fold(0, |acc, selected| acc + selected.note.value);
|
||||
if selected_value < target_value as u64 {
|
||||
return Err(Error(ErrorKind::InsufficientBalance(
|
||||
selected_value,
|
||||
target_value as u64,
|
||||
)));
|
||||
}
|
||||
|
||||
// Create the transaction
|
||||
let mut builder = Builder::new(height);
|
||||
for selected in notes {
|
||||
builder.add_sapling_spend(
|
||||
extsk.clone(),
|
||||
selected.diversifier,
|
||||
selected.note,
|
||||
selected.merkle_path,
|
||||
)?;
|
||||
}
|
||||
match to {
|
||||
RecipientAddress::Shielded(to) => {
|
||||
builder.add_sapling_output(ovk, to.clone(), value, memo.clone())
|
||||
}
|
||||
RecipientAddress::Transparent(to) => builder.add_transparent_output(&to, value),
|
||||
}?;
|
||||
let (tx, tx_metadata) = builder.build(consensus_branch_id, &prover)?;
|
||||
// We only called add_sapling_output() once.
|
||||
let output_index = match tx_metadata.output_index(0) {
|
||||
Some(idx) => idx as i64,
|
||||
None => panic!("Output 0 should exist in the transaction"),
|
||||
};
|
||||
let created = time::get_time();
|
||||
|
||||
// Update the database atomically, to ensure the result is internally consistent.
|
||||
data.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
|
||||
|
||||
// Save the transaction in the database.
|
||||
let mut raw_tx = vec![];
|
||||
tx.write(&mut raw_tx)?;
|
||||
let mut stmt_insert_tx = data.prepare(
|
||||
"INSERT INTO transactions (txid, created, expiry_height, raw)
|
||||
VALUES (?, ?, ?, ?)",
|
||||
)?;
|
||||
stmt_insert_tx.execute(&[
|
||||
tx.txid().0.to_sql()?,
|
||||
created.to_sql()?,
|
||||
tx.expiry_height.to_sql()?,
|
||||
raw_tx.to_sql()?,
|
||||
])?;
|
||||
let id_tx = data.last_insert_rowid();
|
||||
|
||||
// Mark notes as spent.
|
||||
//
|
||||
// This locks the notes so they aren't selected again by a subsequent call to
|
||||
// create_to_address() before this transaction has been mined (at which point the notes
|
||||
// get re-marked as spent).
|
||||
//
|
||||
// Assumes that create_to_address() will never be called in parallel, which is a
|
||||
// reasonable assumption for a light client such as a mobile phone.
|
||||
let mut stmt_mark_spent_note =
|
||||
data.prepare("UPDATE received_notes SET spent = ? WHERE nf = ?")?;
|
||||
for spend in &tx.shielded_spends {
|
||||
stmt_mark_spent_note.execute(&[id_tx.to_sql()?, spend.nullifier.to_sql()?])?;
|
||||
}
|
||||
|
||||
// Save the sent note in the database.
|
||||
// TODO: Decide how to save transparent output information.
|
||||
let to_str = to.to_string();
|
||||
if let Some(memo) = memo {
|
||||
let mut stmt_insert_sent_note = data.prepare(
|
||||
"INSERT INTO sent_notes (tx, output_index, from_account, address, value, memo)
|
||||
VALUES (?, ?, ?, ?, ?, ?)",
|
||||
)?;
|
||||
stmt_insert_sent_note.execute(&[
|
||||
id_tx.to_sql()?,
|
||||
output_index.to_sql()?,
|
||||
account.to_sql()?,
|
||||
to_str.to_sql()?,
|
||||
i64::from(value).to_sql()?,
|
||||
memo.as_bytes().to_sql()?,
|
||||
])?;
|
||||
} else {
|
||||
let mut stmt_insert_sent_note = data.prepare(
|
||||
"INSERT INTO sent_notes (tx, output_index, from_account, address, value)
|
||||
VALUES (?, ?, ?, ?, ?)",
|
||||
)?;
|
||||
stmt_insert_sent_note.execute(&[
|
||||
id_tx.to_sql()?,
|
||||
output_index.to_sql()?,
|
||||
account.to_sql()?,
|
||||
to_str.to_sql()?,
|
||||
i64::from(value).to_sql()?,
|
||||
])?;
|
||||
}
|
||||
|
||||
data.execute("COMMIT", NO_PARAMS)?;
|
||||
|
||||
// Return the row number of the transaction, so the caller can fetch it for sending.
|
||||
Ok(id_tx)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rusqlite::Connection;
|
||||
use tempfile::NamedTempFile;
|
||||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
consensus,
|
||||
note_encryption::try_sapling_output_recovery,
|
||||
prover::TxProver,
|
||||
transaction::{components::Amount, Transaction},
|
||||
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
|
||||
JUBJUB,
|
||||
};
|
||||
use zcash_proofs::prover::LocalTxProver;
|
||||
|
||||
use super::{create_to_address, OvkPolicy};
|
||||
use crate::{
|
||||
init::{init_accounts_table, init_blocks_table, init_cache_database, init_data_database},
|
||||
query::{get_balance, get_verified_balance},
|
||||
scan::scan_cached_blocks,
|
||||
tests::{fake_compact_block, insert_into_cache},
|
||||
SAPLING_ACTIVATION_HEIGHT,
|
||||
};
|
||||
|
||||
fn test_prover() -> impl TxProver {
|
||||
match LocalTxProver::with_default_location() {
|
||||
Some(tx_prover) => tx_prover,
|
||||
None => {
|
||||
panic!("Cannot locate the Zcash parameters. Please run zcash-fetch-params or fetch-params.sh to download the parameters, and then re-run the tests.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_to_address_fails_on_incorrect_extsk() {
|
||||
let data_file = NamedTempFile::new().unwrap();
|
||||
let db_data = data_file.path();
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add two accounts to the wallet
|
||||
let extsk0 = ExtendedSpendingKey::master(&[]);
|
||||
let extsk1 = ExtendedSpendingKey::master(&[0]);
|
||||
let extfvks = [
|
||||
ExtendedFullViewingKey::from(&extsk0),
|
||||
ExtendedFullViewingKey::from(&extsk1),
|
||||
];
|
||||
init_accounts_table(&db_data, &extfvks).unwrap();
|
||||
let to = extsk0.default_address().unwrap().1.into();
|
||||
|
||||
// Invalid extsk for the given account should cause an error
|
||||
match create_to_address(
|
||||
db_data,
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
(0, &extsk1),
|
||||
&to,
|
||||
Amount::from_u64(1).unwrap(),
|
||||
None,
|
||||
OvkPolicy::Sender,
|
||||
) {
|
||||
Ok(_) => panic!("Should have failed"),
|
||||
Err(e) => assert_eq!(e.to_string(), "Incorrect ExtendedSpendingKey for account 0"),
|
||||
}
|
||||
match create_to_address(
|
||||
db_data,
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
(1, &extsk0),
|
||||
&to,
|
||||
Amount::from_u64(1).unwrap(),
|
||||
None,
|
||||
OvkPolicy::Sender,
|
||||
) {
|
||||
Ok(_) => panic!("Should have failed"),
|
||||
Err(e) => assert_eq!(e.to_string(), "Incorrect ExtendedSpendingKey for account 1"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_to_address_fails_with_no_blocks() {
|
||||
let data_file = NamedTempFile::new().unwrap();
|
||||
let db_data = data_file.path();
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let extsk = ExtendedSpendingKey::master(&[]);
|
||||
let extfvks = [ExtendedFullViewingKey::from(&extsk)];
|
||||
init_accounts_table(&db_data, &extfvks).unwrap();
|
||||
let to = extsk.default_address().unwrap().1.into();
|
||||
|
||||
// We cannot do anything if we aren't synchronised
|
||||
match create_to_address(
|
||||
db_data,
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
(0, &extsk),
|
||||
&to,
|
||||
Amount::from_u64(1).unwrap(),
|
||||
None,
|
||||
OvkPolicy::Sender,
|
||||
) {
|
||||
Ok(_) => panic!("Should have failed"),
|
||||
Err(e) => assert_eq!(e.to_string(), "Must scan blocks first"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_to_address_fails_on_insufficient_balance() {
|
||||
let data_file = NamedTempFile::new().unwrap();
|
||||
let db_data = data_file.path();
|
||||
init_data_database(&db_data).unwrap();
|
||||
init_blocks_table(&db_data, 1, BlockHash([1; 32]), 1, &[]).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let extsk = ExtendedSpendingKey::master(&[]);
|
||||
let extfvks = [ExtendedFullViewingKey::from(&extsk)];
|
||||
init_accounts_table(&db_data, &extfvks).unwrap();
|
||||
let to = extsk.default_address().unwrap().1.into();
|
||||
|
||||
// Account balance should be zero
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), Amount::zero());
|
||||
|
||||
// We cannot spend anything
|
||||
match create_to_address(
|
||||
db_data,
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
(0, &extsk),
|
||||
&to,
|
||||
Amount::from_u64(1).unwrap(),
|
||||
None,
|
||||
OvkPolicy::Sender,
|
||||
) {
|
||||
Ok(_) => panic!("Should have failed"),
|
||||
Err(e) => assert_eq!(
|
||||
e.to_string(),
|
||||
"Insufficient balance (have 0, need 10001 including fee)"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_to_address_fails_on_unverified_notes() {
|
||||
let cache_file = NamedTempFile::new().unwrap();
|
||||
let db_cache = cache_file.path();
|
||||
init_cache_database(&db_cache).unwrap();
|
||||
|
||||
let data_file = NamedTempFile::new().unwrap();
|
||||
let db_data = data_file.path();
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let extsk = ExtendedSpendingKey::master(&[]);
|
||||
let extfvk = ExtendedFullViewingKey::from(&extsk);
|
||||
init_accounts_table(&db_data, &[extfvk.clone()]).unwrap();
|
||||
|
||||
// Add funds to the wallet in a single note
|
||||
let value = Amount::from_u64(50000).unwrap();
|
||||
let (cb, _) = fake_compact_block(
|
||||
SAPLING_ACTIVATION_HEIGHT,
|
||||
BlockHash([0; 32]),
|
||||
extfvk.clone(),
|
||||
value,
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
scan_cached_blocks(db_cache, db_data, None).unwrap();
|
||||
|
||||
// Verified balance matches total balance
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), value);
|
||||
assert_eq!(get_verified_balance(db_data, 0).unwrap(), value);
|
||||
|
||||
// Add more funds to the wallet in a second note
|
||||
let (cb, _) = fake_compact_block(
|
||||
SAPLING_ACTIVATION_HEIGHT + 1,
|
||||
cb.hash(),
|
||||
extfvk.clone(),
|
||||
value,
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
scan_cached_blocks(db_cache, db_data, None).unwrap();
|
||||
|
||||
// Verified balance does not include the second note
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), value + value);
|
||||
assert_eq!(get_verified_balance(db_data, 0).unwrap(), value);
|
||||
|
||||
// Spend fails because there are insufficient verified notes
|
||||
let extsk2 = ExtendedSpendingKey::master(&[]);
|
||||
let to = extsk2.default_address().unwrap().1.into();
|
||||
match create_to_address(
|
||||
db_data,
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
(0, &extsk),
|
||||
&to,
|
||||
Amount::from_u64(70000).unwrap(),
|
||||
None,
|
||||
OvkPolicy::Sender,
|
||||
) {
|
||||
Ok(_) => panic!("Should have failed"),
|
||||
Err(e) => assert_eq!(
|
||||
e.to_string(),
|
||||
"Insufficient balance (have 50000, need 80000 including fee)"
|
||||
),
|
||||
}
|
||||
|
||||
// Mine blocks SAPLING_ACTIVATION_HEIGHT + 2 to 9 until just before the second
|
||||
// note is verified
|
||||
for i in 2..10 {
|
||||
let (cb, _) = fake_compact_block(
|
||||
SAPLING_ACTIVATION_HEIGHT + i,
|
||||
cb.hash(),
|
||||
extfvk.clone(),
|
||||
value,
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
}
|
||||
scan_cached_blocks(db_cache, db_data, None).unwrap();
|
||||
|
||||
// Second spend still fails
|
||||
match create_to_address(
|
||||
db_data,
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
(0, &extsk),
|
||||
&to,
|
||||
Amount::from_u64(70000).unwrap(),
|
||||
None,
|
||||
OvkPolicy::Sender,
|
||||
) {
|
||||
Ok(_) => panic!("Should have failed"),
|
||||
Err(e) => assert_eq!(
|
||||
e.to_string(),
|
||||
"Insufficient balance (have 50000, need 80000 including fee)"
|
||||
),
|
||||
}
|
||||
|
||||
// Mine block 11 so that the second note becomes verified
|
||||
let (cb, _) = fake_compact_block(
|
||||
SAPLING_ACTIVATION_HEIGHT + 10,
|
||||
cb.hash(),
|
||||
extfvk.clone(),
|
||||
value,
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
scan_cached_blocks(db_cache, db_data, None).unwrap();
|
||||
|
||||
// Second spend should now succeed
|
||||
create_to_address(
|
||||
db_data,
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
(0, &extsk),
|
||||
&to,
|
||||
Amount::from_u64(70000).unwrap(),
|
||||
None,
|
||||
OvkPolicy::Sender,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_to_address_fails_on_locked_notes() {
|
||||
let cache_file = NamedTempFile::new().unwrap();
|
||||
let db_cache = cache_file.path();
|
||||
init_cache_database(&db_cache).unwrap();
|
||||
|
||||
let data_file = NamedTempFile::new().unwrap();
|
||||
let db_data = data_file.path();
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let extsk = ExtendedSpendingKey::master(&[]);
|
||||
let extfvk = ExtendedFullViewingKey::from(&extsk);
|
||||
init_accounts_table(&db_data, &[extfvk.clone()]).unwrap();
|
||||
|
||||
// Add funds to the wallet in a single note
|
||||
let value = Amount::from_u64(50000).unwrap();
|
||||
let (cb, _) = fake_compact_block(
|
||||
SAPLING_ACTIVATION_HEIGHT,
|
||||
BlockHash([0; 32]),
|
||||
extfvk.clone(),
|
||||
value,
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
scan_cached_blocks(db_cache, db_data, None).unwrap();
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), value);
|
||||
|
||||
// Send some of the funds to another address
|
||||
let extsk2 = ExtendedSpendingKey::master(&[]);
|
||||
let to = extsk2.default_address().unwrap().1.into();
|
||||
create_to_address(
|
||||
db_data,
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
(0, &extsk),
|
||||
&to,
|
||||
Amount::from_u64(15000).unwrap(),
|
||||
None,
|
||||
OvkPolicy::Sender,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// A second spend fails because there are no usable notes
|
||||
match create_to_address(
|
||||
db_data,
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
(0, &extsk),
|
||||
&to,
|
||||
Amount::from_u64(2000).unwrap(),
|
||||
None,
|
||||
OvkPolicy::Sender,
|
||||
) {
|
||||
Ok(_) => panic!("Should have failed"),
|
||||
Err(e) => assert_eq!(
|
||||
e.to_string(),
|
||||
"Insufficient balance (have 0, need 12000 including fee)"
|
||||
),
|
||||
}
|
||||
|
||||
// Mine blocks SAPLING_ACTIVATION_HEIGHT + 1 to 21 (that don't send us funds)
|
||||
// until just before the first transaction expires
|
||||
for i in 1..22 {
|
||||
let (cb, _) = fake_compact_block(
|
||||
SAPLING_ACTIVATION_HEIGHT + i,
|
||||
cb.hash(),
|
||||
ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[i as u8])),
|
||||
value,
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
}
|
||||
scan_cached_blocks(db_cache, db_data, None).unwrap();
|
||||
|
||||
// Second spend still fails
|
||||
match create_to_address(
|
||||
db_data,
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
(0, &extsk),
|
||||
&to,
|
||||
Amount::from_u64(2000).unwrap(),
|
||||
None,
|
||||
OvkPolicy::Sender,
|
||||
) {
|
||||
Ok(_) => panic!("Should have failed"),
|
||||
Err(e) => assert_eq!(
|
||||
e.to_string(),
|
||||
"Insufficient balance (have 0, need 12000 including fee)"
|
||||
),
|
||||
}
|
||||
|
||||
// Mine block SAPLING_ACTIVATION_HEIGHT + 22 so that the first transaction expires
|
||||
let (cb, _) = fake_compact_block(
|
||||
SAPLING_ACTIVATION_HEIGHT + 22,
|
||||
cb.hash(),
|
||||
ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[22])),
|
||||
value,
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
scan_cached_blocks(db_cache, db_data, None).unwrap();
|
||||
|
||||
// Second spend should now succeed
|
||||
create_to_address(
|
||||
db_data,
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
(0, &extsk),
|
||||
&to,
|
||||
Amount::from_u64(2000).unwrap(),
|
||||
None,
|
||||
OvkPolicy::Sender,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ovk_policy_prevents_recovery_from_chain() {
|
||||
let cache_file = NamedTempFile::new().unwrap();
|
||||
let db_cache = cache_file.path();
|
||||
init_cache_database(&db_cache).unwrap();
|
||||
|
||||
let data_file = NamedTempFile::new().unwrap();
|
||||
let db_data = data_file.path();
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let extsk = ExtendedSpendingKey::master(&[]);
|
||||
let extfvk = ExtendedFullViewingKey::from(&extsk);
|
||||
init_accounts_table(&db_data, &[extfvk.clone()]).unwrap();
|
||||
|
||||
// Add funds to the wallet in a single note
|
||||
let value = Amount::from_u64(50000).unwrap();
|
||||
let (cb, _) = fake_compact_block(
|
||||
SAPLING_ACTIVATION_HEIGHT,
|
||||
BlockHash([0; 32]),
|
||||
extfvk.clone(),
|
||||
value,
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
scan_cached_blocks(db_cache, db_data, None).unwrap();
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), value);
|
||||
|
||||
let extsk2 = ExtendedSpendingKey::master(&[]);
|
||||
let addr2 = extsk2.default_address().unwrap().1;
|
||||
let to = addr2.clone().into();
|
||||
|
||||
let send_and_recover_with_policy = |ovk_policy| {
|
||||
let tx_row = create_to_address(
|
||||
db_data,
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
(0, &extsk),
|
||||
&to,
|
||||
Amount::from_u64(15000).unwrap(),
|
||||
None,
|
||||
ovk_policy,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let data = Connection::open(db_data).unwrap();
|
||||
|
||||
// Fetch the transaction from the database
|
||||
let raw_tx: Vec<_> = data
|
||||
.query_row(
|
||||
"SELECT raw FROM transactions
|
||||
WHERE id_tx = ?",
|
||||
&[tx_row],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
let tx = Transaction::read(&raw_tx[..]).unwrap();
|
||||
|
||||
// Fetch the output index from the database
|
||||
let output_index: i64 = data
|
||||
.query_row(
|
||||
"SELECT output_index FROM sent_notes
|
||||
WHERE tx = ?",
|
||||
&[tx_row],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
let output = &tx.shielded_outputs[output_index as usize];
|
||||
|
||||
try_sapling_output_recovery(
|
||||
&extfvk.fvk.ovk,
|
||||
&output.cv,
|
||||
&output.cmu,
|
||||
&output.ephemeral_key.as_prime_order(&JUBJUB).unwrap(),
|
||||
&output.enc_ciphertext,
|
||||
&output.out_ciphertext,
|
||||
)
|
||||
};
|
||||
|
||||
// Send some of the funds to another address, keeping history.
|
||||
// The recipient output is decryptable by the sender.
|
||||
let (_, recovered_to, _) = send_and_recover_with_policy(OvkPolicy::Sender).unwrap();
|
||||
assert_eq!(&recovered_to, &addr2);
|
||||
|
||||
// Mine blocks SAPLING_ACTIVATION_HEIGHT + 1 to 22 (that don't send us funds)
|
||||
// so that the first transaction expires
|
||||
for i in 1..=22 {
|
||||
let (cb, _) = fake_compact_block(
|
||||
SAPLING_ACTIVATION_HEIGHT + i,
|
||||
cb.hash(),
|
||||
ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[i as u8])),
|
||||
value,
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
}
|
||||
scan_cached_blocks(db_cache, db_data, None).unwrap();
|
||||
|
||||
// Send the funds again, discarding history.
|
||||
// Neither transaction output is decryptable by the sender.
|
||||
assert!(send_and_recover_with_policy(OvkPolicy::Discard).is_none());
|
||||
}
|
||||
}
|
|
@ -8,6 +8,8 @@ use crate::{
|
|||
use ff::Field;
|
||||
use pairing::bls12_381::{Bls12, Fr};
|
||||
use rand::{rngs::OsRng, seq::SliceRandom, CryptoRng, RngCore};
|
||||
use std::error;
|
||||
use std::fmt;
|
||||
|
||||
use crate::{
|
||||
consensus,
|
||||
|
@ -48,6 +50,26 @@ pub enum Error {
|
|||
SpendProof,
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Error::AnchorMismatch => {
|
||||
write!(f, "Anchor mismatch (anchors for all spends must be equal)")
|
||||
}
|
||||
Error::BindingSig => write!(f, "Failed to create bindingSig"),
|
||||
Error::ChangeIsNegative(amount) => {
|
||||
write!(f, "Change is negative ({:?} zatoshis)", amount)
|
||||
}
|
||||
Error::InvalidAddress => write!(f, "Invalid address"),
|
||||
Error::InvalidAmount => write!(f, "Invalid amount"),
|
||||
Error::NoChangeAddress => write!(f, "No change address specified or discoverable"),
|
||||
Error::SpendProof => write!(f, "Failed to create Sapling spend proof"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for Error {}
|
||||
|
||||
struct SpendDescriptionInfo {
|
||||
extsk: ExtendedSpendingKey,
|
||||
diversifier: Diversifier,
|
||||
|
|
|
@ -17,6 +17,7 @@ blake2b_simd = "0.5"
|
|||
byteorder = "1"
|
||||
directories = { version = "1", optional = true }
|
||||
ff = { version = "0.6", path = "../ff" }
|
||||
minreq = { version = "2", features = ["https"], optional = true }
|
||||
pairing = { version = "0.16", path = "../pairing" }
|
||||
rand_core = "0.5.1"
|
||||
zcash_primitives = { version = "0.2", path = "../zcash_primitives" }
|
||||
|
@ -26,8 +27,17 @@ rand_xorshift = "0.2"
|
|||
|
||||
[features]
|
||||
default = ["local-prover", "multicore"]
|
||||
download-params = ["minreq"]
|
||||
local-prover = ["directories"]
|
||||
multicore = ["bellman/multicore"]
|
||||
|
||||
[[example]]
|
||||
name = "get-params-path"
|
||||
required-features = ["directories"]
|
||||
|
||||
[[example]]
|
||||
name = "download-params"
|
||||
required-features = ["download-params"]
|
||||
|
||||
[badges]
|
||||
maintenance = { status = "actively-developed" }
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
fn main() -> Result<(), minreq::Error> {
|
||||
zcash_proofs::download_parameters()
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
fn main() {
|
||||
if let Some(path) = zcash_proofs::default_params_folder() {
|
||||
if let Some(path) = path.to_str() {
|
||||
println!("{}", path);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,6 +12,11 @@ use std::fs::File;
|
|||
use std::io::{self, BufReader};
|
||||
use std::path::Path;
|
||||
|
||||
#[cfg(feature = "directories")]
|
||||
use directories::BaseDirs;
|
||||
#[cfg(feature = "directories")]
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub mod circuit;
|
||||
mod hashreader;
|
||||
pub mod sapling;
|
||||
|
@ -20,6 +25,82 @@ pub mod sprout;
|
|||
#[cfg(feature = "local-prover")]
|
||||
pub mod prover;
|
||||
|
||||
// Circuit names
|
||||
const SAPLING_SPEND_NAME: &str = "sapling-spend.params";
|
||||
const SAPLING_OUTPUT_NAME: &str = "sapling-output.params";
|
||||
|
||||
// Circuit hashes
|
||||
const SAPLING_SPEND_HASH: &str = "8270785a1a0d0bc77196f000ee6d221c9c9894f55307bd9357c3f0105d31ca63991ab91324160d8f53e2bbd3c2633a6eb8bdf5205d822e7f3f73edac51b2b70c";
|
||||
const SAPLING_OUTPUT_HASH: &str = "657e3d38dbb5cb5e7dd2970e8b03d69b4787dd907285b5a7f0790dcc8072f60bf593b32cc2d1c030e00ff5ae64bf84c5c3beb84ddc841d48264b4a171744d028";
|
||||
const SPROUT_HASH: &str = "e9b238411bd6c0ec4791e9d04245ec350c9c5744f5610dfcce4365d5ca49dfefd5054e371842b3f88fa1b9d7e8e075249b3ebabd167fa8b0f3161292d36c180a";
|
||||
|
||||
#[cfg(feature = "download-params")]
|
||||
const DOWNLOAD_URL: &str = "https://download.z.cash/downloads";
|
||||
|
||||
/// Returns the default folder that the Zcash proving parameters are located in.
|
||||
#[cfg(feature = "directories")]
|
||||
pub fn default_params_folder() -> Option<PathBuf> {
|
||||
BaseDirs::new().map(|base_dirs| {
|
||||
if cfg!(any(windows, target_os = "macos")) {
|
||||
base_dirs.data_dir().join("ZcashParams")
|
||||
} else {
|
||||
base_dirs.home_dir().join(".zcash-params")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Download the Zcash Sapling parameters, storing them in the default location.
|
||||
///
|
||||
/// This mirrors the behaviour of the `fetch-params.sh` script from `zcashd`.
|
||||
#[cfg(feature = "download-params")]
|
||||
pub fn download_parameters() -> Result<(), minreq::Error> {
|
||||
// Ensure that the default Zcash parameters location exists.
|
||||
let params_dir = default_params_folder().ok_or(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Could not load default params folder",
|
||||
))?;
|
||||
std::fs::create_dir_all(¶ms_dir)?;
|
||||
|
||||
let fetch_params = |name: &str, expected_hash: &str| -> Result<(), minreq::Error> {
|
||||
use std::io::Write;
|
||||
|
||||
// Download the parts directly (Sapling parameters are small enough for this).
|
||||
let part_1 = minreq::get(format!("{}/{}.part.1", DOWNLOAD_URL, name)).send()?;
|
||||
let part_2 = minreq::get(format!("{}/{}.part.2", DOWNLOAD_URL, name)).send()?;
|
||||
|
||||
// Verify parameter file hash.
|
||||
let hash = blake2b_simd::State::new()
|
||||
.update(part_1.as_bytes())
|
||||
.update(part_2.as_bytes())
|
||||
.finalize()
|
||||
.to_hex();
|
||||
if &hash != expected_hash {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"{} failed validation (expected: {}, actual: {}, fetched {} bytes)",
|
||||
name,
|
||||
expected_hash,
|
||||
hash,
|
||||
part_1.as_bytes().len() + part_2.as_bytes().len()
|
||||
),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
// Write parameter file.
|
||||
let mut f = File::create(params_dir.join(name))?;
|
||||
f.write_all(part_1.as_bytes())?;
|
||||
f.write_all(part_2.as_bytes())?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
fetch_params(SAPLING_SPEND_NAME, SAPLING_SPEND_HASH)?;
|
||||
fetch_params(SAPLING_OUTPUT_NAME, SAPLING_OUTPUT_HASH)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_parameters(
|
||||
spend_path: &Path,
|
||||
output_path: &Path,
|
||||
|
@ -31,11 +112,6 @@ pub fn load_parameters(
|
|||
PreparedVerifyingKey<Bls12>,
|
||||
Option<PreparedVerifyingKey<Bls12>>,
|
||||
) {
|
||||
// Sapling circuit hashes
|
||||
const SAPLING_SPEND_HASH: &str = "8270785a1a0d0bc77196f000ee6d221c9c9894f55307bd9357c3f0105d31ca63991ab91324160d8f53e2bbd3c2633a6eb8bdf5205d822e7f3f73edac51b2b70c";
|
||||
const SAPLING_OUTPUT_HASH: &str = "657e3d38dbb5cb5e7dd2970e8b03d69b4787dd907285b5a7f0790dcc8072f60bf593b32cc2d1c030e00ff5ae64bf84c5c3beb84ddc841d48264b4a171744d028";
|
||||
const SPROUT_HASH: &str = "e9b238411bd6c0ec4791e9d04245ec350c9c5744f5610dfcce4365d5ca49dfefd5054e371842b3f88fa1b9d7e8e075249b3ebabd167fa8b0f3161292d36c180a";
|
||||
|
||||
// Load from each of the paths
|
||||
let spend_fs = File::open(spend_path).expect("couldn't load Sapling spend parameters file");
|
||||
let output_fs = File::open(output_path).expect("couldn't load Sapling output parameters file");
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
//! Abstractions over the proving system and parameters for ease of use.
|
||||
|
||||
use bellman::groth16::{Parameters, PreparedVerifyingKey};
|
||||
use directories::BaseDirs;
|
||||
use pairing::bls12_381::{Bls12, Fr};
|
||||
use std::path::Path;
|
||||
use zcash_primitives::{
|
||||
|
@ -17,7 +16,10 @@ use zcash_primitives::{
|
|||
JUBJUB,
|
||||
};
|
||||
|
||||
use crate::{load_parameters, sapling::SaplingProvingContext};
|
||||
use crate::{
|
||||
default_params_folder, load_parameters, sapling::SaplingProvingContext, SAPLING_OUTPUT_NAME,
|
||||
SAPLING_SPEND_NAME,
|
||||
};
|
||||
|
||||
/// An implementation of [`TxProver`] using Sapling Spend and Output parameters from
|
||||
/// locally-accessible paths.
|
||||
|
@ -78,18 +80,11 @@ impl LocalTxProver {
|
|||
/// This function will panic if the parameters in the default local location do not
|
||||
/// have the expected hashes.
|
||||
pub fn with_default_location() -> Option<Self> {
|
||||
let base_dirs = BaseDirs::new()?;
|
||||
let unix_params_dir = base_dirs.home_dir().join(".zcash-params");
|
||||
let win_osx_params_dir = base_dirs.data_dir().join("ZcashParams");
|
||||
let (spend_path, output_path) = if unix_params_dir.exists() {
|
||||
let params_dir = default_params_folder()?;
|
||||
let (spend_path, output_path) = if params_dir.exists() {
|
||||
(
|
||||
unix_params_dir.join("sapling-spend.params"),
|
||||
unix_params_dir.join("sapling-output.params"),
|
||||
)
|
||||
} else if win_osx_params_dir.exists() {
|
||||
(
|
||||
win_osx_params_dir.join("sapling-spend.params"),
|
||||
win_osx_params_dir.join("sapling-output.params"),
|
||||
params_dir.join(SAPLING_SPEND_NAME),
|
||||
params_dir.join(SAPLING_OUTPUT_NAME),
|
||||
)
|
||||
} else {
|
||||
return None;
|
||||
|
|
Loading…
Reference in New Issue