From 591b1fc28fb82dbfd874127837745789b400fd86 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Fri, 12 Oct 2018 18:24:25 +0100 Subject: [PATCH] Parse compact blocks to find wallet transactions --- Cargo.lock | 13 ++ zcash_client_backend/Cargo.toml | 3 + zcash_client_backend/src/lib.rs | 2 + zcash_client_backend/src/wallet.rs | 32 ++++ zcash_client_backend/src/welding_rig.rs | 194 ++++++++++++++++++++++++ 5 files changed, 244 insertions(+) create mode 100644 zcash_client_backend/src/wallet.rs create mode 100644 zcash_client_backend/src/welding_rig.rs diff --git a/Cargo.lock b/Cargo.lock index d87e43db9..75ff772a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -534,6 +534,15 @@ dependencies = [ "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rand_os" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "getrandom 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rand_xorshift" version = "0.2.0" @@ -628,10 +637,13 @@ name = "zcash_client_backend" version = "0.1.0" dependencies = [ "bech32 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "ff 0.5.0", + "hex 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "pairing 0.15.0", "protobuf 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "protobuf-codegen-pure 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_os 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "rand_xorshift 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "zcash_primitives 0.1.0", ] @@ -732,6 +744,7 @@ dependencies = [ "checksum rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853" "checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" "checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +"checksum rand_os 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a788ae3edb696cfcba1c19bfd388cc4b8c21f8a408432b199c072825084da58a" "checksum rand_xorshift 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "77d416b86801d23dde1aa643023b775c3a462efc0ed96443add11546cdf1dca8" "checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" "checksum scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b42e15e59b18a828bbf5c58ea01debb36b9b096346de35d941dcb89009f24a0d" diff --git a/zcash_client_backend/Cargo.toml b/zcash_client_backend/Cargo.toml index 9e6430b0c..e639efd09 100644 --- a/zcash_client_backend/Cargo.toml +++ b/zcash_client_backend/Cargo.toml @@ -13,6 +13,8 @@ edition = "2018" [dependencies] bech32 = "0.7" +ff = { version = "0.5.0", path = "../ff" } +hex = "0.3" pairing = { version = "0.15.0", path = "../pairing" } protobuf = "2" zcash_primitives = { version = "0.1.0", path = "../zcash_primitives" } @@ -22,6 +24,7 @@ protobuf-codegen-pure = "2" [dev-dependencies] rand_core = "0.5" +rand_os = "0.2" rand_xorshift = "0.2" [badges] diff --git a/zcash_client_backend/src/lib.rs b/zcash_client_backend/src/lib.rs index 063ab623b..87a808ced 100644 --- a/zcash_client_backend/src/lib.rs +++ b/zcash_client_backend/src/lib.rs @@ -10,3 +10,5 @@ pub mod constants; pub mod encoding; pub mod keys; pub mod proto; +pub mod wallet; +pub mod welding_rig; diff --git a/zcash_client_backend/src/wallet.rs b/zcash_client_backend/src/wallet.rs new file mode 100644 index 000000000..4e85eef2d --- /dev/null +++ b/zcash_client_backend/src/wallet.rs @@ -0,0 +1,32 @@ +//! Structs representing transaction data scanned from the block chain by a wallet or +//! light client. + +use pairing::bls12_381::{Bls12, Fr}; +use zcash_primitives::{ + jubjub::{edwards, PrimeOrder}, + transaction::TxId, +}; + +pub struct EncCiphertextFrag(pub [u8; 52]); + +/// A subset of a [`Transaction`] relevant to wallets and light clients. +/// +/// [`Transaction`]: zcash_primitives::transaction::Transaction +pub struct WalletTx { + pub txid: TxId, + pub num_spends: usize, + pub num_outputs: usize, + pub shielded_outputs: Vec, +} + +/// A subset of an [`OutputDescription`] relevant to wallets and light clients. +/// +/// [`OutputDescription`]: zcash_primitives::transaction::components::OutputDescription +pub struct WalletShieldedOutput { + pub index: usize, + pub cmu: Fr, + pub epk: edwards::Point, + pub enc_ct: EncCiphertextFrag, + pub account: usize, + pub value: u64, +} diff --git a/zcash_client_backend/src/welding_rig.rs b/zcash_client_backend/src/welding_rig.rs new file mode 100644 index 000000000..cac83ff68 --- /dev/null +++ b/zcash_client_backend/src/welding_rig.rs @@ -0,0 +1,194 @@ +//! Tools for scanning a compact representation of the Zcash block chain. + +use ff::{PrimeField, PrimeFieldRepr}; +use pairing::bls12_381::{Bls12, Fr, FrRepr}; +use zcash_primitives::{ + jubjub::{edwards, fs::Fs}, + note_encryption::try_sapling_compact_note_decryption, + transaction::TxId, + zip32::ExtendedFullViewingKey, + JUBJUB, +}; + +use crate::proto::compact_formats::{CompactBlock, CompactOutput, CompactTx}; +use crate::wallet::{EncCiphertextFrag, WalletShieldedOutput, WalletTx}; + +/// Scans a [`CompactOutput`] with a set of [`ExtendedFullViewingKey`]s. +/// +/// Returns a [`WalletShieldedOutput`] if this output belongs to any of the given +/// [`ExtendedFullViewingKey`]s. +fn scan_output( + (index, output): (usize, CompactOutput), + ivks: &[Fs], +) -> Option { + let mut repr = FrRepr::default(); + if repr.read_le(&output.cmu[..]).is_err() { + return None; + } + let cmu = match Fr::from_repr(repr) { + Ok(cmu) => cmu, + Err(_) => return None, + }; + + let epk = match edwards::Point::::read(&output.epk[..], &JUBJUB) { + Ok(p) => match p.as_prime_order(&JUBJUB) { + Some(epk) => epk, + None => return None, + }, + Err(_) => return None, + }; + + let ct = output.ciphertext; + + for (account, ivk) in ivks.iter().enumerate() { + let value = match try_sapling_compact_note_decryption(ivk, &epk, &cmu, &ct) { + Some((note, _)) => note.value, + None => continue, + }; + + // It's ours, so let's copy the ciphertext fragment and return + let mut enc_ct = EncCiphertextFrag([0u8; 52]); + enc_ct.0.copy_from_slice(&ct); + + return Some(WalletShieldedOutput { + index, + cmu, + epk, + enc_ct, + account, + value, + }); + } + None +} + +/// Scans a [`CompactTx`] with a set of [`ExtendedFullViewingKey`]s. +/// +/// Returns a [`WalletTx`] if this transaction belongs to any of the given +/// [`ExtendedFullViewingKey`]s. +fn scan_tx(tx: CompactTx, extfvks: &[ExtendedFullViewingKey]) -> Option { + let num_spends = tx.spends.len(); + let num_outputs = tx.outputs.len(); + + // Check for incoming notes + let shielded_outputs: Vec = { + let ivks: Vec<_> = extfvks.iter().map(|extfvk| extfvk.fvk.vk.ivk()).collect(); + tx.outputs + .into_iter() + .enumerate() + .filter_map(|(index, output)| scan_output((index, output), &ivks)) + .collect() + }; + + if shielded_outputs.is_empty() { + None + } else { + let mut txid = TxId([0u8; 32]); + txid.0.copy_from_slice(&tx.hash); + Some(WalletTx { + txid, + num_spends, + num_outputs, + shielded_outputs, + }) + } +} + +/// Scans a [`CompactBlock`] for transactions belonging to a set of +/// [`ExtendedFullViewingKey`]s. +/// +/// Returns a vector of [`WalletTx`]s belonging to any of the given +/// [`ExtendedFullViewingKey`]s. +pub fn scan_block(block: CompactBlock, extfvks: &[ExtendedFullViewingKey]) -> Vec { + block + .vtx + .into_iter() + .filter_map(|tx| scan_tx(tx, extfvks)) + .collect() +} + +#[cfg(test)] +mod tests { + use ff::{Field, PrimeField, PrimeFieldRepr}; + use pairing::bls12_381::Bls12; + use rand_core::RngCore; + use rand_os::OsRng; + use zcash_primitives::{ + jubjub::fs::Fs, + note_encryption::{Memo, SaplingNoteEncryption}, + primitives::Note, + transaction::components::Amount, + zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, + JUBJUB, + }; + + use super::scan_block; + use crate::proto::compact_formats::{CompactBlock, CompactOutput, CompactTx}; + + /// 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. + fn fake_compact_block( + height: i32, + extfvk: ExtendedFullViewingKey, + value: Amount, + ) -> CompactBlock { + 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::(&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 mut cmu = vec![]; + note.cm(&JUBJUB).into_repr().write_le(&mut cmu).unwrap(); + 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 cb = CompactBlock::new(); + cb.set_height(height as u64); + + 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); + cb.vtx.push(ctx); + + cb + } + + #[test] + fn scan_block_with_my_tx() { + let extsk = ExtendedSpendingKey::master(&[]); + let extfvk = ExtendedFullViewingKey::from(&extsk); + + let cb = fake_compact_block(1, extfvk.clone(), Amount::from_u64(5).unwrap()); + + let txs = scan_block(cb, &[extfvk]); + assert_eq!(txs.len(), 1); + + let tx = &txs[0]; + assert_eq!(tx.num_spends, 0); + assert_eq!(tx.num_outputs, 1); + assert_eq!(tx.shielded_outputs.len(), 1); + assert_eq!(tx.shielded_outputs[0].index, 0); + assert_eq!(tx.shielded_outputs[0].account, 0); + assert_eq!(tx.shielded_outputs[0].value, 5); + } +}