From 67864dfdb6d666eb294bfb4124f5e9ff0fec8b74 Mon Sep 17 00:00:00 2001 From: adityapk00 <31996805+adityapk00@users.noreply.github.com> Date: Thu, 5 Sep 2019 10:56:19 -0700 Subject: [PATCH] Light Client Support for Memos (#1) * Dependency to note-spending-v6 branch * Spend funds * Add outgoing memos * cleanup * Incoming memo support * Refactor to LightClient * Abstract grpc client creation * TLS example * Add address command * Run blocking IO * Store memos in wallet Tx * Fetch the full txns only for new transactions * Comments * Filter first * More comments * Add commands --- rust-lightclient/Cargo.toml | 18 +- rust-lightclient/src/address.rs | 2 +- rust-lightclient/src/commands.rs | 80 +++ rust-lightclient/src/lightclient.rs | 907 +++++++++++++--------------- rust-lightclient/src/lightwallet.rs | 561 +++++++++++++++++ rust-lightclient/src/main.rs | 164 +---- rust-lightclient/src/prover.rs | 5 +- 7 files changed, 1090 insertions(+), 647 deletions(-) create mode 100644 rust-lightclient/src/commands.rs create mode 100644 rust-lightclient/src/lightwallet.rs diff --git a/rust-lightclient/Cargo.toml b/rust-lightclient/Cargo.toml index e70aa75..9daf6a7 100644 --- a/rust-lightclient/Cargo.toml +++ b/rust-lightclient/Cargo.toml @@ -24,34 +24,32 @@ rustyline = "5.0.2" [dependencies.bellman] git = "https://github.com/str4d/librustzcash.git" -branch = "demo-wasm" +branch = "note-spending-v6" default-features = false features = ["groth16"] [dependencies.pairing] git = "https://github.com/str4d/librustzcash.git" -branch = "demo-wasm" - -[dependencies.sapling-crypto] -git = "https://github.com/str4d/librustzcash.git" -branch = "demo-wasm" -default-features = false +branch = "note-spending-v6" [dependencies.zcash_client_backend] git = "https://github.com/str4d/librustzcash.git" -branch = "demo-wasm" +branch = "note-spending-v6" default-features = false [dependencies.zcash_primitives] git = "https://github.com/str4d/librustzcash.git" -branch = "demo-wasm" +branch = "note-spending-v6" default-features = false [dependencies.zcash_proofs] git = "https://github.com/str4d/librustzcash.git" -branch = "demo-wasm" +branch = "note-spending-v6" default-features = false [build-dependencies] tower-grpc-build = { git = "https://github.com/tower-rs/tower-grpc", features = ["tower-hyper"] } + +[profile.release] +debug = true \ No newline at end of file diff --git a/rust-lightclient/src/address.rs b/rust-lightclient/src/address.rs index 4d6d0a0..d5cc1ed 100644 --- a/rust-lightclient/src/address.rs +++ b/rust-lightclient/src/address.rs @@ -1,7 +1,7 @@ //! Structs for handling supported address types. use pairing::bls12_381::Bls12; -use sapling_crypto::primitives::PaymentAddress; +use zcash_primitives::primitives::PaymentAddress; use zcash_client_backend::encoding::{decode_payment_address, decode_transparent_address}; use zcash_primitives::legacy::TransparentAddress; diff --git a/rust-lightclient/src/commands.rs b/rust-lightclient/src/commands.rs new file mode 100644 index 0000000..7874117 --- /dev/null +++ b/rust-lightclient/src/commands.rs @@ -0,0 +1,80 @@ +use std::collections::HashMap; + +use crate::LightClient; + +pub trait Command { + fn help(&self); + + fn short_help(&self) -> String; + + fn exec(&self, args: &[String], lightclient: &LightClient); +} + +struct SyncCommand {} + +impl Command for SyncCommand { + fn help(&self) { + println!("Type sync for syncing"); + } + + fn short_help(&self) -> String { + "Download CompactBlocks and sync to the server".to_string() + } + + fn exec(&self, args: &[String], lightclient: &LightClient) { + lightclient.do_sync(); + } +} + +struct HelpCommand {} + +impl Command for HelpCommand { + fn help(&self) { + println!("Lists all available commands"); + } + + fn short_help(&self) -> String { + "Lists all available commands".to_string() + } + + fn exec(&self, args: &[String], _: &LightClient) { + // Print a list of all commands + get_commands().iter().for_each(| (cmd, obj) | { + println!("{} - {}", cmd, obj.short_help()); + }); + } +} + +struct AddressCommand {} +impl Command for AddressCommand { + fn help(&self) { + println!("Show my addresses"); + } + + fn short_help(&self) -> String { + "List all current addresses".to_string() + } + + fn exec(&self, args: &[String], lightclient: &LightClient) { + lightclient.do_address(); + } +} + +pub fn get_commands() -> Box>> { + let mut map: HashMap> = HashMap::new(); + + map.insert("sync".to_string(), Box::new(SyncCommand{})); + map.insert("help".to_string(), Box::new(HelpCommand{})); + map.insert("address".to_string(), Box::new(AddressCommand{})); + + Box::new(map) +} + +pub fn do_user_command(cmd: String, lightclient: &LightClient) { + match get_commands().get(&cmd) { + Some(cmd) => cmd.exec(&[], lightclient), + None => { + println!("Unknown command : {}. Type 'help' for a list of commands", cmd); + } + } +} \ No newline at end of file diff --git a/rust-lightclient/src/lightclient.rs b/rust-lightclient/src/lightclient.rs index e29f40d..93bb2f7 100644 --- a/rust-lightclient/src/lightclient.rs +++ b/rust-lightclient/src/lightclient.rs @@ -1,526 +1,477 @@ -use std::time::SystemTime; +use crate::lightwallet::LightWallet; -use pairing::bls12_381::Bls12; -use sapling_crypto::primitives::{Diversifier, Note, PaymentAddress}; -use std::cmp; -use std::collections::HashMap; -use std::sync::{Arc, RwLock}; -use protobuf::*; -use zcash_client_backend::{ - constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS, encoding::encode_payment_address, - proto::compact_formats::CompactBlock, welding_rig::scan_block, -}; -use zcash_primitives::{ - block::BlockHash, - merkle_tree::{CommitmentTree, IncrementalWitness}, - sapling::Node, - transaction::{ - builder::{Builder, DEFAULT_FEE}, - components::Amount, - TxId, - }, - zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, - JUBJUB, -}; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; -use crate::address; -use crate::prover; +use std::error::Error; +use std::io::prelude::*; +use std::fs::File; -// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global -// allocator. -#[cfg(feature = "wee_alloc")] -#[global_allocator] -static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; +use zcash_primitives::transaction::{TxId, Transaction}; +use zcash_primitives::note_encryption::Memo; -const ANCHOR_OFFSET: u32 = 10; +use futures::Future; +use hyper::client::connect::{Destination, HttpConnector}; +use tower_grpc::Request; +use tower_hyper::{client, util}; +use tower_util::MakeService; +use futures::stream::Stream; -const SAPLING_ACTIVATION_HEIGHT: i32 = 280_000; +use crate::grpc_client::{ChainSpec, BlockId, BlockRange, RawTransaction, TxFilter}; +use crate::grpc_client::client::CompactTxStreamer; + +// Used below to return the grpc "Client" type to calling methods +type Client = crate::grpc_client::client::CompactTxStreamer, tower_grpc::BoxBody>>; -fn now() -> f64 { - // web_sys::window() - // .expect("should have a Window") - // .performance() - // .expect("should have a Performance") - // .now() - SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as f64 +pub struct LightClient { + pub wallet : Arc, + pub sapling_output : Vec, + pub sapling_spend : Vec, } -struct BlockData { - height: i32, - hash: BlockHash, - tree: CommitmentTree, -} - -struct SaplingNoteData { - account: usize, - diversifier: Diversifier, - note: Note, - witnesses: Vec>, - nullifier: [u8; 32], - spent: Option, -} - -impl SaplingNoteData { - fn new( - extfvk: &ExtendedFullViewingKey, - output: zcash_client_backend::wallet::WalletShieldedOutput, - witness: IncrementalWitness, - ) -> Self { - let nf = { - let mut nf = [0; 32]; - nf.copy_from_slice( - &output - .note - .nf(&extfvk.fvk.vk, witness.position() as u64, &JUBJUB), - ); - nf - }; - - SaplingNoteData { - account: output.account, - diversifier: output.to.diversifier, - note: output.note, - witnesses: vec![witness], - nullifier: nf, - spent: None, - } - } -} - -struct WalletTx { - block: i32, - notes: Vec, -} - -struct SpendableNote { - txid: TxId, - nullifier: [u8; 32], - diversifier: Diversifier, - note: Note, - witness: IncrementalWitness, -} - -impl SpendableNote { - fn from(txid: TxId, nd: &SaplingNoteData, anchor_offset: usize) -> Option { - if nd.spent.is_none() { - let witness = nd.witnesses.get(nd.witnesses.len() - anchor_offset - 1); - - witness.map(|w| SpendableNote { - txid, - nullifier: nd.nullifier, - diversifier: nd.diversifier, - note: nd.note.clone(), - witness: w.clone(), - }) - } else { - None - } - } -} - -pub struct Client { - extsks: [ExtendedSpendingKey; 1], - extfvks: [ExtendedFullViewingKey; 1], - address: PaymentAddress, - blocks: Arc>>, - txs: Arc>>, -} - -/// Public methods, exported to JavaScript. -impl Client { +impl LightClient { pub fn new() -> Self { - - let extsk = ExtendedSpendingKey::master(&[0; 32]); - let extfvk = ExtendedFullViewingKey::from(&extsk); - let address = extfvk.default_address().unwrap().1; - - Client { - extsks: [extsk], - extfvks: [extfvk], - address, - blocks: Arc::new(RwLock::new(vec![])), - txs: Arc::new(RwLock::new(HashMap::new())), - } - } - - pub fn set_initial_block(&self, height: i32, hash: &str, sapling_tree: &str) -> bool { - let mut blocks = self.blocks.write().unwrap(); - if !blocks.is_empty() { - return false; - } - - let hash = match hex::decode(hash) { - Ok(hash) => { - let mut r = hash; - r.reverse(); - BlockHash::from_slice(&r) - }, - Err(e) => { - eprintln!("{}", e); - return false; - } + let mut w = LightClient { + wallet : Arc::new(LightWallet::new()), + sapling_output : vec![], + sapling_spend : vec![] }; - let sapling_tree = match hex::decode(sapling_tree) { - Ok(tree) => tree, - Err(e) => { - eprintln!("{}", e); - return false; - } - }; + // Read Sapling Params + let mut f = File::open("/home/adityapk/.zcash-params/sapling-output.params").unwrap(); + f.read_to_end(&mut w.sapling_output).unwrap(); + let mut f = File::open("/home/adityapk/.zcash-params/sapling-spend.params").unwrap(); + f.read_to_end(&mut w.sapling_spend).unwrap(); - if let Ok(tree) = CommitmentTree::read(&sapling_tree[..]) { - blocks.push(BlockData { height, hash, tree }); - true - } else { - false - } + w.wallet.set_initial_block(500000, + "004fada8d4dbc5e80b13522d2c6bd0116113c9b7197f0c6be69bc7a62f2824cd", + "01b733e839b5f844287a6a491409a991ec70277f39a50c99163ed378d23a829a0700100001916db36dfb9a0cf26115ed050b264546c0fa23459433c31fd72f63d188202f2400011f5f4e3bd18da479f48d674dbab64454f6995b113fa21c9d8853a9e764fb3e1f01df9d2c233ca60360e3c2bb73caf5839a1be634c8b99aea22d02abda2e747d9100001970d41722c078288101acd0a75612acfb4c434f2a55aab09fb4e812accc2ba7301485150f0deac7774dcd0fe32043bde9ba2b6bbfff787ad074339af68e88ee70101601324f1421e00a43ef57f197faf385ee4cac65aab58048016ecbd94e022973701e1b17f4bd9d1b6ca1107f619ac6d27b53dd3350d5be09b08935923cbed97906c0000000000011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39"); + + return w; } - pub fn last_scanned_height(&self) -> i32 { - self.blocks - .read() - .unwrap() - .last() - .map(|block| block.height) - .unwrap_or(SAPLING_ACTIVATION_HEIGHT - 1) + pub fn last_scanned_height(&self) -> u64 { + self.wallet.last_scanned_height() as u64 } - /// Determines the target height for a transaction, and the offset from which to - /// select anchors, based on the current synchronised block chain. - fn get_target_height_and_anchor_offset(&self) -> Option<(u32, usize)> { - match { - let blocks = self.blocks.read().unwrap(); - ( - blocks.first().map(|block| block.height as u32), - blocks.last().map(|block| block.height as u32), - ) - } { - (Some(min_height), Some(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); - - Some((target_height, (target_height - anchor_height) as usize)) - } - _ => None, - } + pub fn do_address(&self) { + println!("Address: {}", self.wallet.address()); + println!("Balance: {}", self.wallet.balance()); } - pub fn address(&self) -> String { - encode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS, &self.address) - } + pub fn do_sync(&self) { + // Sync is 3 parts + // 1. Get the latest block + // 2. Get all the blocks that we don't have + // 3. Find all new Txns that don't have the full Tx, and get them as full transactions + // and scan them, mainly to get the memos + let mut last_scanned_height = self.wallet.last_scanned_height() as u64; + let mut end_height = last_scanned_height + 1000; - pub fn balance(&self) -> u64 { - self.txs - .read() - .unwrap() - .values() - .map(|tx| { - tx.notes - .iter() - .map(|nd| if nd.spent.is_none() { nd.note.value } else { 0 }) - .sum::() - }) - .sum::() - } + // This will hold the latest block fetched from the RPC + let latest_block_height = Arc::new(AtomicU64::new(0)); + // TODO: this could be a oneshot channel + let latest_block_height_clone = latest_block_height.clone(); + self.fetch_latest_block(move |block: BlockId| { + latest_block_height_clone.store(block.height, Ordering::SeqCst); + }); + let last_block = latest_block_height.load(Ordering::SeqCst); - pub fn verified_balance(&self) -> u64 { - let anchor_height = match self.get_target_height_and_anchor_offset() { - Some((height, anchor_offset)) => height - anchor_offset as u32, - None => return 0, - }; + let bytes_downloaded = Arc::new(AtomicUsize::new(0)); - self.txs - .read() - .unwrap() - .values() - .map(|tx| { - if tx.block as u32 <= anchor_height { - tx.notes - .iter() - .map(|nd| if nd.spent.is_none() { nd.note.value } else { 0 }) - .sum::() - } else { - 0 - } - }) - .sum::() - } + // Fetch CompactBlocks in increments + loop { + let local_light_wallet = self.wallet.clone(); + let local_bytes_downloaded = bytes_downloaded.clone(); - pub fn scan_block(&self, block: &[u8]) -> bool { - let block: CompactBlock = match parse_from_bytes(block) { - Ok(block) => block, - Err(e) => { - eprintln!("Could not parse CompactBlock from bytes: {}", e); - return false; - } - }; + let simple_callback = move |encoded_block: &[u8]| { + local_light_wallet.scan_block(encoded_block); + local_bytes_downloaded.fetch_add(encoded_block.len(), Ordering::SeqCst); + }; - // Scanned blocks MUST be height-sequential. - let height = block.get_height() as i32; - if height == self.last_scanned_height() { - // If the last scanned block is rescanned, check it still matches. - if let Some(hash) = self.blocks.read().unwrap().last().map(|block| block.hash) { - if block.hash() != hash { - eprintln!("Block hash does not match for block {}. {} vs {}", height, block.hash(), hash); - return false; - } - } - return true; - } else if height != (self.last_scanned_height() + 1) { - eprintln!( - "Block is not height-sequential (expected {}, found {})", - self.last_scanned_height() + 1, - height - ); - return false; - } + print!("Syncing {}/{}, Balance = {} \r", + last_scanned_height, last_block, self.wallet.balance()); - // Get the most recent scanned data. - let mut block_data = BlockData { - height, - hash: block.hash(), - tree: self - .blocks - .read() - .unwrap() - .last() - .map(|block| block.tree.clone()) - .unwrap_or(CommitmentTree::new()), - }; - let mut txs = self.txs.write().unwrap(); + self.fetch_blocks(last_scanned_height, end_height, simple_callback); - // Create a Vec containing all unspent nullifiers. - let nfs: Vec<_> = txs - .iter() - .map(|(txid, tx)| { - let txid = *txid; - tx.notes.iter().filter_map(move |nd| { - if nd.spent.is_none() { - Some((nd.nullifier, nd.account, txid)) - } else { - None - } + last_scanned_height = end_height + 1; + end_height = last_scanned_height + 1000 - 1; + + if last_scanned_height > last_block { + break; + } else if end_height > last_block { + end_height = last_block; + } + } + + println!("Synced to {}, Downloaded {} kB \r", + last_block, bytes_downloaded.load(Ordering::SeqCst) / 1024); + + + // Get the Raw transaction for all the wallet transactions + + // We need to first copy over the Txids from the wallet struct, because + // we need to free the read lock from here (Because we'll self.wallet.txs later) + let txids_to_fetch: Vec; + { + // First, build a list of all the TxIDs and Memos that we need + // to fetch. + // 1. Get all (txid, Option) + // 2. Filter out all txids where the Memo is None + // (Which means that particular txid was never fetched. Remember + // that when memos are fetched, if they are empty, they become + // Some(f60000...) + let txids_and_memos = self.wallet.txs.read().unwrap().iter() + .flat_map( |(txid, wtx)| { // flat_map because we're collecting vector of vectors + wtx.notes.iter() + .filter( |nd| nd.memo.is_none()) // only get if memo is None (i.e., it has not been fetched) + .map( |nd| (txid.clone(), nd.memo.clone()) ) // collect (txid, memo) Clone everything because we want copies, so we can release the read lock + .collect::)>>() // convert to vector }) - }) - .flatten() - .collect(); + .collect::)>>(); + + println!("{:?}", txids_and_memos); + // TODO: Assert that all the memos here are None - // Prepare the note witnesses for updating - for tx in txs.values_mut() { - for nd in tx.notes.iter_mut() { - // Duplicate the most recent witness - if let Some(witness) = nd.witnesses.last() { - nd.witnesses.push(witness.clone()); - } - // Trim the oldest witnesses - nd.witnesses = nd - .witnesses - .split_off(nd.witnesses.len().saturating_sub(100)); - } + txids_to_fetch = txids_and_memos.iter() + .map( | (txid, _) | txid.clone() ) // We're only interested in the txids, so drop the Memo, which is None anyway + .collect::>(); // and convert into Vec } - let new_txs = { - let nf_refs: Vec<_> = nfs.iter().map(|(nf, acc, _)| (&nf[..], *acc)).collect(); + // And go and fetch the txids, getting the full transaction, so we can + // read the memos + for txid in txids_to_fetch { + let light_wallet_clone = self.wallet.clone(); + println!("Scanning txid {}", txid); - // Create a single mutable slice of all the newly-added witnesses. - let mut witness_refs: Vec<_> = txs - .values_mut() - .map(|tx| tx.notes.iter_mut().filter_map(|nd| nd.witnesses.last_mut())) - .flatten() - .collect(); + self.fetch_full_tx(txid, move |tx_bytes: &[u8] | { + let tx = Transaction::read(tx_bytes).unwrap(); - scan_block( - block, - &self.extfvks, - &nf_refs[..], - &mut block_data.tree, - &mut witness_refs[..], - ) + light_wallet_clone.scan_full_tx(&tx); + }); }; - for (tx, new_witnesses) in new_txs { - // Mark notes as spent. - for spend in &tx.shielded_spends { - let txid = nfs - .iter() - .find(|(nf, _, _)| &nf[..] == &spend.nf[..]) - .unwrap() - .2; - let mut spent_note = txs - .get_mut(&txid) - .unwrap() - .notes - .iter_mut() - .find(|nd| &nd.nullifier[..] == &spend.nf[..]) - .unwrap(); - spent_note.spent = Some(tx.txid); - } + // Print all the memos for fun. + let memos = self.wallet.txs.read().unwrap() + .values().flat_map(|wtx| { + wtx.notes.iter().map(|nd| nd.memo.clone() ).collect::>>() + }) + .map( |m| match m { + Some(memo) => { + match memo.to_utf8() { + Some(Ok(memo_str)) => Some(memo_str), + _ => None + } + } + _ => None + }) + .collect::>>(); - // Find the existing transaction entry, or create a new one. - if !txs.contains_key(&tx.txid) { - let tx_entry = WalletTx { - block: block_data.height, - notes: vec![], - }; - txs.insert(tx.txid, tx_entry); - } - let tx_entry = txs.get_mut(&tx.txid).unwrap(); - - // Save notes. - for (output, witness) in tx - .shielded_outputs - .into_iter() - .zip(new_witnesses.into_iter()) - { - tx_entry.notes.push(SaplingNoteData::new( - &self.extfvks[output.account], - output, - witness, - )); - } - } - - // Store scanned data for this block. - self.blocks.write().unwrap().push(block_data); - - true + println!("All Wallet Txns {:?}", memos); } - pub fn send_to_address( - &self, - consensus_branch_id: u32, - spend_params: &[u8], - output_params: &[u8], - to: &str, - value: u32, - ) -> Option> { - let start_time = now(); - println!( - "0: Creating transaction sending {} tazoshis to {}", - value, - to + pub fn do_send(&self, addr: String, value: u64, memo: Option) { + let rawtx = self.wallet.send_to_address( + u32::from_str_radix("2bb40e60", 16).unwrap(), // Blossom ID + &self.sapling_spend, &self.sapling_output, + &addr, value, memo ); - - let extsk = &self.extsks[0]; - let extfvk = &self.extfvks[0]; - let ovk = extfvk.fvk.ovk; - - let to = match address::RecipientAddress::from_str(to) { - Some(to) => to, - None => { - eprintln!("Invalid recipient address"); - return None; - } + + match rawtx { + Some(txbytes) => self.broadcast_raw_tx(txbytes), + None => eprintln!("No Tx to broadcast") }; - let value = Amount(value as i64); + } - // Target the next block, assuming we are up-to-date. - let (height, anchor_offset) = match self.get_target_height_and_anchor_offset() { - Some(res) => res, - None => { - eprintln!("Cannot send funds before scanning any blocks"); - return None; - } - }; + pub fn fetch_blocks(&self, start_height: u64, end_height: u64, c: F) + where F : Fn(&[u8]) { + // Fetch blocks + let uri: http::Uri = format!("http://127.0.0.1:9067").parse().unwrap(); - // Select notes to cover the target value - println!("{}: Selecting notes", now() - start_time); - let target_value = value.0 + DEFAULT_FEE.0; - let notes: Vec<_> = self - .txs - .read() - .unwrap() - .iter() - .map(|(txid, tx)| tx.notes.iter().map(move |note| (*txid, note))) - .flatten() - .filter_map(|(txid, note)| SpendableNote::from(txid, note, anchor_offset)) - .scan(0, |running_total, spendable| { - let value = spendable.note.value; - let ret = if *running_total < target_value as u64 { - Some(spendable) - } else { - None - }; - *running_total = *running_total + value; - ret + let dst = Destination::try_from_uri(uri.clone()).unwrap(); + let connector = util::Connector::new(HttpConnector::new(4)); + let settings = client::Builder::new().http2_only(true).clone(); + let mut make_client = client::Connect::with_builder(connector, settings); + + let say_hello = make_client + .make_service(dst) + .map_err(|e| panic!("connect error: {:?}", e)) + .and_then(move |conn| { + + let conn = tower_request_modifier::Builder::new() + .set_origin(uri) + .build(conn) + .unwrap(); + + // Wait until the client is ready... + CompactTxStreamer::new(conn) + .ready() + .map_err(|e| eprintln!("streaming error {:?}", e)) }) - .collect(); + .and_then(move |mut client| { + let bs = BlockId{ height: start_height, hash: vec!()}; + let be = BlockId{ height: end_height, hash: vec!()}; - // Confirm we were able to select sufficient value - let selected_value = notes - .iter() - .map(|selected| selected.note.value) - .sum::(); - if selected_value < target_value as u64 { - eprintln!( - "Insufficient funds (have {}, need {})", - selected_value, target_value - ); - return None; - } + let br = Request::new(BlockRange{ start: Some(bs), end: Some(be)}); + client + .get_block_range(br) + .map_err(|e| { + eprintln!("RouteChat request failed; err={:?}", e); + }) + .and_then(move |response| { + let inbound = response.into_inner(); + inbound.for_each(move |b| { + use prost::Message; + let mut encoded_buf = vec![]; - // Create the transaction - println!("{}: Adding {} inputs", now() - start_time, notes.len()); - let mut builder = Builder::new(height); - for selected in notes.iter() { - if let Err(e) = builder.add_sapling_spend( - extsk.clone(), - selected.diversifier, - selected.note.clone(), - selected.witness.clone(), - ) { - eprintln!("Error adding note: {:?}", e); - return None; - } - } - println!("{}: Adding output", now() - start_time); - if let Err(e) = match to { - address::RecipientAddress::Shielded(to) => { - builder.add_sapling_output(ovk, to.clone(), value, None) - } - address::RecipientAddress::Transparent(to) => { - builder.add_transparent_output(&to, value) - } - } { - eprintln!("Error adding output: {:?}", e); - return None; - } - println!("{}: Building transaction", now() - start_time); - let (tx, _) = match builder.build( - consensus_branch_id, - prover::InMemTxProver::new(spend_params, output_params), - ) { - Ok(res) => res, - Err(e) => { - eprintln!("Error creating transaction: {:?}", e); - return None; - } - }; - println!("{}: Transaction created", now() - start_time); - println!("Transaction ID: {}", tx.txid()); + b.encode(&mut encoded_buf).unwrap(); + c(&encoded_buf); - // Mark notes as spent. - let mut txs = self.txs.write().unwrap(); - for selected in notes { - let mut spent_note = txs - .get_mut(&selected.txid) - .unwrap() - .notes - .iter_mut() - .find(|nd| &nd.nullifier[..] == &selected.nullifier[..]) - .unwrap(); - spent_note.spent = Some(tx.txid()); - } + Ok(()) + }) + .map_err(|e| eprintln!("gRPC inbound stream error: {:?}", e)) + }) + }); - // Return the encoded transaction, so the caller can send it. - let mut raw_tx = vec![]; - tx.write(&mut raw_tx).unwrap(); - Some(raw_tx.into_boxed_slice()) + tokio::runtime::current_thread::Runtime::new().unwrap().block_on(say_hello).unwrap(); + } + + + pub fn fetch_full_tx(&self, txid: TxId, c: F) + where F : Fn(&[u8]) { + let uri: http::Uri = format!("http://127.0.0.1:9067").parse().unwrap(); + + let say_hello = self.make_grpc_client(uri).unwrap() + .and_then(move |mut client| { + let txfilter = TxFilter { block: None, index: 0, hash: txid.0.to_vec() }; + client.get_transaction(Request::new(txfilter)) + }) + .and_then(move |response| { + //let tx = Transaction::read(&response.into_inner().data[..]).unwrap(); + c(&response.into_inner().data); + + Ok(()) + }) + .map_err(|e| { + println!("ERR = {:?}", e); + }); + + tokio::runtime::current_thread::Runtime::new().unwrap().block_on(say_hello).unwrap() + } + + pub fn broadcast_raw_tx(&self, tx_bytes: Box<[u8]>) { + let uri: http::Uri = format!("http://127.0.0.1:9067").parse().unwrap(); + + let say_hello = self.make_grpc_client(uri).unwrap() + .and_then(move |mut client| { + client.send_transaction(Request::new(RawTransaction {data: tx_bytes.to_vec()})) + }) + .and_then(move |response| { + println!("{:?}", response.into_inner()); + Ok(()) + }) + .map_err(|e| { + println!("ERR = {:?}", e); + }); + + tokio::runtime::current_thread::Runtime::new().unwrap().block_on(say_hello).unwrap() + } + + pub fn fetch_latest_block(&self, mut c : F) + where F : FnMut(BlockId) { + let uri: http::Uri = format!("http://127.0.0.1:9067").parse().unwrap(); + + let say_hello = self.make_grpc_client(uri).unwrap() + .and_then(|mut client| { + client.get_latest_block(Request::new(ChainSpec {})) + }) + .and_then(move |response| { + c(response.into_inner()); + Ok(()) + }) + .map_err(|e| { + println!("ERR = {:?}", e); + }); + + tokio::runtime::current_thread::Runtime::new().unwrap().block_on(say_hello).unwrap() + } + + fn make_grpc_client(&self, uri: http::Uri) -> Result + Send>, Box> { + let dst = Destination::try_from_uri(uri.clone())?; + let connector = util::Connector::new(HttpConnector::new(4)); + let settings = client::Builder::new().http2_only(true).clone(); + let mut make_client = client::Connect::with_builder(connector, settings); + + let say_hello = make_client + .make_service(dst) + .map_err(|e| panic!("connect error: {:?}", e)) + .and_then(move |conn| { + + let conn = tower_request_modifier::Builder::new() + .set_origin(uri) + .build(conn) + .unwrap(); + + // Wait until the client is ready... + CompactTxStreamer::new(conn).ready() + }); + Ok(Box::new(say_hello)) } } + + + + + +/* + TLS Example https://gist.github.com/kiratp/dfcbcf0aa713a277d5d53b06d9db9308 + +// [dependencies] +// futures = "0.1.27" +// http = "0.1.17" +// tokio = "0.1.21" +// tower-request-modifier = { git = "https://github.com/tower-rs/tower-http" } +// tower-grpc = { version = "0.1.0", features = ["tower-hyper"] } +// tower-service = "0.2" +// tower-util = "0.1" +// tokio-rustls = "0.10.0-alpha.3" +// webpki = "0.19.1" +// webpki-roots = "0.16.0" +// tower-h2 = { git = "https://github.com/tower-rs/tower-h2" } +// openssl = "*" +// openssl-probe = "*" + +use std::thread; +use std::sync::{Arc}; +use futures::{future, Future}; +use tower_util::MakeService; + +use tokio_rustls::client::TlsStream; +use tokio_rustls::{rustls::ClientConfig, TlsConnector}; +use std::net::SocketAddr; + +use tokio::executor::DefaultExecutor; +use tokio::net::tcp::TcpStream; +use tower_h2; + +use std::net::ToSocketAddrs; + + + +struct Dst(SocketAddr); + + +impl tower_service::Service<()> for Dst { + type Response = TlsStream; + type Error = ::std::io::Error; + type Future = Box, Error = ::std::io::Error> + Send>; + + fn poll_ready(&mut self) -> futures::Poll<(), Self::Error> { + Ok(().into()) + } + + fn call(&mut self, _: ()) -> Self::Future { + println!("{:?}", self.0); + let mut config = ClientConfig::new(); + + config.alpn_protocols.push(b"h2".to_vec()); + config.root_store.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); + let config = Arc::new(config); + let tls_connector = TlsConnector::from(config); + + let addr_string_local = "mydomain.com"; + + let domain = webpki::DNSNameRef::try_from_ascii_str(addr_string_local).unwrap(); + let domain_local = domain.to_owned(); + + let stream = TcpStream::connect(&self.0).and_then(move |sock| { + sock.set_nodelay(true).unwrap(); + tls_connector.connect(domain_local.as_ref(), sock) + }) + .map(move |tcp| tcp); + + Box::new(stream) + } +} + +// Same implementation but without TLS. Should make it straightforward to run without TLS +// when testing on local machine + +// impl tower_service::Service<()> for Dst { +// type Response = TcpStream; +// type Error = ::std::io::Error; +// type Future = Box + Send>; + +// fn poll_ready(&mut self) -> futures::Poll<(), Self::Error> { +// Ok(().into()) +// } + +// fn call(&mut self, _: ()) -> Self::Future { +// let mut config = ClientConfig::new(); +// config.alpn_protocols.push(b"h2".to_vec()); +// config.root_store.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); + +// let addr_string_local = "mydomain.com".to_string(); +// let addr = addr_string_local.as_str(); + +// let stream = TcpStream::connect(&self.0) +// .and_then(move |sock| { +// sock.set_nodelay(true).unwrap(); +// Ok(sock) +// }); +// Box::new(stream) +// } +// } + + +fn connect() { + let keepalive = future::loop_fn((), move |_| { + let uri: http::Uri = "https://mydomain.com".parse().unwrap(); + println!("Connecting to network at: {:?}", uri); + + let addr = "https://mydomain.com:443" + .to_socket_addrs() + .unwrap() + .next() + .unwrap(); + + let h2_settings = Default::default(); + let mut make_client = tower_h2::client::Connect::new(Dst {0: addr}, h2_settings, DefaultExecutor::current()); + + make_client + .make_service(()) + .map_err(|e| { + eprintln!("HTTP/2 connection failed; err={:?}", e); + }) + .and_then(move |conn| { + let conn = tower_request_modifier::Builder::new() + .set_origin(uri) + .build(conn) + .unwrap(); + + MyGrpcService::new(conn) + // Wait until the client is ready... + .ready() + .map_err(|e| eprintln!("client closed: {:?}", e)) + }) + .and_then(move |mut client| { + // do stuff + }) + .then(|e| { + eprintln!("Reopening client connection to network: {:?}", e); + let retry_sleep = std::time::Duration::from_secs(1); + + thread::sleep(retry_sleep); + Ok(future::Loop::Continue(())) + }) + }); + + thread::spawn(move || tokio::run(keepalive)); +} + +pub fn main() { + connect(); +} + + */ \ No newline at end of file diff --git a/rust-lightclient/src/lightwallet.rs b/rust-lightclient/src/lightwallet.rs new file mode 100644 index 0000000..3638b17 --- /dev/null +++ b/rust-lightclient/src/lightwallet.rs @@ -0,0 +1,561 @@ +use std::time::SystemTime; + +use pairing::bls12_381::Bls12; +use zcash_primitives::primitives::{Diversifier, Note, PaymentAddress}; +use std::cmp; +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; +use protobuf::*; +use zcash_client_backend::{ + constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS, encoding::encode_payment_address, + proto::compact_formats::CompactBlock, welding_rig::scan_block, +}; +use zcash_primitives::{ + block::BlockHash, + merkle_tree::{CommitmentTree, IncrementalWitness}, + sapling::Node, + transaction::{ + builder::{Builder}, + components::Amount, components::amount::DEFAULT_FEE, + TxId, Transaction + }, + note_encryption::{Memo, try_sapling_note_decryption}, + zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, + JUBJUB, +}; + +use crate::address; +use crate::prover; + +// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global +// allocator. +#[cfg(feature = "wee_alloc")] +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; + +const ANCHOR_OFFSET: u32 = 10; + +const SAPLING_ACTIVATION_HEIGHT: i32 = 280_000; + + +fn now() -> f64 { + // web_sys::window() + // .expect("should have a Window") + // .performance() + // .expect("should have a Performance") + // .now() + SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as f64 +} + +struct BlockData { + height: i32, + hash: BlockHash, + tree: CommitmentTree, +} + +pub struct SaplingNoteData { + account: usize, + diversifier: Diversifier, + note: Note, + witnesses: Vec>, + nullifier: [u8; 32], + spent: Option, + pub memo: Option +} + +impl SaplingNoteData { + fn new( + extfvk: &ExtendedFullViewingKey, + output: zcash_client_backend::wallet::WalletShieldedOutput + ) -> Self { + let witness = output.witness; + let nf = { + let mut nf = [0; 32]; + nf.copy_from_slice( + &output + .note + .nf(&extfvk.fvk.vk, witness.position() as u64, &JUBJUB), + ); + nf + }; + + SaplingNoteData { + account: output.account, + diversifier: output.to.diversifier, + note: output.note, + witnesses: vec![witness], + nullifier: nf, + spent: None, + memo: None + } + } +} + +pub struct WalletTx { + block: i32, + pub notes: Vec, +} + +struct SpendableNote { + txid: TxId, + nullifier: [u8; 32], + diversifier: Diversifier, + note: Note, + witness: IncrementalWitness, +} + +impl SpendableNote { + fn from(txid: TxId, nd: &SaplingNoteData, anchor_offset: usize) -> Option { + if nd.spent.is_none() { + let witness = nd.witnesses.get(nd.witnesses.len() - anchor_offset - 1); + + witness.map(|w| SpendableNote { + txid, + nullifier: nd.nullifier, + diversifier: nd.diversifier, + note: nd.note.clone(), + witness: w.clone(), + }) + } else { + None + } + } +} + +pub struct LightWallet { + extsks: [ExtendedSpendingKey; 1], + extfvks: [ExtendedFullViewingKey; 1], + address: PaymentAddress, + blocks: Arc>>, + pub txs: Arc>>, +} + +impl LightWallet { + pub fn new() -> Self { + + let extsk = ExtendedSpendingKey::master(&[1; 32]); // New key + let extfvk = ExtendedFullViewingKey::from(&extsk); + let address = extfvk.default_address().unwrap().1; + + LightWallet { + extsks: [extsk], + extfvks: [extfvk], + address, + blocks: Arc::new(RwLock::new(vec![])), + txs: Arc::new(RwLock::new(HashMap::new())), + } + } + + pub fn set_initial_block(&self, height: i32, hash: &str, sapling_tree: &str) -> bool { + let mut blocks = self.blocks.write().unwrap(); + if !blocks.is_empty() { + return false; + } + + let hash = match hex::decode(hash) { + Ok(hash) => { + let mut r = hash; + r.reverse(); + BlockHash::from_slice(&r) + }, + Err(e) => { + eprintln!("{}", e); + return false; + } + }; + + let sapling_tree = match hex::decode(sapling_tree) { + Ok(tree) => tree, + Err(e) => { + eprintln!("{}", e); + return false; + } + }; + + if let Ok(tree) = CommitmentTree::read(&sapling_tree[..]) { + blocks.push(BlockData { height, hash, tree }); + true + } else { + false + } + } + + pub fn last_scanned_height(&self) -> i32 { + self.blocks + .read() + .unwrap() + .last() + .map(|block| block.height) + .unwrap_or(SAPLING_ACTIVATION_HEIGHT - 1) + } + + /// Determines the target height for a transaction, and the offset from which to + /// select anchors, based on the current synchronised block chain. + fn get_target_height_and_anchor_offset(&self) -> Option<(u32, usize)> { + match { + let blocks = self.blocks.read().unwrap(); + ( + blocks.first().map(|block| block.height as u32), + blocks.last().map(|block| block.height as u32), + ) + } { + (Some(min_height), Some(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); + + Some((target_height, (target_height - anchor_height) as usize)) + } + _ => None, + } + } + + pub fn address(&self) -> String { + encode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS, &self.address) + } + + pub fn balance(&self) -> u64 { + self.txs + .read() + .unwrap() + .values() + .map(|tx| { + tx.notes + .iter() + .map(|nd| if nd.spent.is_none() { nd.note.value } else { 0 }) + .sum::() + }) + .sum::() + } + + pub fn verified_balance(&self) -> u64 { + let anchor_height = match self.get_target_height_and_anchor_offset() { + Some((height, anchor_offset)) => height - anchor_offset as u32, + None => return 0, + }; + + self.txs + .read() + .unwrap() + .values() + .map(|tx| { + if tx.block as u32 <= anchor_height { + tx.notes + .iter() + .map(|nd| if nd.spent.is_none() { nd.note.value } else { 0 }) + .sum::() + } else { + 0 + } + }) + .sum::() + } + + pub fn scan_full_tx(&self, tx: &Transaction) { + for output in tx.shielded_outputs.iter() { + + let ivks: Vec<_> = self.extfvks.iter().map(|extfvk| extfvk.fvk.vk.ivk()).collect(); + + let cmu = output.cmu; + let ct = output.enc_ciphertext; + + for (_account, ivk) in ivks.iter().enumerate() { + let epk_prime = output.ephemeral_key.as_prime_order(&JUBJUB).unwrap(); + + let (note, _to, memo) = match try_sapling_note_decryption(ivk, &epk_prime, &cmu, &ct) { + Some(ret) => ret, + None => continue, + }; + + { + // Update the WalletTx + // Do it in a short scope because of the write lock. + let mut txs = self.txs.write().unwrap(); + txs.get_mut(&tx.txid()).unwrap() + .notes.iter_mut() + .find(|nd| nd.note == note).unwrap() + .memo = Some(memo); + } + } + } + } + + pub fn scan_block(&self, block: &[u8]) -> bool { + let block: CompactBlock = match parse_from_bytes(block) { + Ok(block) => block, + Err(e) => { + eprintln!("Could not parse CompactBlock from bytes: {}", e); + return false; + } + }; + + // Scanned blocks MUST be height-sequential. + let height = block.get_height() as i32; + if height == self.last_scanned_height() { + // If the last scanned block is rescanned, check it still matches. + if let Some(hash) = self.blocks.read().unwrap().last().map(|block| block.hash) { + if block.hash() != hash { + eprintln!("Block hash does not match for block {}. {} vs {}", height, block.hash(), hash); + return false; + } + } + return true; + } else if height != (self.last_scanned_height() + 1) { + eprintln!( + "Block is not height-sequential (expected {}, found {})", + self.last_scanned_height() + 1, + height + ); + return false; + } + + // Get the most recent scanned data. + let mut block_data = BlockData { + height, + hash: block.hash(), + tree: self + .blocks + .read() + .unwrap() + .last() + .map(|block| block.tree.clone()) + .unwrap_or(CommitmentTree::new()), + }; + let mut txs = self.txs.write().unwrap(); + + // Create a Vec containing all unspent nullifiers. + let nfs: Vec<_> = txs + .iter() + .map(|(txid, tx)| { + let txid = *txid; + tx.notes.iter().filter_map(move |nd| { + if nd.spent.is_none() { + Some((nd.nullifier, nd.account, txid)) + } else { + None + } + }) + }) + .flatten() + .collect(); + + // Prepare the note witnesses for updating + for tx in txs.values_mut() { + for nd in tx.notes.iter_mut() { + // Duplicate the most recent witness + if let Some(witness) = nd.witnesses.last() { + let clone = witness.clone(); + nd.witnesses.push(clone); + } + // Trim the oldest witnesses + nd.witnesses = nd + .witnesses + .split_off(nd.witnesses.len().saturating_sub(100)); + } + } + + let new_txs = { + let nf_refs: Vec<_> = nfs.iter().map(|(nf, acc, _)| (&nf[..], *acc)).collect(); + + // Create a single mutable slice of all the newly-added witnesses. + let mut witness_refs: Vec<_> = txs + .values_mut() + .map(|tx| tx.notes.iter_mut().filter_map(|nd| nd.witnesses.last_mut())) + .flatten() + .collect(); + + scan_block( + block, + &self.extfvks, + &nf_refs[..], + &mut block_data.tree, + &mut witness_refs[..], + ) + }; + + for tx in new_txs { + // Mark notes as spent. + for spend in &tx.shielded_spends { + let txid = nfs + .iter() + .find(|(nf, _, _)| &nf[..] == &spend.nf[..]) + .unwrap() + .2; + let mut spent_note = txs + .get_mut(&txid) + .unwrap() + .notes + .iter_mut() + .find(|nd| &nd.nullifier[..] == &spend.nf[..]) + .unwrap(); + spent_note.spent = Some(tx.txid); + } + + // Find the existing transaction entry, or create a new one. + if !txs.contains_key(&tx.txid) { + let tx_entry = WalletTx { + block: block_data.height, + notes: vec![], + }; + txs.insert(tx.txid, tx_entry); + } + let tx_entry = txs.get_mut(&tx.txid).unwrap(); + + // Save notes. + for output in tx + .shielded_outputs + .into_iter() + { + tx_entry.notes.push(SaplingNoteData::new( + &self.extfvks[output.account], + output + )); + } + } + + // Store scanned data for this block. + self.blocks.write().unwrap().push(block_data); + + true + } + + pub fn send_to_address( + &self, + consensus_branch_id: u32, + spend_params: &[u8], + output_params: &[u8], + to: &str, + value: u64, + memo: Option, + ) -> Option> { + let start_time = now(); + println!( + "0: Creating transaction sending {} tazoshis to {}", + value, + to + ); + + let extsk = &self.extsks[0]; + let extfvk = &self.extfvks[0]; + let ovk = extfvk.fvk.ovk; + + let to = match address::RecipientAddress::from_str(to) { + Some(to) => to, + None => { + eprintln!("Invalid recipient address"); + return None; + } + }; + let value = Amount::from_u64(value).unwrap(); + + // Target the next block, assuming we are up-to-date. + let (height, anchor_offset) = match self.get_target_height_and_anchor_offset() { + Some(res) => res, + None => { + eprintln!("Cannot send funds before scanning any blocks"); + return None; + } + }; + + // Select notes to cover the target value + println!("{}: Selecting notes", now() - start_time); + let target_value = value + DEFAULT_FEE ; + let notes: Vec<_> = self + .txs + .read() + .unwrap() + .iter() + .map(|(txid, tx)| tx.notes.iter().map(move |note| (*txid, note))) + .flatten() + .filter_map(|(txid, note)| SpendableNote::from(txid, note, anchor_offset)) + .scan(0, |running_total, spendable| { + let value = spendable.note.value; + let ret = if *running_total < u64::from(target_value) { + Some(spendable) + } else { + None + }; + *running_total = *running_total + value; + ret + }) + .collect(); + + // Confirm we were able to select sufficient value + let selected_value = notes + .iter() + .map(|selected| selected.note.value) + .sum::(); + if selected_value < u64::from(target_value) { + eprintln!( + "Insufficient funds (have {}, need {:?})", + selected_value, target_value + ); + return None; + } + + // Create the transaction + println!("{}: Adding {} inputs", now() - start_time, notes.len()); + let mut builder = Builder::new(height); + for selected in notes.iter() { + if let Err(e) = builder.add_sapling_spend( + extsk.clone(), + selected.diversifier, + selected.note.clone(), + selected.witness.clone(), + ) { + eprintln!("Error adding note: {:?}", e); + return None; + } + } + + // Compute memo if it exists + let encoded_memo = memo.map(|s| Memo::from_str(&s).unwrap() ); + + println!("{}: Adding output", now() - start_time); + if let Err(e) = match to { + address::RecipientAddress::Shielded(to) => { + builder.add_sapling_output(ovk, to.clone(), value, encoded_memo) + } + address::RecipientAddress::Transparent(to) => { + builder.add_transparent_output(&to, value) + } + } { + eprintln!("Error adding output: {:?}", e); + return None; + } + println!("{}: Building transaction", now() - start_time); + let (tx, _) = match builder.build( + consensus_branch_id, + prover::InMemTxProver::new(spend_params, output_params), + ) { + Ok(res) => res, + Err(e) => { + eprintln!("Error creating transaction: {:?}", e); + return None; + } + }; + println!("{}: Transaction created", now() - start_time); + println!("Transaction ID: {}", tx.txid()); + + // Mark notes as spent. + let mut txs = self.txs.write().unwrap(); + for selected in notes { + let mut spent_note = txs + .get_mut(&selected.txid) + .unwrap() + .notes + .iter_mut() + .find(|nd| &nd.nullifier[..] == &selected.nullifier[..]) + .unwrap(); + spent_note.spent = Some(tx.txid()); + } + + // Return the encoded transaction, so the caller can send it. + let mut raw_tx = vec![]; + tx.write(&mut raw_tx).unwrap(); + Some(raw_tx.into_boxed_slice()) + } +} diff --git a/rust-lightclient/src/main.rs b/rust-lightclient/src/main.rs index 0f27485..1ab2a91 100644 --- a/rust-lightclient/src/main.rs +++ b/rust-lightclient/src/main.rs @@ -1,24 +1,14 @@ - -use futures::Future; -use hyper::client::connect::{Destination, HttpConnector}; -use tower_grpc::Request; -use tower_hyper::{client, util}; -use tower_util::MakeService; -use futures::stream::Stream; - -use std::sync::Arc; -use std::sync::atomic::{AtomicU64, Ordering}; - - mod lightclient; +mod lightwallet; mod address; mod prover; +mod commands; + +use lightclient::LightClient; use rustyline::error::ReadlineError; use rustyline::Editor; -use crate::grpc_client::{ChainSpec, BlockId, BlockRange}; - pub mod grpc_client { include!(concat!(env!("OUT_DIR"), "/cash.z.wallet.sdk.rpc.rs")); } @@ -26,17 +16,19 @@ pub mod grpc_client { pub fn main() { + let light_client = LightClient::new(); + // `()` can be used when no completer is required let mut rl = Editor::<()>::new(); if rl.load_history("history.txt").is_err() { println!("No previous history."); } loop { - let readline = rl.readline(">> "); + let readline = rl.readline(&format!("Block:{} (h for help) >> ", light_client.last_scanned_height())); match readline { Ok(line) => { rl.add_history_entry(line.as_str()); - do_user_command(line); + commands::do_user_command(line, &light_client); }, Err(ReadlineError::Interrupted) => { println!("CTRL-C"); @@ -53,144 +45,4 @@ pub fn main() { } } rl.save_history("history.txt").unwrap(); -} - -pub fn do_user_command(cmd: String) { - match cmd.as_ref() { - "sync" => { do_sync() } - _ => { println!("Unknown command {}", cmd); } - } -} - -pub fn do_sync() { - let lightclient = Arc::new(lightclient::Client::new()); - lightclient.set_initial_block(500000, - "004fada8d4dbc5e80b13522d2c6bd0116113c9b7197f0c6be69bc7a62f2824cd", - "01b733e839b5f844287a6a491409a991ec70277f39a50c99163ed378d23a829a0700100001916db36dfb9a0cf26115ed050b264546c0fa23459433c31fd72f63d188202f2400011f5f4e3bd18da479f48d674dbab64454f6995b113fa21c9d8853a9e764fb3e1f01df9d2c233ca60360e3c2bb73caf5839a1be634c8b99aea22d02abda2e747d9100001970d41722c078288101acd0a75612acfb4c434f2a55aab09fb4e812accc2ba7301485150f0deac7774dcd0fe32043bde9ba2b6bbfff787ad074339af68e88ee70101601324f1421e00a43ef57f197faf385ee4cac65aab58048016ecbd94e022973701e1b17f4bd9d1b6ca1107f619ac6d27b53dd3350d5be09b08935923cbed97906c0000000000011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39"); - - let mut last_scanned_height = lightclient.last_scanned_height() as u64; - let mut end_height = last_scanned_height + 1000; - - let latest_block_height = Arc::new(AtomicU64::new(0)); - - let latest_block_height_clone = latest_block_height.clone(); - let latest_block = move |block: BlockId| { - latest_block_height_clone.store(block.height, Ordering::SeqCst); - }; - get_latest_block(latest_block); - let last_block = latest_block_height.load(Ordering::SeqCst); - println!("Latest block = {}", last_block); - - loop { - let local_lightclient = lightclient.clone(); - - let simple_callback = move |encoded_block: &[u8]| { - local_lightclient.scan_block(encoded_block); - - print!("Block Height: {}, Balance = {}\r", local_lightclient.last_scanned_height(), local_lightclient.balance()); - }; - - read_blocks(last_scanned_height, end_height, simple_callback); - - last_scanned_height = end_height + 1; - end_height = last_scanned_height + 1000 - 1; - - if last_scanned_height > last_block { - break; - } else if end_height > last_block { - end_height = last_block; - } - } -} - -pub fn read_blocks(start_height: u64, end_height: u64, c: F) - where F : Fn(&[u8]) { - // Fetch blocks - let uri: http::Uri = format!("http://127.0.0.1:9067").parse().unwrap(); - - let dst = Destination::try_from_uri(uri.clone()).unwrap(); - let connector = util::Connector::new(HttpConnector::new(4)); - let settings = client::Builder::new().http2_only(true).clone(); - let mut make_client = client::Connect::with_builder(connector, settings); - - let say_hello = make_client - .make_service(dst) - .map_err(|e| panic!("connect error: {:?}", e)) - .and_then(move |conn| { - use crate::grpc_client::client::CompactTxStreamer; - - let conn = tower_request_modifier::Builder::new() - .set_origin(uri) - .build(conn) - .unwrap(); - - // Wait until the client is ready... - CompactTxStreamer::new(conn) - .ready() - .map_err(|e| eprintln!("streaming error {:?}", e)) - }) - .and_then(move |mut client| { - let bs = BlockId{ height: start_height, hash: vec!()}; - let be = BlockId{ height: end_height, hash: vec!()}; - - let br = Request::new(BlockRange{ start: Some(bs), end: Some(be)}); - client - .get_block_range(br) - .map_err(|e| { - eprintln!("RouteChat request failed; err={:?}", e); - }) - .and_then(move |response| { - let inbound = response.into_inner(); - inbound.for_each(move |b| { - use prost::Message; - let mut encoded_buf = vec![]; - - b.encode(&mut encoded_buf).unwrap(); - c(&encoded_buf); - - Ok(()) - }) - .map_err(|e| eprintln!("gRPC inbound stream error: {:?}", e)) - }) - }); - - tokio::run(say_hello); -} - -pub fn get_latest_block(mut c : F) - where F : FnMut(BlockId) { - let uri: http::Uri = format!("http://127.0.0.1:9067").parse().unwrap(); - - let dst = Destination::try_from_uri(uri.clone()).unwrap(); - let connector = util::Connector::new(HttpConnector::new(4)); - let settings = client::Builder::new().http2_only(true).clone(); - let mut make_client = client::Connect::with_builder(connector, settings); - - let say_hello = make_client - .make_service(dst) - .map_err(|e| panic!("connect error: {:?}", e)) - .and_then(move |conn| { - use crate::grpc_client::client::CompactTxStreamer; - - let conn = tower_request_modifier::Builder::new() - .set_origin(uri) - .build(conn) - .unwrap(); - - // Wait until the client is ready... - CompactTxStreamer::new(conn).ready() - }) - .and_then(|mut client| { - - client.get_latest_block(Request::new(ChainSpec {})) - }) - .and_then(move |response| { - c(response.into_inner()); - Ok(()) - }) - .map_err(|e| { - println!("ERR = {:?}", e); - }); - - tokio::run(say_hello); } \ No newline at end of file diff --git a/rust-lightclient/src/prover.rs b/rust-lightclient/src/prover.rs index 7a3c29a..ae98694 100644 --- a/rust-lightclient/src/prover.rs +++ b/rust-lightclient/src/prover.rs @@ -2,10 +2,11 @@ use bellman::groth16::{prepare_verifying_key, Parameters, PreparedVerifyingKey}; use pairing::bls12_381::{Bls12, Fr}; -use sapling_crypto::{ +use zcash_primitives::{ jubjub::{edwards, fs::Fs, Unknown}, primitives::{Diversifier, PaymentAddress, ProofGenerationKey}, redjubjub::{PublicKey, Signature}, + transaction::components::Amount }; use zcash_primitives::{ merkle_tree::CommitmentTreeWitness, prover::TxProver, sapling::Node, @@ -114,7 +115,7 @@ impl TxProver for InMemTxProver { fn binding_sig( &self, ctx: &mut Self::SaplingProvingContext, - value_balance: i64, + value_balance: Amount, sighash: &[u8; 32], ) -> Result { ctx.binding_sig(value_balance, sighash, &JUBJUB)