diff --git a/rust-lightclient/.gitignore b/rust-lightclient/.gitignore index 2c96eb1..1ec7ed7 100644 --- a/rust-lightclient/.gitignore +++ b/rust-lightclient/.gitignore @@ -1,2 +1,3 @@ target/ Cargo.lock +.vscode/ \ No newline at end of file diff --git a/rust-lightclient/Cargo.toml b/rust-lightclient/Cargo.toml index 8bdcd89..d66fedb 100644 --- a/rust-lightclient/Cargo.toml +++ b/rust-lightclient/Cargo.toml @@ -17,6 +17,40 @@ tower-hyper = "0.1" hyper = "0.12" tower-service = "0.2" tower-util = "0.1" +hex = "0.3" +protobuf = "2" + + +[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" + +[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 + [build-dependencies] tower-grpc-build = { git = "https://github.com/tower-rs/tower-grpc", features = ["tower-hyper"] } diff --git a/rust-lightclient/src/address.rs b/rust-lightclient/src/address.rs new file mode 100644 index 0000000..4d6d0a0 --- /dev/null +++ b/rust-lightclient/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) => { + eprintln!("{}", 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) => { + eprintln!("{}", e); + return None; + } + } { + Some(RecipientAddress::Transparent(addr)) + } else { + None + } + } +} diff --git a/rust-lightclient/src/lightclient.rs b/rust-lightclient/src/lightclient.rs new file mode 100644 index 0000000..3d32057 --- /dev/null +++ b/rust-lightclient/src/lightclient.rs @@ -0,0 +1,530 @@ +use std::time::SystemTime; + +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 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, +} + +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 { + 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) => BlockHash::from_slice(&hash), + 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) + } + + // TODO: This will be inaccurate if the balance exceeds a u32, but u64 -> JavaScript + // requires BigUint64Array which has limited support across browsers, and is not + // implemented in the LTS version of Node.js. For now, let's assume that no one is + // going to use a web wallet with more than ~21 TAZ. + pub fn balance(&self) -> u32 { + self.txs + .read() + .unwrap() + .values() + .map(|tx| { + tx.notes + .iter() + .map(|nd| if nd.spent.is_none() { nd.note.value } else { 0 }) + .sum::() + }) + .sum::() as u32 + } + + // TODO: This will be inaccurate if the balance exceeds a u32, but u64 -> JavaScript + // requires BigUint64Array which has limited support across browsers, and is not + // implemented in the LTS version of Node.js. For now, let's assume that no one is + // going to use a web wallet with more than ~21 TAZ. + pub fn verified_balance(&self) -> u32 { + 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::() as u32 + } + + 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"); + 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() { + nd.witnesses.push(witness.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, 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); + } + + // 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 + } + + 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 + ); + + 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(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; + } + }; + + // 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 + }) + .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 { + 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; + } + } + 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()); + + // 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 6592dc3..c8302f7 100644 --- a/rust-lightclient/src/main.rs +++ b/rust-lightclient/src/main.rs @@ -6,11 +6,48 @@ use tower_hyper::{client, util}; use tower_util::MakeService; use futures::stream::Stream; +use std::sync::Arc; + +mod lightclient; +mod address; +mod prover; + pub mod grpc_client { include!(concat!(env!("OUT_DIR"), "/cash.z.wallet.sdk.rpc.rs")); } + + pub fn main() { + 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; + + loop { + let local_lightclient = lightclient.clone(); + + let simple_callback = move |encoded_block: &[u8]| { + local_lightclient.scan_block(encoded_block); + + println!("Block Height: {}, Balance = {}", local_lightclient.last_scanned_height(), local_lightclient.balance()); + }; + + read_blocks(last_scanned_height, end_height, simple_callback); + + if end_height < 588000 { + last_scanned_height = end_height + 1; + end_height = last_scanned_height + 1000 - 1; + } + } +} + +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(); @@ -34,12 +71,13 @@ pub fn main() { .ready() .map_err(|e| eprintln!("streaming error {:?}", e)) }) - .and_then(|mut client| { + .and_then(move |mut client| { use crate::grpc_client::BlockId; use crate::grpc_client::BlockRange; + - let bs = BlockId{ height: 588300, hash: vec!()}; - let be = BlockId{ height: 588390, hash: vec!()}; + 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 @@ -47,10 +85,15 @@ pub fn main() { .map_err(|e| { eprintln!("RouteChat request failed; err={:?}", e); }) - .and_then(|response| { + .and_then(move |response| { let inbound = response.into_inner(); - inbound.for_each(|b| { - println!("RESPONSE = {:?}", b); + 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)) @@ -58,4 +101,5 @@ pub fn main() { }); tokio::run(say_hello); + println!("All done!"); } \ No newline at end of file diff --git a/rust-lightclient/src/prover.rs b/rust-lightclient/src/prover.rs new file mode 100644 index 0000000..7a3c29a --- /dev/null +++ b/rust-lightclient/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) + } +} diff --git a/rust-lightclient/src/utils.rs b/rust-lightclient/src/utils.rs new file mode 100644 index 0000000..b1d7929 --- /dev/null +++ b/rust-lightclient/src/utils.rs @@ -0,0 +1,10 @@ +pub fn set_panic_hook() { + // When the `console_error_panic_hook` feature is enabled, we can call the + // `set_panic_hook` function at least once during initialization, and then + // we will get better error messages if our code ever panics. + // + // For more details see + // https://github.com/rustwasm/console_error_panic_hook#readme + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); +}