mirror of
https://github.com/Qortal/piratewallet-light-cli.git
synced 2025-02-01 03:12:15 +00:00
Add multi block scanning
This commit is contained in:
parent
76095661cb
commit
ee71507b3d
1
rust-lightclient/.gitignore
vendored
1
rust-lightclient/.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
target/
|
target/
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
.vscode/
|
@ -17,6 +17,40 @@ tower-hyper = "0.1"
|
|||||||
hyper = "0.12"
|
hyper = "0.12"
|
||||||
tower-service = "0.2"
|
tower-service = "0.2"
|
||||||
tower-util = "0.1"
|
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]
|
[build-dependencies]
|
||||||
tower-grpc-build = { git = "https://github.com/tower-rs/tower-grpc", features = ["tower-hyper"] }
|
tower-grpc-build = { git = "https://github.com/tower-rs/tower-grpc", features = ["tower-hyper"] }
|
||||||
|
56
rust-lightclient/src/address.rs
Normal file
56
rust-lightclient/src/address.rs
Normal file
@ -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<Bls12>),
|
||||||
|
Transparent(TransparentAddress),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PaymentAddress<Bls12>> for RecipientAddress {
|
||||||
|
fn from(addr: PaymentAddress<Bls12>) -> Self {
|
||||||
|
RecipientAddress::Shielded(addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TransparentAddress> for RecipientAddress {
|
||||||
|
fn from(addr: TransparentAddress) -> Self {
|
||||||
|
RecipientAddress::Transparent(addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RecipientAddress {
|
||||||
|
pub fn from_str(s: &str) -> Option<Self> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
530
rust-lightclient/src/lightclient.rs
Normal file
530
rust-lightclient/src/lightclient.rs
Normal file
@ -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<Node>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SaplingNoteData {
|
||||||
|
account: usize,
|
||||||
|
diversifier: Diversifier,
|
||||||
|
note: Note<Bls12>,
|
||||||
|
witnesses: Vec<IncrementalWitness<Node>>,
|
||||||
|
nullifier: [u8; 32],
|
||||||
|
spent: Option<TxId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SaplingNoteData {
|
||||||
|
fn new(
|
||||||
|
extfvk: &ExtendedFullViewingKey,
|
||||||
|
output: zcash_client_backend::wallet::WalletShieldedOutput,
|
||||||
|
witness: IncrementalWitness<Node>,
|
||||||
|
) -> 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<SaplingNoteData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SpendableNote {
|
||||||
|
txid: TxId,
|
||||||
|
nullifier: [u8; 32],
|
||||||
|
diversifier: Diversifier,
|
||||||
|
note: Note<Bls12>,
|
||||||
|
witness: IncrementalWitness<Node>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SpendableNote {
|
||||||
|
fn from(txid: TxId, nd: &SaplingNoteData, anchor_offset: usize) -> Option<Self> {
|
||||||
|
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<Bls12>,
|
||||||
|
blocks: Arc<RwLock<Vec<BlockData>>>,
|
||||||
|
txs: Arc<RwLock<HashMap<TxId, WalletTx>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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::<u64>()
|
||||||
|
})
|
||||||
|
.sum::<u64>() 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::<u64>()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sum::<u64>() 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<Box<[u8]>> {
|
||||||
|
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::<u64>();
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
@ -6,11 +6,48 @@ use tower_hyper::{client, util};
|
|||||||
use tower_util::MakeService;
|
use tower_util::MakeService;
|
||||||
use futures::stream::Stream;
|
use futures::stream::Stream;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
mod lightclient;
|
||||||
|
mod address;
|
||||||
|
mod prover;
|
||||||
|
|
||||||
pub mod grpc_client {
|
pub mod grpc_client {
|
||||||
include!(concat!(env!("OUT_DIR"), "/cash.z.wallet.sdk.rpc.rs"));
|
include!(concat!(env!("OUT_DIR"), "/cash.z.wallet.sdk.rpc.rs"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pub fn main() {
|
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<F : 'static + std::marker::Send>(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 uri: http::Uri = format!("http://127.0.0.1:9067").parse().unwrap();
|
||||||
|
|
||||||
let dst = Destination::try_from_uri(uri.clone()).unwrap();
|
let dst = Destination::try_from_uri(uri.clone()).unwrap();
|
||||||
@ -34,12 +71,13 @@ pub fn main() {
|
|||||||
.ready()
|
.ready()
|
||||||
.map_err(|e| eprintln!("streaming error {:?}", e))
|
.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::BlockId;
|
||||||
use crate::grpc_client::BlockRange;
|
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)});
|
let br = Request::new(BlockRange{ start: Some(bs), end: Some(be)});
|
||||||
client
|
client
|
||||||
@ -47,10 +85,15 @@ pub fn main() {
|
|||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
eprintln!("RouteChat request failed; err={:?}", e);
|
eprintln!("RouteChat request failed; err={:?}", e);
|
||||||
})
|
})
|
||||||
.and_then(|response| {
|
.and_then(move |response| {
|
||||||
let inbound = response.into_inner();
|
let inbound = response.into_inner();
|
||||||
inbound.for_each(|b| {
|
inbound.for_each(move |b| {
|
||||||
println!("RESPONSE = {:?}", b);
|
use prost::Message;
|
||||||
|
let mut encoded_buf = vec![];
|
||||||
|
|
||||||
|
b.encode(&mut encoded_buf).unwrap();
|
||||||
|
c(&encoded_buf);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.map_err(|e| eprintln!("gRPC inbound stream error: {:?}", e))
|
.map_err(|e| eprintln!("gRPC inbound stream error: {:?}", e))
|
||||||
@ -58,4 +101,5 @@ pub fn main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
tokio::run(say_hello);
|
tokio::run(say_hello);
|
||||||
|
println!("All done!");
|
||||||
}
|
}
|
122
rust-lightclient/src/prover.rs
Normal file
122
rust-lightclient/src/prover.rs
Normal file
@ -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<Bls12>,
|
||||||
|
spend_vk: PreparedVerifyingKey<Bls12>,
|
||||||
|
output_params: Parameters<Bls12>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InMemTxProver {
|
||||||
|
pub fn new(spend_params: &[u8], output_params: &[u8]) -> Self {
|
||||||
|
// Deserialize params
|
||||||
|
let spend_params = Parameters::<Bls12>::read(spend_params, false)
|
||||||
|
.expect("couldn't deserialize Sapling spend parameters file");
|
||||||
|
let output_params = Parameters::<Bls12>::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<Bls12>,
|
||||||
|
diversifier: Diversifier,
|
||||||
|
rcm: Fs,
|
||||||
|
ar: Fs,
|
||||||
|
value: u64,
|
||||||
|
anchor: Fr,
|
||||||
|
witness: CommitmentTreeWitness<Node>,
|
||||||
|
) -> Result<
|
||||||
|
(
|
||||||
|
[u8; GROTH_PROOF_SIZE],
|
||||||
|
edwards::Point<Bls12, Unknown>,
|
||||||
|
PublicKey<Bls12>,
|
||||||
|
),
|
||||||
|
(),
|
||||||
|
> {
|
||||||
|
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<Bls12>,
|
||||||
|
rcm: Fs,
|
||||||
|
value: u64,
|
||||||
|
) -> ([u8; GROTH_PROOF_SIZE], edwards::Point<Bls12, Unknown>) {
|
||||||
|
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<Signature, ()> {
|
||||||
|
ctx.binding_sig(value_balance, sighash, &JUBJUB)
|
||||||
|
}
|
||||||
|
}
|
10
rust-lightclient/src/utils.rs
Normal file
10
rust-lightclient/src/utils.rs
Normal file
@ -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();
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user