diff --git a/Cargo.toml b/Cargo.toml index cbff7b4..12d4702 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,10 @@ path = "src/main/warp_cli.rs" name = "cli" path = "src/main/cli.rs" +[[bin]] +name = "sign" +path = "src/main/sign.rs" + [dependencies] dotenv = "0.15.0" env_logger = "0.8.4" diff --git a/src/lib.rs b/src/lib.rs index cfcd720..d0e726d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,6 +27,7 @@ mod print; mod scan; mod taddr; mod transaction; +mod pay; mod wallet; pub use crate::builder::advance_tree; @@ -37,10 +38,11 @@ pub use crate::chain::{ pub use crate::commitment::{CTree, Witness}; pub use crate::db::DbAdapter; pub use crate::hash::pedersen_hash; -pub use crate::key::is_valid_key; +pub use crate::key::{is_valid_key, decode_key}; pub use crate::lw_rpc::compact_tx_streamer_client::CompactTxStreamerClient; pub use crate::lw_rpc::*; pub use crate::mempool::MemPool; pub use crate::print::*; pub use crate::scan::{latest_height, scan_all, sync_async}; pub use crate::wallet::{Wallet, WalletBalance}; +pub use crate::pay::{sign_offline_tx, broadcast_tx, Tx}; diff --git a/src/main/sign.rs b/src/main/sign.rs new file mode 100644 index 0000000..d9bf9f9 --- /dev/null +++ b/src/main/sign.rs @@ -0,0 +1,32 @@ +use clap::Clap; +use sync::{decode_key, Tx, sign_offline_tx, NETWORK}; +use std::fs::File; +use std::io::{Read, Write}; +use zcash_client_backend::encoding::decode_extended_spending_key; +use zcash_primitives::consensus::Parameters; + +#[derive(Clap, Debug)] +struct SignArgs { + tx_filename: String, + out_filename: String, +} + +fn main() -> anyhow::Result<()> { + let key = dotenv::var("KEY").unwrap(); + let (_seed, sk, _ivk, _address) = decode_key(&key)?; + + let opts: SignArgs = SignArgs::parse(); + let sk = sk.unwrap(); + let sk = decode_extended_spending_key(NETWORK.hrp_sapling_extended_spending_key(), &sk)?.unwrap(); + + let file_name = opts.tx_filename; + let mut file = File::open(file_name)?; + let mut s = String::new(); + file.read_to_string(&mut s).unwrap(); + let tx: Tx = serde_json::from_str(&s)?; + let raw_tx = sign_offline_tx(&tx, &sk)?; + + let mut out_file = File::create(opts.out_filename)?; + writeln!(out_file, "{}", hex::encode(&raw_tx))?; + Ok(()) +} diff --git a/src/main/warp_cli.rs b/src/main/warp_cli.rs index 29b6b7b..85f2bd5 100644 --- a/src/main/warp_cli.rs +++ b/src/main/warp_cli.rs @@ -28,9 +28,9 @@ async fn test() -> anyhow::Result<()> { log::info!("Height = {}", height); }; let wallet = Wallet::new(DB_NAME, LWD_URL); - // wallet.new_account_with_key("main", &seed).unwrap(); + wallet.new_account_with_key("main", &seed).unwrap(); // wallet.new_account_with_key("test", &seed2).unwrap(); - wallet.new_account_with_key("zecpages", &ivk).unwrap(); + // wallet.new_account_with_key("zecpages", &ivk).unwrap(); let res = wallet.sync(true, ANCHOR_OFFSET, progress).await; if let Err(err) = res { @@ -46,7 +46,7 @@ async fn test() -> anyhow::Result<()> { // &address, // 50000, // "test memo", - // u64::max_value(), + // 0, // 2, // move |progress| { // println!("{}", progress.cur()); @@ -56,6 +56,18 @@ async fn test() -> anyhow::Result<()> { // .unwrap(); // println!("TXID = {}", tx_id); + let tx = wallet + .prepare_payment( + 1, + &address, + 50000, + "test memo", + 0, + 2) + .await + .unwrap(); + println!("TX = {}", tx); + Ok(()) } diff --git a/src/pay.rs b/src/pay.rs new file mode 100644 index 0000000..dda4473 --- /dev/null +++ b/src/pay.rs @@ -0,0 +1,273 @@ +use crate::{NETWORK, connect_lightwalletd, RawTransaction, get_latest_height}; +use zcash_primitives::zip32::{ExtendedSpendingKey, ExtendedFullViewingKey}; +use zcash_primitives::sapling::{Diversifier, Rseed, Node}; +use zcash_primitives::transaction::components::Amount; +use zcash_primitives::sapling::keys::OutgoingViewingKey; +use zcash_primitives::memo::MemoBytes; +use zcash_client_backend::encoding::{encode_extended_full_viewing_key, decode_extended_full_viewing_key}; +use zcash_primitives::consensus::{Parameters, Network, BlockHeight, BranchId}; +use zcash_primitives::transaction::builder::Builder; +use rand::rngs::OsRng; +use jubjub::Fr; +use zcash_primitives::merkle_tree::IncrementalWitness; +use zcash_client_backend::address::RecipientAddress; +use crate::db::SpendableNote; +use crate::wallet::Recipient; +use zcash_primitives::transaction::components::amount::DEFAULT_FEE; +use serde::{Serialize, Deserialize}; +use zcash_proofs::prover::LocalTxProver; +use tonic::Request; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Tx { + height: u32, + inputs: Vec, + outputs: Vec, +} + +impl Tx { + pub fn new(height: u32) -> Self { + Tx { + height, + inputs: vec![], + outputs: vec![], + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct TxIn { + diversifier: String, + fvk: String, + amount: u64, + rseed: String, + witness: String, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct TxOut { + addr: String, + amount: u64, + ovk: String, + memo: String, +} + +pub trait TxBuilder { + fn add_input( + &mut self, + skey: Option, + diversifier: &Diversifier, + fvk: &ExtendedFullViewingKey, + amount: Amount, + rseed: &[u8], + witness: &[u8] + ) -> anyhow::Result<()>; + fn add_t_output(&mut self, address: &str, amount: Amount) -> anyhow::Result<()>; + fn add_z_output(&mut self, address: &str, ovk: &OutgoingViewingKey, amount: Amount, memo: &MemoBytes) -> anyhow::Result<()>; +} + +pub struct ColdTxBuilder { + pub tx: Tx, +} + +impl ColdTxBuilder { + pub fn new(height: u32) -> Self { + ColdTxBuilder { + tx: Tx::new(height), + } + } +} + +impl TxBuilder for ColdTxBuilder { + fn add_input(&mut self, _skey: Option, diversifier: &Diversifier, fvk: &ExtendedFullViewingKey, amount: Amount, rseed: &[u8], witness: &[u8]) -> anyhow::Result<()> { + let tx_in = TxIn { + diversifier: hex::encode(diversifier.0), + fvk: encode_extended_full_viewing_key(NETWORK.hrp_sapling_extended_full_viewing_key(), &fvk), + amount: u64::from(amount), + rseed: hex::encode(rseed), + witness: hex::encode(witness), + }; + self.tx.inputs.push(tx_in); + Ok(()) + } + + fn add_t_output(&mut self, address: &str, amount: Amount) -> anyhow::Result<()> { + let tx_out = TxOut { + addr: address.to_string(), + amount: u64::from(amount), + ovk: String::new(), + memo: String::new(), + }; + self.tx.outputs.push(tx_out); + Ok(()) + } + + fn add_z_output(&mut self, address: &str, ovk: &OutgoingViewingKey, amount: Amount, memo: &MemoBytes) -> anyhow::Result<()> { + let tx_out = TxOut { + addr: address.to_string(), + amount: u64::from(amount), + ovk: hex::encode(ovk.0), + memo: hex::encode(memo.as_slice()), + }; + self.tx.outputs.push(tx_out); + Ok(()) + } +} + +impl TxBuilder for Builder<'_, Network, OsRng> { + fn add_input(&mut self, skey: Option, diversifier: &Diversifier, fvk: &ExtendedFullViewingKey, amount: Amount, rseed: &[u8], witness: &[u8]) -> anyhow::Result<()> { + let pa = fvk.fvk.vk.to_payment_address(diversifier.clone()).unwrap(); + let mut rseed_bytes = [0u8; 32]; + rseed_bytes.copy_from_slice(rseed); + let fr = Fr::from_bytes(&rseed_bytes).unwrap(); + let note = pa.create_note(u64::from(amount), Rseed::BeforeZip212(fr)).unwrap(); + let witness = IncrementalWitness::::read(&*witness).unwrap(); + let merkle_path = witness.path().unwrap(); + self.add_sapling_spend(skey.unwrap(), diversifier.clone(), note, merkle_path)?; + Ok(()) + } + + fn add_t_output(&mut self, address: &str, amount: Amount) -> anyhow::Result<()> { + let to_addr = RecipientAddress::decode(&NETWORK, address).ok_or(anyhow::anyhow!("Not a valid address"))?; + if let RecipientAddress::Transparent(t_address) = to_addr { + self.add_transparent_output(&t_address, amount)?; + } + Ok(()) + } + + fn add_z_output(&mut self, address: &str, ovk: &OutgoingViewingKey, amount: Amount, memo: &MemoBytes) -> anyhow::Result<()> { + let to_addr = RecipientAddress::decode(&NETWORK, address).ok_or(anyhow::anyhow!("Not a valid address"))?; + if let RecipientAddress::Shielded(pa) = to_addr { + self.add_sapling_output( + Some(ovk.clone()), + pa.clone(), + amount, + Some(memo.clone()), + )?; + } + Ok(()) + } +} + +pub fn prepare_tx(builder: &mut B, skey: Option, notes: &[SpendableNote], target_amount: Amount, fvk: &ExtendedFullViewingKey, recipients: &[Recipient]) -> anyhow::Result> { + let mut amount = target_amount; + amount += DEFAULT_FEE; + let target_amount_with_fee = amount; + let mut selected_notes: Vec = vec![]; + for n in notes.iter() { + if amount.is_positive() { + let a = amount.min( + Amount::from_u64(n.note.value) + .map_err(|_| anyhow::anyhow!("Invalid amount"))?, + ); + amount -= a; + let mut witness_bytes: Vec = vec![]; + n.witness.write(&mut witness_bytes)?; + if let Rseed::BeforeZip212(rseed) = n.note.rseed { // rseed are stored as pre-zip212 + builder.add_input( + skey.clone(), + &n.diversifier, + fvk, + Amount::from_u64(n.note.value).unwrap(), + &rseed.to_bytes(), + &witness_bytes, + )?; + selected_notes.push(n.id); + } + } + } + if amount.is_positive() { + log::info!("Not enough balance"); + anyhow::bail!( + "Not enough balance, need {} zats, missing {} zats", + u64::from(target_amount_with_fee), + u64::from(amount) + ); + } + + log::info!("Preparing tx"); + let ovk = &fvk.fvk.ovk; + + for r in recipients.iter() { + let to_addr = RecipientAddress::decode(&NETWORK, &r.address) + .ok_or(anyhow::anyhow!("Invalid address"))?; + let amount = Amount::from_u64(r.amount).unwrap(); + match &to_addr { + RecipientAddress::Shielded(_pa) => { + log::info!("Sapling output: {}", r.amount); + let memo_bytes = hex::decode(&r.memo).unwrap(); + let memo = MemoBytes::from_bytes(&memo_bytes)?; + builder.add_z_output( + &r.address, + ovk, + amount, + &memo, + ) + } + RecipientAddress::Transparent(_address) => { + builder.add_t_output(&r.address, amount) + } + }?; + } + + Ok(selected_notes) +} + +pub fn sign_offline_tx(tx: &Tx, sk: &ExtendedSpendingKey) -> anyhow::Result> { + let last_height = BlockHeight::from_u32(tx.height as u32); + let mut builder = Builder::new(NETWORK, last_height); + for txin in tx.inputs.iter() { + let mut diversifier = [0u8; 11]; + hex::decode_to_slice(&txin.diversifier, &mut diversifier)?; + let diversifier = Diversifier(diversifier); + let fvk = decode_extended_full_viewing_key(NETWORK.hrp_sapling_extended_full_viewing_key(), &txin.fvk)?.unwrap(); + let pa = fvk.fvk.vk.to_payment_address(diversifier).unwrap(); + let mut rseed_bytes = [0u8; 32]; + hex::decode_to_slice(&txin.rseed, &mut rseed_bytes)?; + let rseed = Fr::from_bytes(&rseed_bytes).unwrap(); + let note = pa.create_note(txin.amount, Rseed::BeforeZip212(rseed)).unwrap(); + let w = hex::decode(&txin.witness)?; + let witness = IncrementalWitness::::read(&*w)?; + let merkle_path = witness.path().unwrap(); + + builder.add_sapling_spend(sk.clone(), diversifier, note, merkle_path)?; + } + for txout in tx.outputs.iter() { + let recipient = RecipientAddress::decode(&NETWORK, &txout.addr).unwrap(); + let amount = Amount::from_u64(txout.amount).unwrap(); + match recipient { + RecipientAddress::Transparent(ta) => { + builder.add_transparent_output(&ta, amount)?; + } + RecipientAddress::Shielded(pa) => { + let mut ovk = [0u8; 32]; + hex::decode_to_slice(&txout.ovk, &mut ovk)?; + let ovk = OutgoingViewingKey(ovk); + let mut memo = vec![0; 512]; + let m = hex::decode(&txout.memo)?; + memo[..m.len()].copy_from_slice(&m); + let memo = MemoBytes::from_bytes(&memo)?; + builder.add_sapling_output(Some(ovk), pa, amount, Some(memo))?; + } + } + } + + let prover = LocalTxProver::with_default_location().unwrap(); + let consensus_branch_id = BranchId::for_height(&NETWORK, last_height); + let (tx, _) = builder.build(consensus_branch_id, &prover)?; + let mut raw_tx = vec![]; + tx.write(&mut raw_tx)?; + + Ok(raw_tx) +} + +pub async fn broadcast_tx(tx: &[u8], ld_url: &str) -> anyhow::Result { + let mut client = connect_lightwalletd(ld_url).await?; + let latest_height = get_latest_height(&mut client).await?; + let raw_tx = RawTransaction { + data: tx.to_vec(), + height: latest_height as u64, + }; + let rep = client.send_transaction(Request::new(raw_tx)).await?.into_inner(); + Ok(rep.error_message) +} diff --git a/src/wallet.rs b/src/wallet.rs index 3720fb2..178de2c 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -3,12 +3,10 @@ use crate::key::{decode_key, is_valid_key}; use crate::scan::ProgressCallback; use crate::taddr::{get_taddr_balance, shield_taddr}; use crate::{connect_lightwalletd, get_latest_height, BlockId, CTree, DbAdapter, NETWORK}; -use anyhow::Context; use bip39::{Language, Mnemonic}; use rand::prelude::SliceRandom; use rand::rngs::OsRng; use rand::RngCore; -use std::str::FromStr; use std::sync::{mpsc, Arc}; use tokio::sync::Mutex; use tonic::Request; @@ -18,13 +16,18 @@ use zcash_client_backend::encoding::{ }; use zcash_params::{OUTPUT_PARAMS, SPEND_PARAMS}; use zcash_primitives::consensus::{BlockHeight, BranchId, Parameters}; -use zcash_primitives::memo::Memo; use zcash_primitives::transaction::builder::{Builder, Progress}; -use zcash_primitives::transaction::components::amount::{DEFAULT_FEE, MAX_MONEY}; +use zcash_primitives::transaction::components::amount::MAX_MONEY; use zcash_primitives::transaction::components::Amount; use zcash_primitives::zip32::ExtendedFullViewingKey; use zcash_proofs::prover::LocalTxProver; use serde::Deserialize; +use crate::pay::prepare_tx; +use zcash_primitives::memo::{Memo, MemoBytes}; +use std::convert::TryFrom; +use std::str::FromStr; +use crate::db::SpendableNote; +use crate::pay::{ColdTxBuilder, Tx}; const DEFAULT_CHUNK_SIZE: u32 = 100_000; @@ -205,6 +208,32 @@ impl Wallet { self._send_payment(account, &recipients, anchor_offset, progress_callback).await } + pub async fn prepare_payment( + &self, + account: u32, + to_address: &str, + amount: u64, + memo: &str, + max_amount_per_note: u64, + anchor_offset: u32, + ) -> anyhow::Result { + let last_height = self.get_latest_height().await?; + let recipients = Self::_build_recipients(to_address, amount, max_amount_per_note, memo)?; + let tx = self._prepare_payment(account, amount, last_height, &recipients, anchor_offset)?; + let tx_str = serde_json::to_string(&tx)?; + Ok(tx_str) + } + + fn _prepare_payment(&self, account: u32, amount: u64, last_height: u32, recipients: &Vec, anchor_offset: u32) -> anyhow::Result { + let amount = Amount::from_u64(amount).unwrap(); + let ivk = self.db.get_ivk(account)?; + let extfvk = decode_extended_full_viewing_key(NETWORK.hrp_sapling_extended_full_viewing_key(), &ivk)?.unwrap(); + let notes = self._get_spendable_notes(account, &extfvk, last_height, anchor_offset)?; + let mut builder = ColdTxBuilder::new(last_height); + prepare_tx(&mut builder, None, ¬es, amount, &extfvk, recipients)?; + Ok(builder.tx) + } + pub async fn send_payment( &self, account: u32, @@ -215,25 +244,7 @@ impl Wallet { anchor_offset: u32, progress_callback: impl Fn(Progress) + Send + 'static, ) -> anyhow::Result { - let mut recipients: Vec = vec![]; - let target_amount = Amount::from_u64(amount).unwrap(); - let max_amount_per_note = if max_amount_per_note != 0 { - Amount::from_u64(max_amount_per_note).unwrap() - } else { - Amount::from_i64(MAX_MONEY).unwrap() - }; - let mut remaining_amount = target_amount; - while remaining_amount.is_positive() { - let note_amount = remaining_amount.min(max_amount_per_note); - let recipient = Recipient { - address: to_address.to_string(), - amount: u64::from(note_amount), - memo: memo.to_string(), - }; - recipients.push(recipient); - remaining_amount -= note_amount; - } - + let recipients = Self::_build_recipients(to_address, amount, max_amount_per_note, memo)?; self._send_payment(account, &recipients, anchor_offset, progress_callback).await } @@ -250,76 +261,13 @@ impl Wallet { decode_extended_spending_key(NETWORK.hrp_sapling_extended_spending_key(), &secret_key)? .unwrap(); let extfvk = ExtendedFullViewingKey::from(&skey); - let (_, change_address) = extfvk.default_address().unwrap(); - let ovk = extfvk.fvk.ovk; let last_height = self.get_latest_height().await?; - let mut builder = Builder::new(NETWORK, BlockHeight::from_u32(last_height)); - let anchor_height = self - .db - .get_last_sync_height()? - .ok_or_else(|| anyhow::anyhow!("No spendable notes"))?; - let anchor_height = anchor_height.min(last_height - anchor_offset); - log::info!("Anchor = {}", anchor_height); - let mut notes = self - .db - .get_spendable_notes(account, anchor_height, &extfvk)?; - notes.shuffle(&mut OsRng); + let notes = self._get_spendable_notes(account, &extfvk, last_height, anchor_offset)?; log::info!("Spendable notes = {}", notes.len()); - let mut amount = target_amount; - amount += DEFAULT_FEE; - let target_amount_with_fee = amount; - let mut selected_note: Vec = vec![]; - for n in notes.iter() { - if amount.is_positive() { - let a = amount.min( - Amount::from_u64(n.note.value) - .map_err(|_| anyhow::anyhow!("Invalid amount"))?, - ); - amount -= a; - let merkle_path = n.witness.path().context("Invalid Merkle Path")?; - let mut witness_bytes: Vec = vec![]; - n.witness.write(&mut witness_bytes)?; - builder.add_sapling_spend( - skey.clone(), - n.diversifier, - n.note.clone(), - merkle_path, - )?; - selected_note.push(n.id); - } - } - if amount.is_positive() { - log::info!("Not enough balance"); - anyhow::bail!( - "Not enough balance, need {} zats, missing {} zats", - u64::from(target_amount_with_fee), - u64::from(amount) - ); - } - + let mut builder = Builder::new(NETWORK, BlockHeight::from_u32(last_height)); log::info!("Preparing tx"); - builder.send_change_to(ovk, change_address); - - for r in recipients.iter() { - let to_addr = RecipientAddress::decode(&NETWORK, &r.address) - .ok_or(anyhow::anyhow!("Invalid address"))?; - let amount = Amount::from_u64(r.amount).unwrap(); - match &to_addr { - RecipientAddress::Shielded(pa) => { - log::info!("Sapling output: {}", r.amount); - builder.add_sapling_output( - Some(ovk), - pa.clone(), - amount, - Some(Memo::from_str(&r.memo)?.into()), - ) - } - RecipientAddress::Transparent(t_address) => { - builder.add_transparent_output(&t_address, amount) - } - }?; - } + let selected_notes = prepare_tx(&mut builder, Some(skey.clone()), ¬es, target_amount, &extfvk, recipients)?; let (progress_tx, progress_rx) = mpsc::channel::(); @@ -342,7 +290,7 @@ impl Wallet { let tx_id = send_transaction(&mut client, &raw_tx, last_height).await?; log::info!("Tx ID = {}", tx_id); - for id_note in selected_note.iter() { + for id_note in selected_notes.iter() { self.db.mark_spent(*id_note, 0)?; } Ok(tx_id) @@ -387,6 +335,46 @@ impl Wallet { self.ld_url = ld_url.to_string(); Ok(()) } + + fn _get_spendable_notes(&self, account: u32, extfvk: &ExtendedFullViewingKey, last_height: u32, anchor_offset: u32) -> anyhow::Result> { + let anchor_height = self + .db + .get_last_sync_height()? + .ok_or_else(|| anyhow::anyhow!("No spendable notes"))?; + let anchor_height = anchor_height.min(last_height - anchor_offset); + log::info!("Anchor = {}", anchor_height); + let mut notes = self + .db + .get_spendable_notes(account, anchor_height, extfvk)?; + notes.shuffle(&mut OsRng); + log::info!("Spendable notes = {}", notes.len()); + + Ok(notes) + } + + fn _build_recipients(to_address: &str, amount: u64, max_amount_per_note: u64, memo: &str) -> anyhow::Result> { + let mut recipients: Vec = vec![]; + let target_amount = Amount::from_u64(amount).unwrap(); + let max_amount_per_note = if max_amount_per_note != 0 { + Amount::from_u64(max_amount_per_note).unwrap() + } else { + Amount::from_i64(MAX_MONEY).unwrap() + }; + let mut remaining_amount = target_amount; + let memo = Memo::from_str(memo)?; + let memo_bytes = MemoBytes::try_from(memo)?; + while remaining_amount.is_positive() { + let note_amount = remaining_amount.min(max_amount_per_note); + let recipient = Recipient { + address: to_address.to_string(), + amount: u64::from(note_amount), + memo: hex::encode(memo_bytes.as_slice()), + }; + recipients.push(recipient); + remaining_amount -= note_amount; + } + Ok(recipients) + } } #[cfg(test)]