diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 5f76fb0617..85231fa09b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -14,6 +14,14 @@ jobs: toolchain: 1.37.0 override: true + # cargo fmt does not build the code, and running it in a fresh clone of + # the codebase will fail because the protobuf code has not been generated. + - name: cargo build + uses: actions-rs/cargo@v1 + with: + command: build + args: --all + # Ensure all code has been formatted with rustfmt - run: rustup component add rustfmt - name: Check formatting diff --git a/.travis.yml b/.travis.yml index 85dfd70305..f0f9e46a39 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,8 +14,8 @@ before_script: - rustup component add rustfmt script: - - cargo fmt --all -- --check - cargo build --verbose --release --all + - cargo fmt --all -- --check - cargo test --verbose --release --all - cargo test --verbose --release --all -- --ignored diff --git a/Cargo.lock b/Cargo.lock index 7a33a24e50..fe20d7a3ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -467,6 +467,28 @@ dependencies = [ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "protobuf" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "protobuf-codegen" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "protobuf 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "protobuf-codegen-pure" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "protobuf 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "protobuf-codegen 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "quote" version = "1.0.2" @@ -512,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" @@ -557,6 +588,11 @@ dependencies = [ "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "subtle" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "syn" version = "1.0.5" @@ -606,9 +642,15 @@ 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)", + "subtle 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "zcash_primitives 0.1.0", ] @@ -700,17 +742,22 @@ dependencies = [ "checksum ppv-lite86 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e3cbf9f658cdb5000fcf6f362b8ea2ba154b9f146a61c7a20d647034c6b6561b" "checksum proc-macro-hack 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e688f31d92ffd7c1ddc57a1b4e6d773c0f2a14ee437a4b0a4f5a69c80eb221c8" "checksum proc-macro2 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e98a83a9f9b331f54b924e68a66acb1bb35cb01fb0a23645139967abefb697e8" +"checksum protobuf 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "40361836defdd5871ff7e84096c6f6444af7fc157f8ef1789f54f147687caa20" +"checksum protobuf-codegen 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "12c6abd78435445fc86898ebbd0521a68438063d4a73e23527b7134e6bf58b4a" +"checksum protobuf-codegen-pure 2.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c1646acda5319f5b28b0bff4a484324df43ddae2c0f5a3f3e63c0b26095cd600" "checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" "checksum rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d47eab0e83d9693d40f825f86948aa16eff6750ead4bdffc4ab95b8b3a7f052c" "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" "checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" "checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" "checksum sha2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b4d8bfd0e469f417657573d8451fb33d16cfe0989359b93baf3a1ffc639543d" +"checksum subtle 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ab3af2eb31c42e8f0ccf43548232556c42737e01a96db6e1777b0be108e79799" "checksum syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "66850e97125af79138385e9b88339cbcd037e3f28ceab8c5ad98e64f0f1f80bf" "checksum typenum 1.11.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6d2783fe2d6b8c1101136184eb41be8b1ad379e4657050b8aaff0c79ee7575f9" "checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" diff --git a/zcash_client_backend/.gitignore b/zcash_client_backend/.gitignore new file mode 100644 index 0000000000..7025829d91 --- /dev/null +++ b/zcash_client_backend/.gitignore @@ -0,0 +1,2 @@ +# Protobufs +src/proto/ diff --git a/zcash_client_backend/Cargo.toml b/zcash_client_backend/Cargo.toml index 7594630a30..3c8a014315 100644 --- a/zcash_client_backend/Cargo.toml +++ b/zcash_client_backend/Cargo.toml @@ -13,11 +13,19 @@ 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" +subtle = "2" zcash_primitives = { version = "0.1.0", path = "../zcash_primitives" } +[build-dependencies] +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/build.rs b/zcash_client_backend/build.rs new file mode 100644 index 0000000000..41e021444f --- /dev/null +++ b/zcash_client_backend/build.rs @@ -0,0 +1,11 @@ +use protobuf_codegen_pure; + +fn main() { + protobuf_codegen_pure::run(protobuf_codegen_pure::Args { + out_dir: "src/proto", + input: &["proto/compact_formats.proto"], + includes: &["proto"], + customize: Default::default(), + }) + .expect("protoc"); +} diff --git a/zcash_client_backend/proto/compact_formats.proto b/zcash_client_backend/proto/compact_formats.proto new file mode 100644 index 0000000000..7e1bc54127 --- /dev/null +++ b/zcash_client_backend/proto/compact_formats.proto @@ -0,0 +1,48 @@ +syntax = "proto3"; +package cash.z.wallet.sdk.rpc; +option go_package = "walletrpc"; + +// Remember that proto3 fields are all optional. A field that is not present will be set to its zero value. +// bytes fields of hashes are in canonical little-endian format. + +// CompactBlock is a packaging of ONLY the data from a block that's needed to: +// 1. Detect a payment to your shielded Sapling address +// 2. Detect a spend of your shielded Sapling notes +// 3. Update your witnesses to generate new Sapling spend proofs. +message CompactBlock { + uint32 protoVersion = 1; // the version of this wire format, for storage + uint64 height = 2; // the height of this block + bytes hash = 3; + bytes prevHash = 4; + uint32 time = 5; + bytes header = 6; // (hash, prevHash, and time) OR (full header) + repeated CompactTx vtx = 7; // compact transactions from this block +} + +message CompactTx { + // Index and hash will allow the receiver to call out to chain + // explorers or other data structures to retrieve more information + // about this transaction. + uint64 index = 1; + bytes hash = 2; + + // The transaction fee: present if server can provide. In the case of a + // stateless server and a transaction with transparent inputs, this will be + // unset because the calculation requires reference to prior transactions. + // in a pure-Sapling context, the fee will be calculable as: + // valueBalance + (sum(vPubNew) - sum(vPubOld) - sum(tOut)) + uint32 fee = 3; + + repeated CompactSpend spends = 4; + repeated CompactOutput outputs = 5; +} + +message CompactSpend { + bytes nf = 1; +} + +message CompactOutput { + bytes cmu = 1; + bytes epk = 2; + bytes ciphertext = 3; +} diff --git a/zcash_client_backend/src/lib.rs b/zcash_client_backend/src/lib.rs index 6d1d99353b..87a808ced3 100644 --- a/zcash_client_backend/src/lib.rs +++ b/zcash_client_backend/src/lib.rs @@ -9,3 +9,6 @@ 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/proto/mod.rs b/zcash_client_backend/src/proto/mod.rs new file mode 100644 index 0000000000..0ab1b6d51c --- /dev/null +++ b/zcash_client_backend/src/proto/mod.rs @@ -0,0 +1,83 @@ +//! Generated code for handling light client protobuf structs. + +use ff::{PrimeField, PrimeFieldRepr}; +use pairing::bls12_381::{Bls12, Fr, FrRepr}; +use zcash_primitives::{ + block::{BlockHash, BlockHeader}, + jubjub::{edwards, PrimeOrder}, + JUBJUB, +}; + +pub mod compact_formats; + +impl compact_formats::CompactBlock { + /// Returns the [`BlockHash`] for this block. + /// + /// # Panics + /// + /// This function will panic if [`CompactBlock.header`] is not set and + /// [`CompactBlock.hash`] is not exactly 32 bytes. + /// + /// [`CompactBlock.header`]: #structfield.header + /// [`CompactBlock.hash`]: #structfield.hash + pub fn hash(&self) -> BlockHash { + if let Some(header) = self.header() { + header.hash() + } else { + BlockHash::from_slice(&self.hash) + } + } + + /// Returns the [`BlockHash`] for this block's parent. + /// + /// # Panics + /// + /// This function will panic if [`CompactBlock.header`] is not set and + /// [`CompactBlock.prevHash`] is not exactly 32 bytes. + /// + /// [`CompactBlock.header`]: #structfield.header + /// [`CompactBlock.prevHash`]: #structfield.prevHash + pub fn prev_hash(&self) -> BlockHash { + if let Some(header) = self.header() { + header.prev_block + } else { + BlockHash::from_slice(&self.prevHash) + } + } + + /// Returns the [`BlockHeader`] for this block if present. + /// + /// A convenience method that parses [`CompactBlock.header`] if present. + /// + /// [`CompactBlock.header`]: #structfield.header + pub fn header(&self) -> Option { + if self.header.is_empty() { + None + } else { + BlockHeader::read(&self.header[..]).ok() + } + } +} + +impl compact_formats::CompactOutput { + /// Returns the note commitment for this output. + /// + /// A convenience method that parses [`CompactOutput.cmu`]. + /// + /// [`CompactOutput.cmu`]: #structfield.cmu + pub fn cmu(&self) -> Result { + let mut repr = FrRepr::default(); + repr.read_le(&self.cmu[..]).map_err(|_| ())?; + Fr::from_repr(repr).map_err(|_| ()) + } + + /// Returns the ephemeral public key for this output. + /// + /// A convenience method that parses [`CompactOutput.epk`]. + /// + /// [`CompactOutput.epk`]: #structfield.epk + pub fn epk(&self) -> Result, ()> { + let p = edwards::Point::::read(&self.epk[..], &JUBJUB).map_err(|_| ())?; + p.as_prime_order(&JUBJUB).ok_or(()) + } +} diff --git a/zcash_client_backend/src/wallet.rs b/zcash_client_backend/src/wallet.rs new file mode 100644 index 0000000000..dc46a86c7f --- /dev/null +++ b/zcash_client_backend/src/wallet.rs @@ -0,0 +1,46 @@ +//! 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}, + merkle_tree::IncrementalWitness, + primitives::{Note, PaymentAddress}, + sapling::Node, + transaction::TxId, +}; + +/// A subset of a [`Transaction`] relevant to wallets and light clients. +/// +/// [`Transaction`]: zcash_primitives::transaction::Transaction +pub struct WalletTx { + pub txid: TxId, + pub index: usize, + pub num_spends: usize, + pub num_outputs: usize, + pub shielded_spends: Vec, + pub shielded_outputs: Vec, +} + +/// A subset of a [`SpendDescription`] relevant to wallets and light clients. +/// +/// [`SpendDescription`]: zcash_primitives::transaction::components::SpendDescription +pub struct WalletShieldedSpend { + pub index: usize, + pub nf: Vec, + pub account: usize, +} + +/// 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 account: usize, + pub note: Note, + pub to: PaymentAddress, + pub is_change: bool, + pub witness: IncrementalWitness, +} diff --git a/zcash_client_backend/src/welding_rig.rs b/zcash_client_backend/src/welding_rig.rs new file mode 100644 index 0000000000..6b9ec59b26 --- /dev/null +++ b/zcash_client_backend/src/welding_rig.rs @@ -0,0 +1,399 @@ +//! Tools for scanning a compact representation of the Zcash block chain. + +use ff::PrimeField; +use std::collections::HashSet; +use subtle::{ConditionallySelectable, ConstantTimeEq, CtOption}; +use zcash_primitives::{ + jubjub::fs::Fs, + merkle_tree::{CommitmentTree, IncrementalWitness}, + note_encryption::try_sapling_compact_note_decryption, + sapling::Node, + transaction::TxId, + zip32::ExtendedFullViewingKey, +}; + +use crate::proto::compact_formats::{CompactBlock, CompactOutput}; +use crate::wallet::{WalletShieldedOutput, WalletShieldedSpend, WalletTx}; + +/// Scans a [`CompactOutput`] with a set of [`ExtendedFullViewingKey`]s. +/// +/// Returns a [`WalletShieldedOutput`] and corresponding [`IncrementalWitness`] if this +/// output belongs to any of the given [`ExtendedFullViewingKey`]s. +/// +/// The given [`CommitmentTree`] and existing [`IncrementalWitness`]es are incremented +/// with this output's commitment. +fn scan_output( + (index, output): (usize, CompactOutput), + ivks: &[Fs], + spent_from_accounts: &HashSet, + tree: &mut CommitmentTree, + existing_witnesses: &mut [&mut IncrementalWitness], + block_witnesses: &mut [&mut IncrementalWitness], + new_witnesses: &mut [&mut IncrementalWitness], +) -> Option { + let cmu = output.cmu().ok()?; + let epk = output.epk().ok()?; + let ct = output.ciphertext; + + // Increment tree and witnesses + let node = Node::new(cmu.into_repr()); + for witness in existing_witnesses { + witness.append(node).unwrap(); + } + for witness in block_witnesses { + witness.append(node).unwrap(); + } + for witness in new_witnesses { + witness.append(node).unwrap(); + } + tree.append(node).unwrap(); + + for (account, ivk) in ivks.iter().enumerate() { + let (note, to) = match try_sapling_compact_note_decryption(ivk, &epk, &cmu, &ct) { + Some(ret) => ret, + None => continue, + }; + + // A note is marked as "change" if the account that received it + // also spent notes in the same transaction. This will catch, + // for instance: + // - Change created by spending fractions of notes. + // - Notes created by consolidation transactions. + // - Notes sent from one account to itself. + let is_change = spent_from_accounts.contains(&account); + + return Some(WalletShieldedOutput { + index, + cmu, + epk, + account, + note, + to, + is_change, + witness: IncrementalWitness::from_tree(tree), + }); + } + None +} + +/// Scans a [`CompactBlock`] with a set of [`ExtendedFullViewingKey`]s. +/// +/// Returns a vector of [`WalletTx`]s belonging to any of the given +/// [`ExtendedFullViewingKey`]s, and the corresponding new [`IncrementalWitness`]es. +/// +/// The given [`CommitmentTree`] and existing [`IncrementalWitness`]es are +/// incremented appropriately. +pub fn scan_block( + block: CompactBlock, + extfvks: &[ExtendedFullViewingKey], + nullifiers: &[(&[u8], usize)], + tree: &mut CommitmentTree, + existing_witnesses: &mut [&mut IncrementalWitness], +) -> Vec { + let mut wtxs: Vec = vec![]; + let ivks: Vec<_> = extfvks.iter().map(|extfvk| extfvk.fvk.vk.ivk()).collect(); + + for tx in block.vtx.into_iter() { + let num_spends = tx.spends.len(); + let num_outputs = tx.outputs.len(); + + // Check for spent notes + // The only step that is not constant-time is the filter() at the end. + let shielded_spends: Vec<_> = tx + .spends + .into_iter() + .enumerate() + .map(|(index, spend)| { + // Find the first tracked nullifier that matches this spend, and produce + // a WalletShieldedSpend if there is a match, in constant time. + nullifiers + .iter() + .map(|&(nf, account)| CtOption::new(account as u64, nf.ct_eq(&spend.nf[..]))) + .fold(CtOption::new(0, 0.into()), |first, next| { + CtOption::conditional_select(&next, &first, first.is_some()) + }) + .map(|account| WalletShieldedSpend { + index, + nf: spend.nf, + account: account as usize, + }) + }) + .filter(|spend| spend.is_some().into()) + .map(|spend| spend.unwrap()) + .collect(); + + // Collect the set of accounts that were spent from in this transaction + let spent_from_accounts: HashSet<_> = + shielded_spends.iter().map(|spend| spend.account).collect(); + + // Check for incoming notes while incrementing tree and witnesses + let mut shielded_outputs: Vec = vec![]; + { + // Grab mutable references to new witnesses from previous transactions + // in this block so that we can update them. Scoped so we don't hold + // mutable references to wtxs for too long. + let mut block_witnesses: Vec<_> = wtxs + .iter_mut() + .map(|tx| { + tx.shielded_outputs + .iter_mut() + .map(|output| &mut output.witness) + }) + .flatten() + .collect(); + + for to_scan in tx.outputs.into_iter().enumerate() { + // Grab mutable references to new witnesses from previous outputs + // in this transaction so that we can update them. Scoped so we + // don't hold mutable references to shielded_outputs for too long. + let mut new_witnesses: Vec<_> = shielded_outputs + .iter_mut() + .map(|output| &mut output.witness) + .collect(); + + if let Some(output) = scan_output( + to_scan, + &ivks, + &spent_from_accounts, + tree, + existing_witnesses, + &mut block_witnesses, + &mut new_witnesses, + ) { + shielded_outputs.push(output); + } + } + } + + if !(shielded_spends.is_empty() && shielded_outputs.is_empty()) { + let mut txid = TxId([0u8; 32]); + txid.0.copy_from_slice(&tx.hash); + wtxs.push(WalletTx { + txid, + index: tx.index as usize, + num_spends, + num_outputs, + shielded_spends, + shielded_outputs, + }); + } + } + + wtxs +} + +#[cfg(test)] +mod tests { + use ff::{Field, PrimeField, PrimeFieldRepr}; + use pairing::bls12_381::{Bls12, Fr}; + use rand_core::RngCore; + use rand_os::OsRng; + use zcash_primitives::{ + jubjub::{fs::Fs, FixedGenerators, JubjubParams, ToUniform}, + merkle_tree::CommitmentTree, + note_encryption::{Memo, SaplingNoteEncryption}, + primitives::Note, + transaction::components::Amount, + zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, + JUBJUB, + }; + + use super::scan_block; + use crate::proto::compact_formats::{CompactBlock, CompactOutput, CompactSpend, CompactTx}; + + fn random_compact_tx(rng: &mut R) -> CompactTx { + let fake_nf = { + let mut nf = vec![0; 32]; + rng.fill_bytes(&mut nf); + nf + }; + let fake_cmu = { + let fake_cmu = Fr::random(rng); + let mut bytes = vec![]; + fake_cmu.into_repr().write_le(&mut bytes).unwrap(); + bytes + }; + let fake_epk = { + let mut buffer = vec![0; 64]; + rng.fill_bytes(&mut buffer); + let fake_esk = Fs::to_uniform(&buffer[..]); + let fake_epk = JUBJUB + .generator(FixedGenerators::SpendingKeyGenerator) + .mul(fake_esk, &JUBJUB); + let mut bytes = vec![]; + fake_epk.write(&mut bytes).unwrap(); + bytes + }; + let mut cspend = CompactSpend::new(); + cspend.set_nf(fake_nf); + let mut cout = CompactOutput::new(); + cout.set_cmu(fake_cmu); + cout.set_epk(fake_epk); + cout.set_ciphertext(vec![0; 52]); + let mut ctx = CompactTx::new(); + let mut txid = vec![0; 32]; + rng.fill_bytes(&mut txid); + ctx.set_hash(txid); + ctx.spends.push(cspend); + ctx.outputs.push(cout); + ctx + } + + /// Create a fake CompactBlock at the given height, with a transaction containing a + /// single spend of the given nullifier and a single output paying the given address. + /// Returns the CompactBlock. + fn fake_compact_block( + height: i32, + nf: [u8; 32], + extfvk: ExtendedFullViewingKey, + value: Amount, + tx_after: bool, + ) -> 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); + + // Add a random Sapling tx before ours + { + let mut tx = random_compact_tx(&mut rng); + tx.index = cb.vtx.len() as u64; + cb.vtx.push(tx); + } + + let mut cspend = CompactSpend::new(); + cspend.set_nf(nf.to_vec()); + 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.spends.push(cspend); + ctx.outputs.push(cout); + ctx.index = cb.vtx.len() as u64; + cb.vtx.push(ctx); + + // Optionally add another random Sapling tx after ours + if tx_after { + let mut tx = random_compact_tx(&mut rng); + tx.index = cb.vtx.len() as u64; + cb.vtx.push(tx); + } + + cb + } + + #[test] + fn scan_block_with_my_tx() { + let extsk = ExtendedSpendingKey::master(&[]); + let extfvk = ExtendedFullViewingKey::from(&extsk); + + let cb = fake_compact_block( + 1, + [0; 32], + extfvk.clone(), + Amount::from_u64(5).unwrap(), + false, + ); + assert_eq!(cb.vtx.len(), 2); + + let mut tree = CommitmentTree::new(); + let txs = scan_block(cb, &[extfvk], &[], &mut tree, &mut []); + assert_eq!(txs.len(), 1); + + let tx = &txs[0]; + assert_eq!(tx.index, 1); + assert_eq!(tx.num_spends, 1); + assert_eq!(tx.num_outputs, 1); + assert_eq!(tx.shielded_spends.len(), 0); + 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].note.value, 5); + + // Check that the witness root matches + assert_eq!(tx.shielded_outputs[0].witness.root(), tree.root()); + } + + #[test] + fn scan_block_with_txs_after_my_tx() { + let extsk = ExtendedSpendingKey::master(&[]); + let extfvk = ExtendedFullViewingKey::from(&extsk); + + let cb = fake_compact_block( + 1, + [0; 32], + extfvk.clone(), + Amount::from_u64(5).unwrap(), + true, + ); + assert_eq!(cb.vtx.len(), 3); + + let mut tree = CommitmentTree::new(); + let txs = scan_block(cb, &[extfvk], &[], &mut tree, &mut []); + assert_eq!(txs.len(), 1); + + let tx = &txs[0]; + assert_eq!(tx.index, 1); + assert_eq!(tx.num_spends, 1); + assert_eq!(tx.num_outputs, 1); + assert_eq!(tx.shielded_spends.len(), 0); + 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].note.value, 5); + + // Check that the witness root matches + assert_eq!(tx.shielded_outputs[0].witness.root(), tree.root()); + } + + #[test] + fn scan_block_with_my_spend() { + let extsk = ExtendedSpendingKey::master(&[]); + let extfvk = ExtendedFullViewingKey::from(&extsk); + let nf = [7; 32]; + let account = 12; + + let cb = fake_compact_block(1, nf, extfvk, Amount::from_u64(5).unwrap(), false); + assert_eq!(cb.vtx.len(), 2); + + let mut tree = CommitmentTree::new(); + let txs = scan_block(cb, &[], &[(&nf, account)], &mut tree, &mut []); + assert_eq!(txs.len(), 1); + + let tx = &txs[0]; + assert_eq!(tx.index, 1); + assert_eq!(tx.num_spends, 1); + assert_eq!(tx.num_outputs, 1); + assert_eq!(tx.shielded_spends.len(), 1); + assert_eq!(tx.shielded_outputs.len(), 0); + assert_eq!(tx.shielded_spends[0].index, 0); + assert_eq!(tx.shielded_spends[0].nf, nf); + assert_eq!(tx.shielded_spends[0].account, account); + } +} diff --git a/zcash_primitives/src/block.rs b/zcash_primitives/src/block.rs index ecb0e5b096..8432cd4c93 100644 --- a/zcash_primitives/src/block.rs +++ b/zcash_primitives/src/block.rs @@ -2,6 +2,7 @@ use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use hex; +use sha2::{Digest, Sha256}; use std::fmt; use std::io::{self, Read, Write}; use std::ops::Deref; @@ -21,14 +22,31 @@ impl fmt::Display for BlockHash { } } +impl BlockHash { + /// Constructs a [`BlockHash`] from the given slice. + /// + /// # Panics + /// + /// This function will panic if the slice is not exactly 32 bytes. + pub fn from_slice(bytes: &[u8]) -> Self { + assert_eq!(bytes.len(), 32); + let mut hash = [0; 32]; + hash.copy_from_slice(&bytes); + BlockHash(hash) + } +} + /// A Zcash block header. -pub struct BlockHeader(BlockHeaderData); +pub struct BlockHeader { + hash: BlockHash, + data: BlockHeaderData, +} impl Deref for BlockHeader { type Target = BlockHeaderData; fn deref(&self) -> &BlockHeaderData { - &self.0 + &self.data } } @@ -44,12 +62,31 @@ pub struct BlockHeaderData { } impl BlockHeaderData { - pub fn freeze(self) -> BlockHeader { - BlockHeader(self) + pub fn freeze(self) -> io::Result { + BlockHeader::from_data(self) } } impl BlockHeader { + fn from_data(data: BlockHeaderData) -> io::Result { + let mut header = BlockHeader { + hash: BlockHash([0; 32]), + data, + }; + let mut raw = vec![]; + header.write(&mut raw)?; + header + .hash + .0 + .copy_from_slice(&Sha256::digest(&Sha256::digest(&raw))); + Ok(header) + } + + /// Returns the hash of this header. + pub fn hash(&self) -> BlockHash { + self.hash + } + pub fn read(mut reader: R) -> io::Result { let version = reader.read_i32::()?; @@ -70,7 +107,7 @@ impl BlockHeader { let solution = Vector::read(&mut reader, |r| r.read_u8())?; - Ok(BlockHeader(BlockHeaderData { + BlockHeader::from_data(BlockHeaderData { version, prev_block, merkle_root, @@ -79,7 +116,7 @@ impl BlockHeader { bits, nonce, solution, - })) + }) } pub fn write(&self, mut writer: W) -> io::Result<()> { @@ -206,6 +243,10 @@ mod tests { #[test] fn header_read_write() { let header = BlockHeader::read(&HEADER_MAINNET_415000[..]).unwrap(); + assert_eq!( + format!("{}", header.hash()), + "0000000001ab37793ce771262b2ffa082519aa3fe891250a1adb43baaf856168" + ); let mut encoded = Vec::with_capacity(HEADER_MAINNET_415000.len()); header.write(&mut encoded).unwrap(); assert_eq!(&HEADER_MAINNET_415000[..], &encoded[..]);