Cold wallet prepare/sign

This commit is contained in:
Hanh 2021-08-05 21:38:48 +08:00
parent d51c15c571
commit af51bf664f
6 changed files with 405 additions and 94 deletions

View File

@ -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"

View File

@ -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};

32
src/main/sign.rs Normal file
View File

@ -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(())
}

View File

@ -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(())
}

273
src/pay.rs Normal file
View File

@ -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<TxIn>,
outputs: Vec<TxOut>,
}
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<ExtendedSpendingKey>,
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<ExtendedSpendingKey>, 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<ExtendedSpendingKey>, 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::<Node>::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<B: TxBuilder>(builder: &mut B, skey: Option<ExtendedSpendingKey>, notes: &[SpendableNote], target_amount: Amount, fvk: &ExtendedFullViewingKey, recipients: &[Recipient]) -> anyhow::Result<Vec<u32>> {
let mut amount = target_amount;
amount += DEFAULT_FEE;
let target_amount_with_fee = amount;
let mut selected_notes: Vec<u32> = 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<u8> = 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<Vec<u8>> {
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::<Node>::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<String> {
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)
}

View File

@ -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<String> {
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<Recipient>, anchor_offset: u32) -> anyhow::Result<Tx> {
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, &notes, 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<String> {
let mut recipients: Vec<Recipient> = 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<u32> = 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<u8> = 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()), &notes, target_amount, &extfvk, recipients)?;
let (progress_tx, progress_rx) = mpsc::channel::<Progress>();
@ -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<Vec<SpendableNote>> {
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<Vec<Recipient>> {
let mut recipients: Vec<Recipient> = 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)]