diff --git a/Cargo.lock b/Cargo.lock index ae85297..9ac6c2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -537,10 +537,13 @@ name = "zcash_client_backend" version = "0.0.0" dependencies = [ "bech32 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "ff 0.4.0", + "hex 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "pairing 0.14.2", "protobuf 2.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "protobuf-codegen-pure 2.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_os 0.2.1 (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.0.0", ] diff --git a/zcash_client_backend/Cargo.toml b/zcash_client_backend/Cargo.toml index 446c1ee..a95e5a0 100644 --- a/zcash_client_backend/Cargo.toml +++ b/zcash_client_backend/Cargo.toml @@ -8,6 +8,8 @@ edition = "2018" [dependencies] bech32 = "0.7" +ff = { path = "../ff" } +hex = "0.3" pairing = { path = "../pairing" } protobuf = "2" zcash_primitives = { path = "../zcash_primitives" } @@ -17,4 +19,5 @@ protobuf-codegen-pure = "2" [dev-dependencies] rand_core = "0.5" +rand_os = "0.2" rand_xorshift = "0.2" diff --git a/zcash_client_backend/src/lib.rs b/zcash_client_backend/src/lib.rs index cb0be13..7f2af0a 100644 --- a/zcash_client_backend/src/lib.rs +++ b/zcash_client_backend/src/lib.rs @@ -7,3 +7,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 0000000..4e85eef --- /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 0000000..26273e5 --- /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); + } +}