diff --git a/Cargo.lock b/Cargo.lock index 1c698cb1..e1ce0055 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -275,6 +275,15 @@ dependencies = [ "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "directories" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "failure" version = "0.1.5" @@ -426,9 +435,11 @@ dependencies = [ "cbindgen 0.8.7 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "ffi_helpers 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "hex 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "zcash_client_backend 0.0.0 (git+https://github.com/str4d/librustzcash.git?branch=note-spending-v5)", "zcash_client_sqlite 0.0.0 (git+https://github.com/str4d/librustzcash.git?branch=note-spending-v5)", "zcash_primitives 0.0.0 (git+https://github.com/str4d/librustzcash.git?branch=note-spending-v5)", + "zcash_proofs 0.0.0 (git+https://github.com/str4d/librustzcash.git?branch=note-spending-v5)", ] [[package]] @@ -972,6 +983,22 @@ dependencies = [ "sha2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "zcash_proofs" +version = "0.0.0" +source = "git+https://github.com/str4d/librustzcash.git?branch=note-spending-v5#ae63116f639847e54716fbf47f19589d12038a16" +dependencies = [ + "bellman 0.1.0 (git+https://github.com/str4d/librustzcash.git?branch=note-spending-v5)", + "blake2-rfc 0.2.18 (git+https://github.com/gtank/blake2-rfc?rev=7a5b5fc99ae483a0043db7547fb79a6fa44b88a9)", + "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "directories 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "ff 0.4.0 (git+https://github.com/str4d/librustzcash.git?branch=note-spending-v5)", + "pairing 0.14.2 (git+https://github.com/str4d/librustzcash.git?branch=note-spending-v5)", + "rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "sapling-crypto 0.0.1 (git+https://github.com/str4d/librustzcash.git?branch=note-spending-v5)", + "zcash_primitives 0.0.0 (git+https://github.com/str4d/librustzcash.git?branch=note-spending-v5)", +] + [metadata] "checksum aes 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e6fb1737cdc8da3db76e90ca817a194249a38fcb500c2e6ecec39b29448aa873" "checksum aes-soft 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "67cc03b0a090a05cb01e96998a01905d7ceedce1bc23b756c0bb7faa0682ccb1" @@ -1007,6 +1034,7 @@ dependencies = [ "checksum crypto_api_chachapoly 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9ee35dbace0831b5fe7cb9b43eb029aa14a10f594a115025d4628a2baa63ab" "checksum digest 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)" = "03b072242a8cbaf9c145665af9d250c59af3b958f83ed6824e13533cf76d5b90" "checksum digest 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "05f47366984d3ad862010e22c7ce81a7dbcaebbdfb37241a620f8b6596ee135c" +"checksum directories 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "72d337a64190607d4fcca2cb78982c5dd57f4916e19696b48a575fa746b6cb0f" "checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2" "checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1" "checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" @@ -1088,3 +1116,4 @@ dependencies = [ "checksum zcash_client_backend 0.0.0 (git+https://github.com/str4d/librustzcash.git?branch=note-spending-v5)" = "" "checksum zcash_client_sqlite 0.0.0 (git+https://github.com/str4d/librustzcash.git?branch=note-spending-v5)" = "" "checksum zcash_primitives 0.0.0 (git+https://github.com/str4d/librustzcash.git?branch=note-spending-v5)" = "" +"checksum zcash_proofs 0.0.0 (git+https://github.com/str4d/librustzcash.git?branch=note-spending-v5)" = "" diff --git a/Cargo.toml b/Cargo.toml index ca30eb68..39bb1456 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ build = "rust/build.rs" [dependencies] failure = "0.1" ffi_helpers = "0.1" +hex = "0.3" [dependencies.zcash_client_backend] git = "https://github.com/str4d/librustzcash.git" @@ -21,6 +22,10 @@ branch = "note-spending-v5" git = "https://github.com/str4d/librustzcash.git" branch = "note-spending-v5" +[dependencies.zcash_proofs] +git = "https://github.com/str4d/librustzcash.git" +branch = "note-spending-v5" + [build-dependencies] cbindgen = "0.8" diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 18f99a08..8eaef9ef 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1,16 +1,27 @@ use failure::format_err; use ffi_helpers::panic::catch_panic; -use std::ffi::{CString, OsStr}; +use std::ffi::{CStr, CString, OsStr}; use std::os::raw::c_char; use std::os::unix::ffi::OsStrExt; use std::path::Path; use std::slice; use zcash_client_backend::{ - constants::testnet::HRP_SAPLING_EXTENDED_SPENDING_KEY, encoding::encode_extended_spending_key, + constants::{testnet::HRP_SAPLING_EXTENDED_SPENDING_KEY, SAPLING_CONSENSUS_BRANCH_ID}, + encoding::{decode_extended_spending_key, encode_extended_spending_key}, keys::spending_key, }; -use zcash_client_sqlite::{get_address, init_accounts_table, init_data_database, ErrorKind}; -use zcash_primitives::zip32::ExtendedFullViewingKey; +use zcash_client_sqlite::{ + address::RecipientAddress, + chain::{rewind_to_height, validate_combined_chain}, + get_address, get_balance, get_received_memo_as_utf8, get_sent_memo_as_utf8, + get_verified_balance, init_accounts_table, init_blocks_table, init_data_database, + scan_cached_blocks, send_to_address, ErrorKind, +}; +use zcash_primitives::{ + block::BlockHash, note_encryption::Memo, transaction::components::Amount, + zip32::ExtendedFullViewingKey, +}; +use zcash_proofs::prover::LocalTxProver; fn unwrap_exc_or(exc: Result, def: T) -> T { match exc { @@ -119,6 +130,39 @@ pub extern "C" fn zcashlc_init_accounts_table( unwrap_exc_or_null(res) } +/// Initialises the data database with the given block. +/// +/// This enables a newly-created database to be immediately-usable, without needing to +/// synchronise historic blocks. +#[no_mangle] +pub extern "C" fn zcashlc_init_blocks_table( + db_data: *const u8, + db_data_len: usize, + height: i32, + hash_hex: *const c_char, + time: u32, + sapling_tree_hex: *const c_char, +) -> i32 { + let res = catch_panic(|| { + let db_data = Path::new(OsStr::from_bytes(unsafe { + slice::from_raw_parts(db_data, db_data_len) + })); + let hash = { + let mut hash = hex::decode(unsafe { CStr::from_ptr(hash_hex) }.to_str()?).unwrap(); + hash.reverse(); + BlockHash::from_slice(&hash) + }; + let sapling_tree = + hex::decode(unsafe { CStr::from_ptr(sapling_tree_hex) }.to_str()?).unwrap(); + + match init_blocks_table(&db_data, height, hash, time, &sapling_tree) { + Ok(()) => Ok(1), + Err(e) => Err(format_err!("Error while initializing blocks table: {}", e)), + } + }); + unwrap_exc_or_null(res) +} + /// Returns the address for the account. /// /// Call `zcashlc_string_free` on the returned pointer when you are finished with it. @@ -149,6 +193,295 @@ pub extern "C" fn zcashlc_get_address( unwrap_exc_or_null(res) } +/// Returns the balance for the account, including all unspent notes that we know about. +#[no_mangle] +pub extern "C" fn zcashlc_get_balance(db_data: *const u8, db_data_len: usize, account: i32) -> i64 { + let res = catch_panic(|| { + let db_data = Path::new(OsStr::from_bytes(unsafe { + slice::from_raw_parts(db_data, db_data_len) + })); + let account = if account >= 0 { + account as u32 + } else { + return Err(format_err!("account argument must be positive")); + }; + + match get_balance(&db_data, account) { + Ok(balance) => Ok(balance.0), + Err(e) => Err(format_err!("Error while fetching balance: {}", e)), + } + }); + unwrap_exc_or(res, -1) +} + +/// Returns the verified balance for the account, which ignores notes that have been +/// received too recently and are not yet deemed spendable. +#[no_mangle] +pub extern "C" fn zcashlc_get_verified_balance( + db_data: *const u8, + db_data_len: usize, + account: i32, +) -> i64 { + let res = catch_panic(|| { + let db_data = Path::new(OsStr::from_bytes(unsafe { + slice::from_raw_parts(db_data, db_data_len) + })); + let account = if account >= 0 { + account as u32 + } else { + return Err(format_err!("account argument must be positive")); + }; + + match get_verified_balance(&db_data, account) { + Ok(balance) => Ok(balance.0), + Err(e) => Err(format_err!("Error while fetching verified balance: {}", e)), + } + }); + unwrap_exc_or(res, -1) +} + +/// 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. +/// +/// Call `zcashlc_string_free` on the returned pointer when you are finished with it. +#[no_mangle] +pub extern "C" fn zcashlc_get_received_memo_as_utf8( + db_data: *const u8, + db_data_len: usize, + id_note: i64, +) -> *mut c_char { + let res = catch_panic(|| { + let db_data = Path::new(OsStr::from_bytes(unsafe { + slice::from_raw_parts(db_data, db_data_len) + })); + + let memo = match get_received_memo_as_utf8(db_data, id_note) { + Ok(memo) => memo.unwrap_or_default(), + Err(e) => return Err(format_err!("Error while fetching memo: {}", e)), + }; + + Ok(CString::new(memo).unwrap().into_raw()) + }); + unwrap_exc_or_null(res) +} + +/// 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. +/// +/// Call `zcashlc_string_free` on the returned pointer when you are finished with it. +#[no_mangle] +pub extern "C" fn zcashlc_get_sent_memo_as_utf8( + db_data: *const u8, + db_data_len: usize, + id_note: i64, +) -> *mut c_char { + let res = catch_panic(|| { + let db_data = Path::new(OsStr::from_bytes(unsafe { + slice::from_raw_parts(db_data, db_data_len) + })); + + let memo = match get_sent_memo_as_utf8(db_data, id_note) { + Ok(memo) => memo.unwrap_or_default(), + Err(e) => return Err(format_err!("Error while fetching memo: {}", e)), + }; + + Ok(CString::new(memo).unwrap().into_raw()) + }); + unwrap_exc_or_null(res) +} + +/// 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: +/// - `-1` if the combined chain is valid. +/// - `upper_bound` 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). +/// - `0` if there was an error during validation unrelated to chain validity. +/// +/// This function does not mutate either of the databases. +#[no_mangle] +pub extern "C" fn zcashlc_validate_combined_chain( + db_cache: *const u8, + db_cache_len: usize, + db_data: *const u8, + db_data_len: usize, +) -> i32 { + let res = catch_panic(|| { + let db_cache = Path::new(OsStr::from_bytes(unsafe { + slice::from_raw_parts(db_cache, db_cache_len) + })); + let db_data = Path::new(OsStr::from_bytes(unsafe { + slice::from_raw_parts(db_data, db_data_len) + })); + + if let Err(e) = validate_combined_chain(&db_cache, &db_data) { + match e.kind() { + ErrorKind::InvalidChain(upper_bound, _) => Ok(*upper_bound), + _ => Err(format_err!("Error while validating chain: {}", e)), + } + } else { + // All blocks are valid, so "highest invalid block height" is below genesis. + Ok(-1) + } + }); + unwrap_exc_or_null(res) +} + +/// 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. +#[no_mangle] +pub extern "C" fn zcashlc_rewind_to_height( + db_data: *const u8, + db_data_len: usize, + height: i32, +) -> i32 { + let res = catch_panic(|| { + let db_data = Path::new(OsStr::from_bytes(unsafe { + slice::from_raw_parts(db_data, db_data_len) + })); + + match rewind_to_height(&db_data, height) { + Ok(()) => Ok(1), + Err(e) => Err(format_err!( + "Error while rewinding data DB to height {}: {}", + height, + e + )), + } + }); + unwrap_exc_or_null(res) +} + +/// Scans new blocks added to the cache for any transactions received by the tracked +/// accounts. +/// +/// 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 +/// [`zcashlc_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 signalled. +#[no_mangle] +pub extern "C" fn zcashlc_scan_blocks( + db_cache: *const u8, + db_cache_len: usize, + db_data: *const u8, + db_data_len: usize, +) -> i32 { + let res = catch_panic(|| { + let db_cache = Path::new(OsStr::from_bytes(unsafe { + slice::from_raw_parts(db_cache, db_cache_len) + })); + let db_data = Path::new(OsStr::from_bytes(unsafe { + slice::from_raw_parts(db_data, db_data_len) + })); + + match scan_cached_blocks(&db_cache, &db_data) { + Ok(()) => Ok(1), + Err(e) => Err(format_err!("Error while scanning blocks: {}", e)), + } + }); + unwrap_exc_or_null(res) +} + +/// 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. +#[no_mangle] +pub extern "C" fn zcashlc_send_to_address( + db_data: *const u8, + db_data_len: usize, + account: i32, + extsk: *const c_char, + to: *const c_char, + value: i64, + memo: *const c_char, + spend_params: *const u8, + spend_params_len: usize, + output_params: *const u8, + output_params_len: usize, +) -> i64 { + let res = catch_panic(|| { + let db_data = Path::new(OsStr::from_bytes(unsafe { + slice::from_raw_parts(db_data, db_data_len) + })); + let account = if account >= 0 { + account as u32 + } else { + return Err(format_err!("account argument must be positive")); + }; + let extsk = unsafe { CStr::from_ptr(extsk) }.to_str()?; + let to = unsafe { CStr::from_ptr(to) }.to_str()?; + let value = Amount(value); + let memo = unsafe { CStr::from_ptr(memo) }.to_str()?; + let spend_params = Path::new(OsStr::from_bytes(unsafe { + slice::from_raw_parts(spend_params, spend_params_len) + })); + let output_params = Path::new(OsStr::from_bytes(unsafe { + slice::from_raw_parts(output_params, output_params_len) + })); + + let extsk = match decode_extended_spending_key(HRP_SAPLING_EXTENDED_SPENDING_KEY, &extsk) { + Ok(Some(extsk)) => extsk, + Ok(None) => { + return Err(format_err!("ExtendedSpendingKey is for the wrong network")); + } + Err(e) => { + return Err(format_err!("Invalid ExtendedSpendingKey: {}", e)); + } + }; + + let to = match RecipientAddress::from_str(&to) { + Ok(Some(to)) => to, + Ok(None) => { + return Err(format_err!("PaymentAddress is for the wrong network")); + } + Err(e) => { + return Err(format_err!("Invalid address: {}", e)); + } + }; + + let memo = Memo::from_str(&memo); + + let prover = LocalTxProver::new(spend_params, output_params); + + send_to_address( + &db_data, + SAPLING_CONSENSUS_BRANCH_ID, + prover, + (account, &extsk), + &to, + value, + memo, + ) + .map_err(|e| format_err!("Error while sending funds: {}", e)) + }); + unwrap_exc_or(res, -1) +} + /// Frees strings returned by other zcashlc functions. #[no_mangle] pub extern "C" fn zcashlc_string_free(s: *mut c_char) {