From 96e6f1b5fe99633561e4fea512289f61c4cc1766 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Wed, 12 Jun 2019 17:37:27 +0100 Subject: [PATCH] Add transaction creation to WASM backend --- zcash-client-backend-wasm/Cargo.toml | 18 +++ zcash-client-backend-wasm/src/address.rs | 56 +++++++ zcash-client-backend-wasm/src/lib.rs | 193 ++++++++++++++++++++++- zcash-client-backend-wasm/src/prover.rs | 122 ++++++++++++++ 4 files changed, 386 insertions(+), 3 deletions(-) create mode 100644 zcash-client-backend-wasm/src/address.rs create mode 100644 zcash-client-backend-wasm/src/prover.rs diff --git a/zcash-client-backend-wasm/Cargo.toml b/zcash-client-backend-wasm/Cargo.toml index 5c60f5e..5924092 100644 --- a/zcash-client-backend-wasm/Cargo.toml +++ b/zcash-client-backend-wasm/Cargo.toml @@ -16,6 +16,10 @@ protobuf = "2" wasm-bindgen = "0.2" web-sys = { version = "0.3", features = ["console"] } +# We don't use these crates directly, but we add it as a dependency so we can +# enable necessary features for WASM compatibility. +rand = { version = "0.6", features = ["wasm-bindgen"] } + # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for @@ -29,6 +33,12 @@ console_error_panic_hook = { version = "0.1.1", optional = true } # Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now. wee_alloc = { version = "0.4.2", optional = true } +[dependencies.bellman] +git = "https://github.com/str4d/librustzcash.git" +branch = "demo-wasm" +default-features = false +features = ["groth16"] + [dependencies.pairing] git = "https://github.com/str4d/librustzcash.git" branch = "demo-wasm" @@ -36,14 +46,22 @@ branch = "demo-wasm" [dependencies.sapling-crypto] git = "https://github.com/str4d/librustzcash.git" branch = "demo-wasm" +default-features = false [dependencies.zcash_client_backend] git = "https://github.com/str4d/librustzcash.git" branch = "demo-wasm" +default-features = false [dependencies.zcash_primitives] git = "https://github.com/str4d/librustzcash.git" branch = "demo-wasm" +default-features = false + +[dependencies.zcash_proofs] +git = "https://github.com/str4d/librustzcash.git" +branch = "demo-wasm" +default-features = false [dev-dependencies] wasm-bindgen-test = "0.2" diff --git a/zcash-client-backend-wasm/src/address.rs b/zcash-client-backend-wasm/src/address.rs new file mode 100644 index 0000000..55c7a67 --- /dev/null +++ b/zcash-client-backend-wasm/src/address.rs @@ -0,0 +1,56 @@ +//! Structs for handling supported address types. + +use pairing::bls12_381::Bls12; +use sapling_crypto::primitives::PaymentAddress; +use zcash_client_backend::encoding::{decode_payment_address, decode_transparent_address}; +use zcash_primitives::legacy::TransparentAddress; + +use zcash_client_backend::constants::testnet::{ + B58_PUBKEY_ADDRESS_PREFIX, B58_SCRIPT_ADDRESS_PREFIX, HRP_SAPLING_PAYMENT_ADDRESS, +}; + +/// An address that funds can be sent to. +pub enum RecipientAddress { + Shielded(PaymentAddress), + Transparent(TransparentAddress), +} + +impl From> for RecipientAddress { + fn from(addr: PaymentAddress) -> Self { + RecipientAddress::Shielded(addr) + } +} + +impl From for RecipientAddress { + fn from(addr: TransparentAddress) -> Self { + RecipientAddress::Transparent(addr) + } +} + +impl RecipientAddress { + pub fn from_str(s: &str) -> Option { + if let Some(pa) = match decode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS, s) { + Ok(ret) => ret, + Err(e) => { + error!("{}", e); + return None; + } + } { + Some(RecipientAddress::Shielded(pa)) + } else if let Some(addr) = match decode_transparent_address( + &B58_PUBKEY_ADDRESS_PREFIX, + &B58_SCRIPT_ADDRESS_PREFIX, + s, + ) { + Ok(ret) => ret, + Err(e) => { + error!("{}", e); + return None; + } + } { + Some(RecipientAddress::Transparent(addr)) + } else { + None + } + } +} diff --git a/zcash-client-backend-wasm/src/lib.rs b/zcash-client-backend-wasm/src/lib.rs index dc71420..b1528ee 100644 --- a/zcash-client-backend-wasm/src/lib.rs +++ b/zcash-client-backend-wasm/src/lib.rs @@ -1,14 +1,23 @@ +macro_rules! log { + ( $( $t:tt )* ) => { + web_sys::console::log_1(&format!( $( $t )* ).into()); + }; +} + macro_rules! error { ( $( $t:tt )* ) => { web_sys::console::error_1(&format!( $( $t )* ).into()); }; } +mod address; +mod prover; mod utils; use pairing::bls12_381::Bls12; use protobuf::parse_from_bytes; -use sapling_crypto::primitives::{Note, PaymentAddress}; +use sapling_crypto::primitives::{Diversifier, Note, PaymentAddress}; +use std::cmp; use std::collections::HashMap; use std::sync::{Arc, RwLock}; use zcash_client_backend::{ @@ -19,7 +28,11 @@ use zcash_primitives::{ block::BlockHash, merkle_tree::{CommitmentTree, IncrementalWitness}, sapling::Node, - transaction::TxId, + transaction::{ + builder::{Builder, DEFAULT_FEE}, + components::Amount, + TxId, + }, zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, JUBJUB, }; @@ -32,6 +45,8 @@ use wasm_bindgen::prelude::*; #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; +const ANCHOR_OFFSET: u32 = 10; + const SAPLING_ACTIVATION_HEIGHT: i32 = 280_000; #[wasm_bindgen] @@ -47,6 +62,7 @@ struct BlockData { struct SaplingNoteData { account: usize, + diversifier: Diversifier, note: Note, witnesses: Vec>, nullifier: [u8; 32], @@ -71,8 +87,9 @@ impl SaplingNoteData { SaplingNoteData { account: output.account, + diversifier: output.to.diversifier, note: output.note, - witnesses: vec![], + witnesses: vec![witness], nullifier: nf, spent: None, } @@ -84,6 +101,32 @@ struct WalletTx { 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 + } + } +} + #[wasm_bindgen] pub struct Client { extsks: [ExtendedSpendingKey; 1], @@ -151,6 +194,30 @@ impl Client { .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) } @@ -312,4 +379,124 @@ impl Client { true } + + pub fn send_to_address( + &self, + consensus_branch_id: u32, + spend_params: &[u8], + output_params: &[u8], + to: &str, + value: u32, + ) -> Option> { + 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 => { + error!("Invalid recipient address"); + return None; + } + }; + 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 => { + error!("Cannot send funds before scanning any blocks"); + return None; + } + }; + + // Select notes to cover the target value + 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 + }) + .collect(); + + // 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 { + error!( + "Insufficient funds (have {}, need {})", + selected_value, target_value + ); + return None; + } + + // Create the transaction + 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(), + ) { + error!("Error adding note: {:?}", e); + return None; + } + } + 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) + } + } { + error!("Error adding output: {:?}", e); + return None; + } + let (tx, _) = match builder.build( + consensus_branch_id, + prover::InMemTxProver::new(spend_params, output_params), + ) { + Ok(res) => res, + Err(e) => { + error!("Error creating transaction: {:?}", e); + return None; + } + }; + log!("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/zcash-client-backend-wasm/src/prover.rs b/zcash-client-backend-wasm/src/prover.rs new file mode 100644 index 0000000..7a3c29a --- /dev/null +++ b/zcash-client-backend-wasm/src/prover.rs @@ -0,0 +1,122 @@ +//! Abstractions over the proving system and parameters for ease of use. + +use bellman::groth16::{prepare_verifying_key, Parameters, PreparedVerifyingKey}; +use pairing::bls12_381::{Bls12, Fr}; +use sapling_crypto::{ + jubjub::{edwards, fs::Fs, Unknown}, + primitives::{Diversifier, PaymentAddress, ProofGenerationKey}, + redjubjub::{PublicKey, Signature}, +}; +use zcash_primitives::{ + merkle_tree::CommitmentTreeWitness, prover::TxProver, sapling::Node, + transaction::components::GROTH_PROOF_SIZE, JUBJUB, +}; +use zcash_proofs::sapling::SaplingProvingContext; + +/// An implementation of [`TxProver`] using Sapling Spend and Output parameters provided +/// in-memory. +pub struct InMemTxProver { + spend_params: Parameters, + spend_vk: PreparedVerifyingKey, + output_params: Parameters, +} + +impl InMemTxProver { + pub fn new(spend_params: &[u8], output_params: &[u8]) -> Self { + // Deserialize params + let spend_params = Parameters::::read(spend_params, false) + .expect("couldn't deserialize Sapling spend parameters file"); + let output_params = Parameters::::read(output_params, false) + .expect("couldn't deserialize Sapling spend parameters file"); + + // Prepare verifying keys + let spend_vk = prepare_verifying_key(&spend_params.vk); + + InMemTxProver { + spend_params, + spend_vk, + output_params, + } + } +} + +impl TxProver for InMemTxProver { + type SaplingProvingContext = SaplingProvingContext; + + fn new_sapling_proving_context(&self) -> Self::SaplingProvingContext { + SaplingProvingContext::new() + } + + fn spend_proof( + &self, + ctx: &mut Self::SaplingProvingContext, + proof_generation_key: ProofGenerationKey, + diversifier: Diversifier, + rcm: Fs, + ar: Fs, + value: u64, + anchor: Fr, + witness: CommitmentTreeWitness, + ) -> Result< + ( + [u8; GROTH_PROOF_SIZE], + edwards::Point, + PublicKey, + ), + (), + > { + let (proof, cv, rk) = ctx.spend_proof( + proof_generation_key, + diversifier, + rcm, + ar, + value, + anchor, + witness, + &self.spend_params, + &self.spend_vk, + &JUBJUB, + )?; + + let mut zkproof = [0u8; GROTH_PROOF_SIZE]; + proof + .write(&mut zkproof[..]) + .expect("should be able to serialize a proof"); + + Ok((zkproof, cv, rk)) + } + + fn output_proof( + &self, + ctx: &mut Self::SaplingProvingContext, + esk: Fs, + payment_address: PaymentAddress, + rcm: Fs, + value: u64, + ) -> ([u8; GROTH_PROOF_SIZE], edwards::Point) { + let (proof, cv) = ctx.output_proof( + esk, + payment_address, + rcm, + value, + &self.output_params, + &JUBJUB, + ); + + let mut zkproof = [0u8; GROTH_PROOF_SIZE]; + proof + .write(&mut zkproof[..]) + .expect("should be able to serialize a proof"); + + (zkproof, cv) + } + + fn binding_sig( + &self, + ctx: &mut Self::SaplingProvingContext, + value_balance: i64, + sighash: &[u8; 32], + ) -> Result { + ctx.binding_sig(value_balance, sighash, &JUBJUB) + } +}