diff --git a/Cargo.toml b/Cargo.toml index 7b2df48..827a1e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,11 +19,6 @@ name = "wallet" path = "src/main/wallet.rs" required-features = ["dotenv"] -[[bin]] -name = "tests" -path = "src/main/tests.rs" -required-features = ["dotenv"] - #[[bin]] #name = "ledger" #path = "src/main/ledger.rs" diff --git a/docs/regtest.md b/docs/regtest.md index e96560e..e8489f9 100644 --- a/docs/regtest.md +++ b/docs/regtest.md @@ -39,6 +39,7 @@ Example $ zcashd -datadir=$PWD --daemon $ zcash-cli -datadir=$PWD getinfo $ zcash-cli -datadir=$PWD z_getnewaccount +$ zcash-cli -datadir=$PWD z_getnewaddress $ zcash-cli -datadir=$PWD listaddresses $ zcash-cli -datadir=$PWD generate 200 $ zcash-cli -datadir=$PWD getbalance @@ -48,6 +49,11 @@ $ zcash-cli -datadir=$PWD generate 10 $ zcash-cli -datadir=$PWD z_gettotalbalance ``` +zcash-cli -datadir=$PWD z_sendmany "ANY_TADDR" '[{"address": "zregtestsapling12qlzvqkla5ysscxx9l4dn69m7zwjggplap4gp3x9r0mnk868whgpxc03atkj83zh3xqgz0rguq0", "amount": 62.49999}]' + +zcash-cli -datadir=$PWD z_sendmany "ANY_TADDR" '[{"address": "zregtestsapling1zdrds45f09kxhzq3ak2p6j6qj9a094tjp955f9nmk44ke5qm8xsrpncauxrx3efh76euq78nhyt", "amount": 624.99999}]' + + ## Lightwalletd - Start lightwalletd @@ -84,6 +90,15 @@ $ curl -X GET http://localhost:8000/latest_height $ curl -X GET http://localhost:8000/unified_address?t=1\&s=1\&o=1 ``` +zcash-cli -datadir=$PWD z_sendmany "zregtestsapling1zdrds45f09kxhzq3ak2p6j6qj9a094tjp955f9nmk44ke5qm8xsrpncauxrx3efh76euq78nhyt" '[ +{"address": "tmWXoSBwPoCjJCNZjw4P7heoVMcT2Ronrqq", "amount": 100}, +{"address": "zregtestsapling1qzy9wafd2axnenul6t6wav76dys6s8uatsq778mpmdvmx4k9myqxsd9m73aqdgc7gwnv53wga4j", "amount": 100}, +{"address": "uregtest1mzt5lx5s5u8kczlfr82av97kjckmfjfuq8y9849h6cl9chhdekxsm6r9dklracflqwplrnfzm5rucp5txfdm04z5myrde8y3y5rayev8", "amount": 100} +]' 1 0.00001 "AllowRevealedRecipients" + + + + zcash-cli -datadir=$PWD z_sendmany "ANY_TADDR" '[{"address": "zregtestsapling1rlf8jpvk6qymgsn6pclkpnee0u77pajpz5g7955uzrxsefc837h326rkjag7rwuhn2cyympd8jh", "amount": 6.24999}]' zcash-cli -datadir=$PWD z_sendmany "zregtestsapling1rlf8jpvk6qymgsn6pclkpnee0u77pajpz5g7955uzrxsefc837h326rkjag7rwuhn2cyympd8jh" '[{"address": "tmWXoSBwPoCjJCNZjw4P7heoVMcT2Ronrqq", "amount": 6.24997}]' zcash-cli -datadir=$PWD z_sendmany "zregtestsapling1rlf8jpvk6qymgsn6pclkpnee0u77pajpz5g7955uzrxsefc837h326rkjag7rwuhn2cyympd8jh" '[{"address": "uregtest1mzt5lx5s5u8kczlfr82av97kjckmfjfuq8y9849h6cl9chhdekxsm6r9dklracflqwplrnfzm5rucp5txfdm04z5myrde8y3y5rayev8", "amount": 6.24997}]' 1 0.00001 "AllowRevealedAmounts" diff --git a/src/api.rs b/src/api.rs index 282f423..57f86b2 100644 --- a/src/api.rs +++ b/src/api.rs @@ -5,8 +5,8 @@ pub mod historical_prices; pub mod mempool; pub mod message; pub mod payment; -pub mod payment_v2; pub mod payment_uri; +pub mod payment_v2; pub mod sync; #[cfg(feature = "dart_ffi")] diff --git a/src/api/account.rs b/src/api/account.rs index 7113392..25dad14 100644 --- a/src/api/account.rs +++ b/src/api/account.rs @@ -7,6 +7,8 @@ use crate::db::AccountData; use crate::key2::decode_key; use crate::taddr::{derive_taddr, derive_tkeys}; use crate::transaction::retrieve_tx_info; +use crate::unified::UnifiedAddressType; +use crate::zip32::derive_zip32; use crate::{connect_lightwalletd, AccountInfo, KeyPack}; use anyhow::anyhow; use bip39::{Language, Mnemonic}; @@ -16,8 +18,6 @@ use std::fs::File; use std::io::BufReader; use zcash_client_backend::encoding::{decode_extended_full_viewing_key, encode_payment_address}; use zcash_primitives::consensus::Parameters; -use crate::unified::UnifiedAddressType; -use crate::zip32::derive_zip32; /// Create a new account /// # Arguments @@ -89,8 +89,7 @@ fn new_account_with_key(coin: u8, name: &str, key: &str, index: u32) -> anyhow:: db.create_orchard(account)?; } db.store_ua_settings(account, true, true, true)?; - } - else { + } else { db.store_ua_settings(account, false, true, false)?; } Ok(account) @@ -136,7 +135,8 @@ pub fn new_diversified_address() -> anyhow::Result { let fvk = decode_extended_full_viewing_key( c.chain.network().hrp_sapling_extended_full_viewing_key(), &fvk, - ).map_err(|_| anyhow!("Bech32 Decode Error"))?; + ) + .map_err(|_| anyhow!("Bech32 Decode Error"))?; let mut diversifier_index = db.get_diversifier(c.id_account)?; diversifier_index.increment().unwrap(); let (new_diversifier_index, pa) = fvk @@ -301,13 +301,19 @@ pub async fn import_sync_data(coin: u8, file: &str) -> anyhow::Result<()> { /// * t, s, o: include transparent, sapling, orchard receivers? /// /// The address depends on the UA settings and may include transparent, sapling & orchard receivers -pub fn get_unified_address(coin: u8, id_account: u32, t: bool, s: bool, o: bool) -> anyhow::Result { +pub fn get_unified_address( + coin: u8, + id_account: u32, + t: bool, + s: bool, + o: bool, +) -> anyhow::Result { let c = CoinConfig::get(coin); let db = c.db()?; let tpe = UnifiedAddressType { transparent: t, sapling: s, - orchard: o + orchard: o, }; let address = crate::get_unified_address(c.chain.network(), &db, id_account, Some(tpe))?; // use ua settings from db Ok(address) diff --git a/src/api/fullbackup.rs b/src/api/fullbackup.rs index 2d19372..06a59c7 100644 --- a/src/api/fullbackup.rs +++ b/src/api/fullbackup.rs @@ -4,8 +4,8 @@ use crate::db::AccountBackup; use bech32::{FromBase32, ToBase32, Variant}; use chacha20poly1305::aead::{Aead, NewAead}; use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce}; -use rand::RngCore; use rand::rngs::OsRng; +use rand::RngCore; const NONCE: &[u8; 12] = b"unique nonce"; diff --git a/src/api/mempool.rs b/src/api/mempool.rs index e29a818..c11bd84 100644 --- a/src/api/mempool.rs +++ b/src/api/mempool.rs @@ -1,9 +1,9 @@ //! Access to server mempool +use crate::api::sync::get_latest_height; use anyhow::anyhow; use zcash_client_backend::encoding::decode_extended_full_viewing_key; use zcash_primitives::consensus::Parameters; -use crate::api::sync::get_latest_height; use crate::coinconfig::CoinConfig; use crate::db::AccountData; @@ -22,7 +22,8 @@ pub async fn scan() -> anyhow::Result { let fvk = decode_extended_full_viewing_key( c.chain.network().hrp_sapling_extended_full_viewing_key(), &fvk, - ).map_err(|_| anyhow!("Decode error"))?; + ) + .map_err(|_| anyhow!("Decode error"))?; let mut client = c.connect_lwd().await?; mempool .update(&mut client, height, &fvk.fvk.vk.ivk()) diff --git a/src/api/payment.rs b/src/api/payment.rs index b11b2f9..a9da03c 100644 --- a/src/api/payment.rs +++ b/src/api/payment.rs @@ -208,7 +208,7 @@ pub struct Recipient { pub max_amount_per_note: u64, } -#[derive(Deserialize)] +#[derive(Clone, Deserialize)] pub struct RecipientShort { pub address: String, pub amount: u64, @@ -243,7 +243,7 @@ impl From for RecipientMemo { address: r.address, amount: r.amount, memo: Memo::Empty, - max_amount_per_note: 0 + max_amount_per_note: 0, } } } diff --git a/src/api/payment_v2.rs b/src/api/payment_v2.rs index 3f1ef0e..29c61b6 100644 --- a/src/api/payment_v2.rs +++ b/src/api/payment_v2.rs @@ -1,5 +1,57 @@ -use crate::api::payment::RecipientShort; +use std::cmp::min; +use zcash_primitives::memo::MemoBytes; +use crate::api::payment::{RecipientMemo, RecipientShort}; +use crate::{AccountData, CoinConfig, fetch_utxos, TransactionBuilderConfig, TransactionPlan}; +use crate::note_selection::{FeeZIP327, Order}; async fn prepare_payment_v2(recipients: &[RecipientShort]) -> anyhow::Result<()> { todo!() } + +pub async fn build_tx_plan( + coin: u8, + account: u32, + last_height: u32, + recipients: &[RecipientMemo], + config: &TransactionBuilderConfig, + confirmations: u32, +) -> anyhow::Result { + let c = CoinConfig::get(coin); + let fvk = { + let db = c.db()?; + let AccountData { fvk, .. } = db.get_account_info(account)?; + fvk + }; + + let mut orders = vec![]; + let mut id_order = 0; + for r in recipients { + let mut amount = r.amount; + let max_amount_per_note = if r.max_amount_per_note == 0 { + u64::MAX + } else { + r.max_amount_per_note + }; + while amount > 0 { + let a = min(amount, max_amount_per_note); + let memo_bytes: MemoBytes = r.memo.clone().into(); + let order = Order::new(id_order, &r.address, a, memo_bytes); + orders.push(order); + amount -= a; + id_order += 1; + } + } + let utxos = fetch_utxos( + coin, + account, + last_height, + true, + confirmations, + ) + .await?; + + log::info!("UTXO: {:?}", utxos); + + let tx_plan = crate::note_selection::build_tx_plan::(&fvk, last_height, &utxos, &orders, config)?; + Ok(tx_plan) +} diff --git a/src/api/sync.rs b/src/api/sync.rs index 7ccfef3..78504c8 100644 --- a/src/api/sync.rs +++ b/src/api/sync.rs @@ -2,13 +2,13 @@ use crate::coinconfig::CoinConfig; use crate::scan::{AMProgressCallback, Progress}; +use crate::sync::CTree; use crate::{AccountData, BlockId, CompactTxStreamerClient, DbAdapter}; use std::sync::Arc; use tokio::sync::Mutex; use tonic::transport::Channel; use tonic::Request; use zcash_primitives::sapling::Note; -use crate::sync::CTree; const DEFAULT_CHUNK_SIZE: u32 = 100_000; diff --git a/src/chain.rs b/src/chain.rs index 8923140..1f28161 100644 --- a/src/chain.rs +++ b/src/chain.rs @@ -355,7 +355,10 @@ fn decrypt_notes<'a, N: Parameters>( let mut count_outputs = 0u32; let mut spends: Vec = vec![]; let mut notes: Vec = vec![]; - let vvks: Vec<_> = vks.iter().map(|vk| PreparedIncomingViewingKey::new(&vk.1.ivk)).collect(); + let vvks: Vec<_> = vks + .iter() + .map(|vk| PreparedIncomingViewingKey::new(&vk.1.ivk)) + .collect(); let mut outputs: Vec<(SaplingDomain, AccountOutput)> = vec![]; for (tx_index, vtx) in block.vtx.iter().enumerate() { for cs in vtx.spends.iter() { diff --git a/src/coinconfig.rs b/src/coinconfig.rs index 721f3b4..77d6fda 100644 --- a/src/coinconfig.rs +++ b/src/coinconfig.rs @@ -1,6 +1,6 @@ -use crate::{connect_lightwalletd, CompactTxStreamerClient, DbAdapter}; use crate::fountain::FountainCodes; use crate::mempool::MemPool; +use crate::{connect_lightwalletd, CompactTxStreamerClient, DbAdapter}; use anyhow::anyhow; use lazy_static::lazy_static; use lazycell::AtomicLazyCell; diff --git a/src/db.rs b/src/db.rs index ad861bb..5f30e23 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,27 +1,27 @@ -use crate::chain::{Nf, NfRef}; +use crate::chain::Nf; use crate::contact::Contact; +use crate::orchard::{derive_orchard_keys, OrchardKeyBytes, OrchardViewKey}; use crate::prices::Quote; +use crate::sapling::SaplingViewKey; +use crate::sync; +use crate::sync::tree::{CTree, TreeCheckpoint}; use crate::taddr::{derive_tkeys, TBalance}; use crate::transaction::{GetTransactionDetailRequest, TransactionDetails}; -use crate::sync::tree::{CTree, TreeCheckpoint, Witness}; +use crate::unified::UnifiedAddressType; +use crate::note_selection::{Source, UTXO}; +use orchard::keys::FullViewingKey; use rusqlite::Error::QueryReturnedNoRows; use rusqlite::{params, Connection, OptionalExtension, Transaction}; use serde::{Deserialize, Serialize}; use serde_with::serde_as; use std::collections::HashMap; use std::convert::TryInto; -use orchard::keys::FullViewingKey; use zcash_client_backend::encoding::decode_extended_full_viewing_key; -use zcash_params::coin::{CoinType, get_coin_chain, get_coin_id}; +use zcash_params::coin::{get_coin_chain, get_coin_id, CoinType}; use zcash_primitives::consensus::{Network, NetworkUpgrade, Parameters}; use zcash_primitives::merkle_tree::IncrementalWitness; -use zcash_primitives::sapling::{Diversifier, Node, Note, Rseed, SaplingIvk}; +use zcash_primitives::sapling::{Diversifier, Node, Note, SaplingIvk}; use zcash_primitives::zip32::{DiversifierIndex, ExtendedFullViewingKey}; -use crate::note_selection::{Source, UTXO}; -use crate::orchard::{derive_orchard_keys, OrchardKeyBytes, OrchardViewKey}; -use crate::sapling::SaplingViewKey; -use crate::sync; -use crate::unified::UnifiedAddressType; mod migration; @@ -88,11 +88,6 @@ pub struct AccountBackup { pub t_addr: Option, } -pub struct AccountSeed { - pub id_account: u32, - pub seed: String, -} - pub fn wrap_query_no_rows(name: &'static str) -> impl Fn(rusqlite::Error) -> anyhow::Error { move |err: rusqlite::Error| match err { QueryReturnedNoRows => anyhow::anyhow!("Query {} returned no rows", name), @@ -102,10 +97,7 @@ pub fn wrap_query_no_rows(name: &'static str) -> impl Fn(rusqlite::Error) -> any impl DbAdapterBuilder { pub fn build(&self) -> anyhow::Result { - DbAdapter::new( - self.coin_type, - &self.db_path, - ) + DbAdapter::new(self.coin_type, &self.db_path) } } @@ -209,11 +201,7 @@ impl DbAdapter { ) .unwrap(); let ivk = fvk.fvk.vk.ivk(); - Ok(SaplingViewKey { - account, - fvk, - ivk - }) + Ok(SaplingViewKey { account, fvk, ivk }) })?; let mut fvks = vec![]; for r in rows { @@ -224,17 +212,15 @@ impl DbAdapter { } pub fn get_orchard_fvks(&self) -> anyhow::Result> { - let mut statement = self.connection.prepare("SELECT account, fvk FROM orchard_addrs")?; + let mut statement = self + .connection + .prepare("SELECT account, fvk FROM orchard_addrs")?; let rows = statement.query_map([], |row| { let account: u32 = row.get(0)?; let fvk: Vec = row.get(1)?; let fvk: [u8; 96] = fvk.try_into().unwrap(); let fvk = FullViewingKey::from_bytes(&fvk).unwrap(); - let vk = - OrchardViewKey { - account, - fvk, - }; + let vk = OrchardViewKey { account, fvk }; Ok(vk) })?; let mut fvks = vec![]; @@ -260,8 +246,14 @@ impl DbAdapter { let tx = self.connection.transaction()?; tx.execute("DELETE FROM blocks WHERE height > ?1", params![height])?; - tx.execute("DELETE FROM sapling_tree WHERE height > ?1", params![height])?; - tx.execute("DELETE FROM orchard_tree WHERE height > ?1", params![height])?; + tx.execute( + "DELETE FROM sapling_tree WHERE height > ?1", + params![height], + )?; + tx.execute( + "DELETE FROM orchard_tree WHERE height > ?1", + params![height], + )?; tx.execute( "DELETE FROM sapling_witnesses WHERE height > ?1", params![height], @@ -324,7 +316,7 @@ impl DbAdapter { log::debug!("+transaction"); db_tx.execute( "INSERT INTO transactions(account, txid, height, timestamp, tx_index, value) - VALUES (?1, ?2, ?3, ?4, ?5, 0)", + VALUES (?1, ?2, ?3, ?4, ?5, 0) ON CONFLICT DO NOTHING", // ignore conflict when same tx has sapling + orchard outputs params![account, txid, height, timestamp, tx_index], )?; let id_tx: u32 = db_tx @@ -351,8 +343,8 @@ impl DbAdapter { note.diversifier, note.value as i64, note.rcm, note.rho, note.nf, orchard, note.spent])?; let id_note: u32 = db_tx .query_row( - "SELECT id_note FROM received_notes WHERE tx = ?1 AND output_index = ?2", - params![id_tx, note.output_index], + "SELECT id_note FROM received_notes WHERE tx = ?1 AND output_index = ?2 AND orchard = ?3", + params![id_tx, note.output_index, orchard], |row| row.get(0), ) .map_err(wrap_query_no_rows("store_received_note/id_note"))?; @@ -365,33 +357,58 @@ impl DbAdapter { height: u32, id_note: u32, connection: &Connection, - shielded_pool: &str + shielded_pool: &str, ) -> anyhow::Result<()> { log::debug!("+store_witness"); let mut bb: Vec = vec![]; witness.write(&mut bb)?; connection.execute( - &format!("INSERT INTO {}_witnesses(note, height, witness) VALUES (?1, ?2, ?3)", shielded_pool), + &format!( + "INSERT INTO {}_witnesses(note, height, witness) VALUES (?1, ?2, ?3)", + shielded_pool + ), params![id_note, height, bb], )?; log::debug!("-store_witness"); Ok(()) } - pub fn store_block_timestamp(&self, height: u32, hash: &[u8], timestamp: u32) -> anyhow::Result<()> { - self.connection.execute("INSERT INTO blocks(height, hash, timestamp) VALUES (?1,?2,?3)", params![height, hash, timestamp])?; + pub fn store_block_timestamp( + &self, + height: u32, + hash: &[u8], + timestamp: u32, + ) -> anyhow::Result<()> { + self.connection.execute( + "INSERT INTO blocks(height, hash, timestamp) VALUES (?1,?2,?3)", + params![height, hash, timestamp], + )?; Ok(()) } - pub fn store_tree(height: u32, tree: &CTree, db_tx: &Connection, shielded_pool: &str) -> anyhow::Result<()> { + pub fn store_tree( + height: u32, + tree: &CTree, + db_tx: &Connection, + shielded_pool: &str, + ) -> anyhow::Result<()> { let mut bb: Vec = vec![]; tree.write(&mut bb)?; - db_tx.execute(&format!("INSERT INTO {}_tree(height, tree) VALUES (?1,?2)", shielded_pool), params![height, &bb])?; + db_tx.execute( + &format!( + "INSERT INTO {}_tree(height, tree) VALUES (?1,?2)", + shielded_pool + ), + params![height, &bb], + )?; Ok(()) } pub fn update_transaction_with_memo(&self, details: &TransactionDetails) -> anyhow::Result<()> { - self.connection.execute("UPDATE transactions SET address = ?1, memo = ?2 WHERE id_tx = ?3", params![details.address, details.memo, details.id_tx])?; + self.connection.execute( + "UPDATE transactions SET address = ?1, memo = ?2 WHERE id_tx = ?3", + params![details.address, details.memo, details.id_tx], + )?; Ok(()) } @@ -447,13 +464,22 @@ impl DbAdapter { })) } - pub fn get_tree_by_name(&self, height: u32, shielded_pool: &str) -> anyhow::Result { - let tree = self.connection.query_row( - &format!("SELECT tree FROM {}_tree WHERE height = ?1", shielded_pool), - [height], |row| { - let tree: Vec = row.get(0)?; - Ok(tree) - }).optional()?; + pub fn get_tree_by_name( + &self, + height: u32, + shielded_pool: &str, + ) -> anyhow::Result { + let tree = self + .connection + .query_row( + &format!("SELECT tree FROM {}_tree WHERE height = ?1", shielded_pool), + [height], + |row| { + let tree: Vec = row.get(0)?; + Ok(tree) + }, + ) + .optional()?; match tree { Some(tree) => { @@ -474,7 +500,7 @@ impl DbAdapter { None => Ok(TreeCheckpoint { tree: CTree::new(), witnesses: vec![], - }) + }), } } @@ -502,9 +528,7 @@ impl DbAdapter { Ok(nfs) } - pub fn get_unspent_nullifiers( - &self, - ) -> anyhow::Result> { + pub fn get_unspent_nullifiers(&self) -> anyhow::Result> { let sql = "SELECT id_note, account, nf, value FROM received_notes WHERE spent IS NULL OR spent = 0"; let mut statement = self.connection.prepare(sql)?; let nfs_res = statement.query_map(params![], |row| { @@ -529,7 +553,11 @@ impl DbAdapter { Ok(nfs) } - pub fn get_unspent_received_notes(&self, account: u32, anchor_height: u32) -> anyhow::Result> { + pub fn get_unspent_received_notes( + &self, + account: u32, + anchor_height: u32, + ) -> anyhow::Result> { let mut notes = vec![]; let mut statement = self.connection.prepare( "SELECT id_note, diversifier, value, rcm, witness FROM received_notes r, sapling_witnesses w WHERE spent IS NULL AND account = ?2 AND rho IS NULL @@ -546,7 +574,7 @@ impl DbAdapter { id_note, diversifier: diversifier.try_into().unwrap(), rseed: rcm.try_into().unwrap(), - witness + witness, }; Ok(UTXO { source, @@ -575,7 +603,7 @@ impl DbAdapter { diversifier: diversifier.try_into().unwrap(), rseed: rcm.try_into().unwrap(), rho: rho.try_into().unwrap(), - witness + witness, }; Ok(UTXO { source, @@ -787,19 +815,47 @@ impl DbAdapter { Ok(()) } + pub fn find_account_by_fvk(&self, fvk: &str) -> anyhow::Result> { + let account = self + .connection + .query_row( + "SELECT id_account FROM accounts WHERE fvk = ?1", + params![fvk], + |row| { + let account: u32 = row.get(0)?; + Ok(account) + }, + ) + .optional()?; + Ok(account) + } + pub fn get_orchard(&self, account: u32) -> anyhow::Result> { - let key = self.connection.query_row("SELECT sk, fvk FROM orchard_addrs WHERE account = ?1", params![account], |row| { - let sk: Vec = row.get(0)?; - let fvk: Vec = row.get(1)?; - Ok(OrchardKeyBytes { - sk: sk.try_into().unwrap(), - fvk: fvk.try_into().unwrap(), - }) - }).optional()?; + let key = self + .connection + .query_row( + "SELECT sk, fvk FROM orchard_addrs WHERE account = ?1", + params![account], + |row| { + let sk: Vec = row.get(0)?; + let fvk: Vec = row.get(1)?; + Ok(OrchardKeyBytes { + sk: sk.try_into().unwrap(), + fvk: fvk.try_into().unwrap(), + }) + }, + ) + .optional()?; Ok(key) } - pub fn store_ua_settings(&self, account: u32, transparent: bool, sapling: bool, orchard: bool) -> anyhow::Result<()> { + pub fn store_ua_settings( + &self, + account: u32, + transparent: bool, + sapling: bool, + orchard: bool, + ) -> anyhow::Result<()> { self.connection.execute( "INSERT INTO ua_settings(account, transparent, sapling, orchard) VALUES (?1, ?2, ?3, ?4)", params![account, transparent, sapling, orchard], @@ -808,16 +864,20 @@ impl DbAdapter { } pub fn get_ua_settings(&self, account: u32) -> anyhow::Result { - let tpe = self.connection.query_row("SELECT transparent, sapling, orchard FROM ua_settings WHERE account = ?1", params![account], |row| { - let transparent: bool = row.get(0)?; - let sapling: bool = row.get(1)?; - let orchard: bool = row.get(2)?; - Ok(UnifiedAddressType { - transparent, - sapling, - orchard - }) - })?; + let tpe = self.connection.query_row( + "SELECT transparent, sapling, orchard FROM ua_settings WHERE account = ?1", + params![account], + |row| { + let transparent: bool = row.get(0)?; + let sapling: bool = row.get(1)?; + let orchard: bool = row.get(2)?; + Ok(UnifiedAddressType { + transparent, + sapling, + orchard, + }) + }, + )?; Ok(tpe) } @@ -1051,7 +1111,9 @@ impl DbAdapter { } pub fn get_txid_without_memo(&self) -> anyhow::Result> { - let mut stmt = self.connection.prepare("SELECT account, id_tx, height, txid FROM transactions WHERE memo IS NULL")?; + let mut stmt = self + .connection + .prepare("SELECT account, id_tx, height, txid FROM transactions WHERE memo IS NULL")?; let rows = stmt.query_map([], |row| { let account: u32 = row.get(0)?; let id_tx: u32 = row.get(1)?; @@ -1223,9 +1285,9 @@ pub struct AccountData { #[cfg(test)] mod tests { - use crate::db::{DbAdapter, DEFAULT_DB_PATH, ReceivedNote}; - use zcash_params::coin::CoinType; + use crate::db::{DbAdapter, ReceivedNote, DEFAULT_DB_PATH}; use crate::sync::{CTree, Witness}; + use zcash_params::coin::CoinType; #[test] fn test_balance() { diff --git a/src/db/migration.rs b/src/db/migration.rs index 2f0c650..0399708 100644 --- a/src/db/migration.rs +++ b/src/db/migration.rs @@ -1,6 +1,6 @@ +use crate::orchard::derive_orchard_keys; use rusqlite::{params, Connection, OptionalExtension}; use zcash_primitives::consensus::{Network, Parameters}; -use crate::orchard::derive_orchard_keys; pub fn get_schema_version(connection: &Connection) -> anyhow::Result { let version: Option = connection @@ -180,23 +180,38 @@ pub fn init_db(connection: &Connection, network: &Network) -> anyhow::Result<()> } if version < 5 { - connection.execute("CREATE TABLE orchard_addrs( + connection.execute( + "CREATE TABLE orchard_addrs( account INTEGER PRIMARY KEY, sk BLOB, - fvk BLOB NOT NULL)", [])?; - connection.execute("CREATE TABLE ua_settings( + fvk BLOB NOT NULL)", + [], + )?; + connection.execute( + "CREATE TABLE ua_settings( account INTEGER PRIMARY KEY, transparent BOOL NOT NULL, sapling BOOL NOT NULL, - orchard BOOL NOT NULL)", [])?; + orchard BOOL NOT NULL)", + [], + )?; upgrade_accounts(&connection, network)?; - connection.execute("CREATE TABLE sapling_tree( + connection.execute( + "CREATE TABLE sapling_tree( height INTEGER PRIMARY KEY, - tree BLOB NOT NULL)", [])?; - connection.execute("CREATE TABLE orchard_tree( + tree BLOB NOT NULL)", + [], + )?; + connection.execute( + "CREATE TABLE orchard_tree( height INTEGER PRIMARY KEY, - tree BLOB NOT NULL)", [])?; - connection.execute("INSERT INTO sapling_tree SELECT height, sapling_tree FROM blocks", [])?; + tree BLOB NOT NULL)", + [], + )?; + connection.execute( + "INSERT INTO sapling_tree SELECT height, sapling_tree FROM blocks", + [], + )?; connection.execute("ALTER TABLE blocks DROP sapling_tree", [])?; connection.execute( "CREATE TABLE IF NOT EXISTS new_received_notes ( @@ -217,12 +232,18 @@ pub fn init_db(connection: &Connection, network: &Network) -> anyhow::Result<()> CONSTRAINT tx_output UNIQUE (tx, orchard, output_index))", [], )?; - connection.execute("INSERT INTO new_received_notes( + connection.execute( + "INSERT INTO new_received_notes( id_note, account, position, tx, height, output_index, diversifier, value, rcm, nf, spent, excluded - ) SELECT * FROM received_notes", [])?; + ) SELECT * FROM received_notes", + [], + )?; connection.execute("DROP TABLE received_notes", [])?; - connection.execute("ALTER TABLE new_received_notes RENAME TO received_notes", [])?; + connection.execute( + "ALTER TABLE new_received_notes RENAME TO received_notes", + [], + )?; connection.execute( "CREATE TABLE IF NOT EXISTS orchard_witnesses ( id_witness INTEGER PRIMARY KEY, @@ -282,10 +303,15 @@ fn upgrade_accounts(connection: &Connection, network: &Network) -> anyhow::Resul let has_orchard = seed.is_some(); if let Some(seed) = seed { let orchard_keys = derive_orchard_keys(network.coin_type(), &seed, aindex); - connection.execute("INSERT INTO orchard_addrs(account, sk, fvk) VALUES (?1,?2,?3)", params![id_account, &orchard_keys.sk, &orchard_keys.fvk])?; + connection.execute( + "INSERT INTO orchard_addrs(account, sk, fvk) VALUES (?1,?2,?3)", + params![id_account, &orchard_keys.sk, &orchard_keys.fvk], + )?; } - connection.execute("INSERT INTO ua_settings(account, transparent, sapling, orchard) VALUES (?1,?2,?3,?4)", - params![id_account, has_transparent, true, has_orchard])?; + connection.execute( + "INSERT INTO ua_settings(account, transparent, sapling, orchard) VALUES (?1,?2,?3,?4)", + params![id_account, has_transparent, true, has_orchard], + )?; } Ok(()) } diff --git a/src/gpu/cuda.rs b/src/gpu/cuda.rs index caf71aa..43cf0e2 100644 --- a/src/gpu/cuda.rs +++ b/src/gpu/cuda.rs @@ -1,7 +1,7 @@ use crate::chain::DecryptedBlock; use crate::gpu::{collect_nf, GPUProcessor}; use crate::lw_rpc::CompactBlock; -use crate::{Hash, hash::GENERATORS_EXP}; +use crate::{hash::GENERATORS_EXP, Hash}; use anyhow::Result; use ff::BatchInverter; use jubjub::Fq; diff --git a/src/hash.rs b/src/hash.rs index 9dea9d9..767015f 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -1,3 +1,4 @@ +use crate::Hash; use ff::PrimeField; use group::{Curve, GroupEncoding}; use jubjub::{ExtendedNielsPoint, ExtendedPoint, Fr, SubgroupPoint}; @@ -6,7 +7,6 @@ use std::io::Read; use std::ops::AddAssign; use zcash_params::GENERATORS; use zcash_primitives::constants::PEDERSEN_HASH_CHUNKS_PER_GENERATOR; -use crate::Hash; lazy_static! { pub static ref GENERATORS_EXP: Vec = read_generators_bin(); diff --git a/src/key2.rs b/src/key2.rs index e64d7c1..76f004f 100644 --- a/src/key2.rs +++ b/src/key2.rs @@ -41,11 +41,11 @@ pub fn is_valid_key(coin: u8, key: &str) -> i8 { if Mnemonic::from_phrase(key, Language::English).is_ok() { return 0; } - if decode_extended_spending_key(network.hrp_sapling_extended_spending_key(), key).is_ok() - { + if decode_extended_spending_key(network.hrp_sapling_extended_spending_key(), key).is_ok() { return 1; } - if decode_extended_full_viewing_key(network.hrp_sapling_extended_full_viewing_key(), key).is_ok() + if decode_extended_full_viewing_key(network.hrp_sapling_extended_full_viewing_key(), key) + .is_ok() { return 2; } @@ -61,7 +61,6 @@ pub fn is_valid_address(coin: u8, address: &str) -> bool { recipient.is_some() } - fn derive_secret_key( network: &Network, mnemonic: &Mnemonic, diff --git a/src/lib.rs b/src/lib.rs index 7ac19fd..f0c531d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,9 +80,9 @@ mod contact; mod db; mod fountain; mod hash; -mod sync; -mod sapling; mod orchard; +mod sapling; +mod sync; // mod key; mod key2; mod mempool; @@ -90,9 +90,9 @@ mod misc; mod pay; mod prices; // mod print; +mod note_selection; mod scan; mod taddr; -mod note_selection; mod transaction; mod unified; // mod ua; @@ -115,15 +115,11 @@ mod ledger { } } -pub use crate::chain::{ - connect_lightwalletd, get_best_server, - ChainError, -}; +pub use crate::chain::{connect_lightwalletd, get_best_server, ChainError}; pub use crate::coinconfig::{ - CoinConfig, init_coin, set_active, set_active_account, set_coin_lwd_url, - COIN_CONFIG, + init_coin, set_active, set_active_account, set_coin_lwd_url, CoinConfig, COIN_CONFIG, }; -pub use crate::db::{AccountData, AccountInfo, AccountRec, DbAdapter, TxRec, DbAdapterBuilder}; +pub use crate::db::{AccountData, AccountInfo, AccountRec, DbAdapter, DbAdapterBuilder, TxRec}; pub use crate::fountain::{FountainCodes, RaptorQDrops}; // pub use crate::key::KeyHelpers; pub use crate::lw_rpc::compact_tx_streamer_client::CompactTxStreamerClient; @@ -132,7 +128,13 @@ pub use crate::pay::{broadcast_tx, Tx, TxIn, TxOut}; pub use zip32::KeyPack; // pub use crate::wallet::{decrypt_backup, encrypt_backup, RecipientMemo, Wallet, WalletBalance}; -pub use unified::{get_unified_address, decode_unified_address}; +pub use note_selection::{ + build_tx_plan, build_tx, get_secret_keys, + TransactionPlan, TransactionBuilderConfig, + TxBuilderContext, + fetch_utxos, +}; +pub use unified::{decode_unified_address, get_unified_address}; #[cfg(feature = "ledger_sapling")] pub use crate::ledger::sapling::build_tx_ledger; @@ -150,3 +152,4 @@ pub fn init_test() { init_coin(0, "./zec.db").unwrap(); set_coin_lwd_url(0, "http://127.0.0.1:9067"); } + diff --git a/src/main/opt.rs b/src/main/opt.rs new file mode 100644 index 0000000..7e8ef63 --- /dev/null +++ b/src/main/opt.rs @@ -0,0 +1,16 @@ +use thiserror::Error; +use warp_api_ffi::{fetch_utxos, init_test, note_select_with_fee_v2, TransactionBuilderConfig, TransactionPlan}; + +#[tokio::main] +async fn main() { + init_test(); + + let config = TransactionBuilderConfig::new(); + + let utxos = fetch_utxos(0, 1, 220, true, 0).await.unwrap(); + let mut orders = vec![]; + + note_select_with_fee_v2("", 0, &utxos, &mut orders, &config).unwrap(); +} + + diff --git a/src/main/rpc.rs b/src/main/rpc.rs index e337b5b..9171941 100644 --- a/src/main/rpc.rs +++ b/src/main/rpc.rs @@ -3,6 +3,7 @@ extern crate rocket; use anyhow::anyhow; use lazy_static::lazy_static; +use rand::rngs::OsRng; use rocket::fairing::AdHoc; use rocket::http::Status; use rocket::response::Responder; @@ -13,9 +14,9 @@ use std::fs::File; use std::io::{BufReader, Read}; use std::sync::Mutex; use thiserror::Error; -use warp_api_ffi::api::payment::{Recipient, RecipientMemo}; +use warp_api_ffi::api::payment::{Recipient, RecipientMemo, RecipientShort}; use warp_api_ffi::api::payment_uri::PaymentURI; -use warp_api_ffi::{get_best_server, AccountData, AccountInfo, AccountRec, CoinConfig, KeyPack, Tx, TxRec, RaptorQDrops}; +use warp_api_ffi::{build_tx, get_best_server, get_secret_keys, AccountData, AccountInfo, AccountRec, CoinConfig, KeyPack, RaptorQDrops, TransactionPlan, Tx, TxRec, TransactionBuilderConfig, TxBuilderContext}; lazy_static! { static ref SYNC_CANCELED: Mutex = Mutex::new(false); @@ -101,6 +102,8 @@ async fn main() -> anyhow::Result<()> { merge_data, derive_keys, instant_sync, + get_tx_plan, + build_from_plan, ], ) .attach(AdHoc::config::()) @@ -182,7 +185,13 @@ pub fn get_address() -> Result { #[get("/unified_address?&&")] pub fn get_unified_address(t: u8, s: u8, o: u8) -> Result { let c = CoinConfig::get_active(); - let address = warp_api_ffi::api::account::get_unified_address(c.coin, c.id_account, t != 0, s != 0, o != 0)?; + let address = warp_api_ffi::api::account::get_unified_address( + c.coin, + c.id_account, + t != 0, + s != 0, + o != 0, + )?; Ok(address) } @@ -292,6 +301,60 @@ pub async fn broadcast_tx(tx_hex: String) -> Result { Ok(tx_id) } +#[post( + "/get_tx_plan?", + data = "" +)] +pub async fn get_tx_plan( + confirmations: u32, + recipients: Json>, +) -> Result, Error> { + let c = CoinConfig::get_active(); + let coin = c.coin; + let account = c.id_account; + let last_height = warp_api_ffi::api::sync::get_latest_height().await?; + let change_address = + warp_api_ffi::api::account::get_unified_address(coin, account, true, true, true)?; + let recipients: Vec<_> = recipients + .iter() + .map(|r| RecipientMemo::from(r.clone())) + .collect(); + let config = TransactionBuilderConfig::new(&change_address); + + let plan = warp_api_ffi::api::payment_v2::build_tx_plan( + coin, + account, + last_height, + &recipients, + &config, + confirmations, + ) + .await?; + Ok(Json(plan)) +} + +#[post("/build_from_plan", data = "")] +pub async fn build_from_plan(tx_plan: Json) -> Result { + let c = CoinConfig::get_active(); + let fvk = { + let db = c.db()?; + let AccountData { fvk, .. } = db.get_account_info(c.id_account)?; + fvk + }; + + if fvk != tx_plan.fvk { + return Err(Error::Other(anyhow::anyhow!( + "Account does not match transaction" + ))); + } + + let keys = get_secret_keys(c.coin, c.id_account)?; + let context = TxBuilderContext::from_height(c.coin, tx_plan.height)?; + let tx = build_tx(c.chain.network(), &keys, &tx_plan, context, OsRng).unwrap(); + let tx = hex::encode(&tx); + Ok(tx) +} + #[get("/new_diversified_address")] pub fn new_diversified_address() -> Result { let address = warp_api_ffi::api::account::new_diversified_address()?; diff --git a/src/mempool.rs b/src/mempool.rs index d8d5504..4086e1a 100644 --- a/src/mempool.rs +++ b/src/mempool.rs @@ -6,7 +6,9 @@ use tonic::Request; use crate::coinconfig::CoinConfig; use zcash_primitives::consensus::BlockHeight; -use zcash_primitives::sapling::note_encryption::{PreparedIncomingViewingKey, try_sapling_compact_note_decryption}; +use zcash_primitives::sapling::note_encryption::{ + try_sapling_compact_note_decryption, PreparedIncomingViewingKey, +}; use zcash_primitives::sapling::SaplingIvk; const DEFAULT_EXCLUDE_LEN: u8 = 1; diff --git a/src/note_selection.rs b/src/note_selection.rs index a37796a..604256a 100644 --- a/src/note_selection.rs +++ b/src/note_selection.rs @@ -1,48 +1,53 @@ +use std::str::FromStr; +pub use crate::note_selection::TransactionBuilderError::TxTooComplex; +pub use crate::note_selection::types::{ + UTXO, Order, RecipientShort, TransactionBuilderConfig, TransactionPlan, + Source, Destination }; +pub use utxo::fetch_utxos; +pub use builder::{TxBuilderContext, get_secret_keys, build_tx}; +pub use optimize::build_tx_plan; +pub use fee::{FeeCalculator, FeeZIP327}; + +use thiserror::Error; +use zcash_primitives::memo::Memo; +use ua::decode; +use optimize::{allocate_funds, fill, group_orders, outputs_for_change, select_inputs, sum_utxos}; +use crate::api::payment::Recipient; + +#[derive(Error, Debug)] +pub enum TransactionBuilderError { + #[error("Not enough funds")] + NotEnoughFunds, + #[error("Tx too complex")] + TxTooComplex, + #[error(transparent)] + Other(#[from] anyhow::Error), +} + +pub type Result = std::result::Result; + mod types; -mod fee; +mod ser; +mod ua; mod utxo; -mod fill; -mod select; +mod optimize; +mod fee; mod builder; +const MAX_ATTEMPTS: usize = 10; + +pub fn recipients_to_orders(recipients: &[Recipient]) -> Result> { + let orders: Result> = recipients.iter().enumerate().map(|(i, r)| { + let destinations = decode(&r.address)?; + Ok::<_, TransactionBuilderError>(Order { + id: i as u32, + destinations, + amount: r.amount, + memo: Memo::from_str(&r.memo).unwrap().into(), + }) + }).collect(); + Ok(orders?) +} + #[cfg(test)] mod tests; - -use std::cmp::min; -use zcash_primitives::memo::MemoBytes; -pub use types::{Source, Destination, PrivacyPolicy, Pool, UTXO, NoteSelectConfig}; -pub use utxo::fetch_utxos; -pub use fill::decode; -pub use select::note_select_with_fee; -pub use fee::{FeeZIP327, FeeFlat}; -use crate::api::payment::RecipientMemo; -use crate::note_selection::types::TransactionPlan; - -async fn prepare_multi_payment( - coin: u8, - account: u32, - last_height: u32, - recipients: &[RecipientMemo], - config: &NoteSelectConfig, - anchor_offset: u32) -> anyhow::Result -{ - let mut orders = vec![]; - let mut id_order = 0; - for r in recipients { - let mut amount = r.amount; - let max_amount_per_note = if r.max_amount_per_note == 0 { u64::MAX } else { r.max_amount_per_note }; - while amount > 0 { - let a = min(amount, max_amount_per_note); - let memo_bytes: MemoBytes = r.memo.clone().into(); - let order = decode(id_order, &r.address, a, memo_bytes)?; - orders.push(order); - amount -= a; - id_order += 1; - } - } - let utxos = fetch_utxos(coin, account, last_height, config.use_transparent, anchor_offset).await?; - - let tx_plan = note_select_with_fee::(&utxos, &mut orders, config)?; - - Ok(tx_plan) -} diff --git a/src/note_selection/builder.rs b/src/note_selection/builder.rs index 26ad308..7cdfa96 100644 --- a/src/note_selection/builder.rs +++ b/src/note_selection/builder.rs @@ -1,39 +1,41 @@ -use std::str::FromStr; +use crate::coinconfig::get_prover; +use super::types::*; +use super::{decode}; +use crate::orchard::{get_proving_key, OrchardHasher, ORCHARD_ROOTS}; +use crate::sapling::{SaplingHasher, SAPLING_ROOTS}; +use crate::sync::tree::TreeCheckpoint; +use crate::sync::Witness; +use crate::{broadcast_tx, init_coin, set_active, set_coin_lwd_url, AccountData, CoinConfig}; use anyhow::anyhow; use jubjub::Fr; -use orchard::{Address, Anchor, Bundle}; -use orchard::keys::{FullViewingKey, Scope, SpendAuthorizingKey, SpendingKey}; -use secp256k1::{All, PublicKey, Secp256k1, SecretKey}; -use zcash_primitives::consensus::{BlockHeight, BranchId, Network, Parameters}; -use zcash_primitives::transaction::builder::Builder; use orchard::builder::Builder as OrchardBuilder; use orchard::bundle::Flags; +use orchard::keys::{FullViewingKey, Scope, SpendAuthorizingKey, SpendingKey}; use orchard::note::Nullifier; use orchard::value::NoteValue; -use rand::{CryptoRng, RngCore}; +use orchard::{Address, Anchor, Bundle}; use rand::rngs::OsRng; +use rand::{CryptoRng, RngCore}; use ripemd::{Digest, Ripemd160}; +use secp256k1::{All, PublicKey, Secp256k1, SecretKey}; use sha2::Sha256; +use std::str::FromStr; use zcash_client_backend::encoding::decode_extended_spending_key; +use zcash_primitives::consensus::{BlockHeight, BranchId, Network, Parameters}; use zcash_primitives::legacy::TransparentAddress; use zcash_primitives::memo::MemoBytes; use zcash_primitives::merkle_tree::IncrementalWitness; -use zcash_primitives::sapling::{Diversifier, Node, PaymentAddress, Rseed}; use zcash_primitives::sapling::prover::TxProver; +use zcash_primitives::sapling::{Diversifier, Node, PaymentAddress, Rseed}; +use zcash_primitives::transaction::builder::Builder; use zcash_primitives::transaction::components::{Amount, OutPoint, TxOut}; -use zcash_primitives::transaction::{Transaction, TransactionData, TxVersion}; use zcash_primitives::transaction::sighash::SignableInput; use zcash_primitives::transaction::sighash_v5::v5_signature_hash; use zcash_primitives::transaction::txid::TxIdDigester; +use zcash_primitives::transaction::{Transaction, TransactionData, TxVersion}; use zcash_primitives::zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}; -use crate::{AccountData, broadcast_tx, CoinConfig, init_coin, set_active, set_coin_lwd_url}; -use crate::coinconfig::get_prover; -use crate::note_selection::types::TransactionPlan; -use crate::note_selection::{decode, Destination, FeeFlat, fetch_utxos, note_select_with_fee, NoteSelectConfig, PrivacyPolicy, Source}; -use crate::orchard::{get_proving_key, ORCHARD_ROOTS, OrchardHasher}; -use crate::sapling::{SAPLING_ROOTS, SaplingHasher}; -use crate::sync::tree::TreeCheckpoint; -use crate::sync::Witness; +use crate::note_selection::fee::FeeFlat; +use crate::note_selection::{build_tx_plan, fetch_utxos}; pub struct SecretKeys { pub transparent: Option, @@ -41,24 +43,24 @@ pub struct SecretKeys { pub orchard: Option, } -pub struct Context { +pub struct TxBuilderContext { pub height: u32, pub sapling_anchor: [u8; 32], pub orchard_anchor: [u8; 32], } -impl Context { +impl TxBuilderContext { pub fn from_height(coin: u8, height: u32) -> anyhow::Result { let c = CoinConfig::get(coin); let db = c.db.as_ref().unwrap(); let db = db.lock().unwrap(); let TreeCheckpoint { tree, .. } = db.get_tree_by_name(height, "sapling")?; let hasher = SaplingHasher {}; - let sapling_anchor= tree.root(32, &SAPLING_ROOTS, &hasher); + let sapling_anchor = tree.root(32, &SAPLING_ROOTS, &hasher); let TreeCheckpoint { tree, .. } = db.get_tree_by_name(height, "orchard")?; let hasher = OrchardHasher::new(); - let orchard_anchor= tree.root(32, &ORCHARD_ROOTS, &hasher); - let context = Context { + let orchard_anchor = tree.root(32, &ORCHARD_ROOTS, &hasher); + let context = TxBuilderContext { height, sapling_anchor, orchard_anchor, @@ -69,8 +71,13 @@ impl Context { const EXPIRY_HEIGHT: u32 = 50; -// TODO: Remove unwrap() -pub fn build_tx(network: &Network, skeys: &SecretKeys, plan: &TransactionPlan, context: Context, mut rng: impl RngCore + CryptoRng + Clone) -> anyhow::Result> { +pub fn build_tx( + network: &Network, + skeys: &SecretKeys, + plan: &TransactionPlan, + context: TxBuilderContext, + mut rng: impl RngCore + CryptoRng + Clone, +) -> anyhow::Result> { let secp = Secp256k1::::new(); let transparent_address = skeys.transparent.map(|tkey| { let pub_key = PublicKey::from_secret_key(&secp, &tkey); @@ -94,7 +101,9 @@ pub fn build_tx(network: &Network, skeys: &SecretKeys, plan: &TransactionPlan, c let mut has_orchard = false; let mut builder = Builder::new(*network, BlockHeight::from_u32(context.height)); - let anchor: Anchor = orchard::tree::MerkleHashOrchard::from_bytes(&context.orchard_anchor).unwrap().into(); + let anchor: Anchor = orchard::tree::MerkleHashOrchard::from_bytes(&context.orchard_anchor) + .unwrap() + .into(); let mut orchard_builder = OrchardBuilder::new(Flags::from_parts(true, true), anchor); for spend in plan.spends.iter() { match &spend.source { @@ -102,11 +111,18 @@ pub fn build_tx(network: &Network, skeys: &SecretKeys, plan: &TransactionPlan, c let utxo = OutPoint::new(*txid, *index); let coin = TxOut { value: Amount::from_u64(spend.amount).unwrap(), - script_pubkey: transparent_address.ok_or(anyhow!("No transparent key")).map(|ta| ta.script())?, + script_pubkey: transparent_address + .ok_or(anyhow!("No transparent key")) + .map(|ta| ta.script())?, }; builder.add_transparent_input(skeys.transparent.unwrap(), utxo, coin)?; } - Source::Sapling { diversifier, rseed, witness, .. } => { + Source::Sapling { + diversifier, + rseed, + witness, + .. + } => { let diversifier = Diversifier(*diversifier); let sapling_address = sapling_fvk.fvk.vk.to_payment_address(diversifier).unwrap(); let rseed = Rseed::BeforeZip212(Fr::from_bytes(rseed).unwrap()); @@ -115,21 +131,36 @@ pub fn build_tx(network: &Network, skeys: &SecretKeys, plan: &TransactionPlan, c let merkle_path = witness.path().unwrap(); builder.add_sapling_spend(skeys.sapling.clone(), diversifier, note, merkle_path)?; } - Source::Orchard { id_note, diversifier, rho, rseed, witness} => { + Source::Orchard { + id_note, + diversifier, + rho, + rseed, + witness, + } => { has_orchard = true; let diversifier = orchard::keys::Diversifier::from_bytes(*diversifier); - let sender_address = orchard_fvk.as_ref().ok_or(anyhow!("No Orchard key")).map(|fvk| fvk.address(diversifier, Scope::External))?; + let sender_address = orchard_fvk + .as_ref() + .ok_or(anyhow!("No Orchard key")) + .map(|fvk| fvk.address(diversifier, Scope::External))?; let value = NoteValue::from_raw(spend.amount); let rho = Nullifier::from_bytes(&rho).unwrap(); let rseed = orchard::note::RandomSeed::from_bytes(*rseed, &rho).unwrap(); let note = orchard::Note::from_parts(sender_address, value, rho, rseed).unwrap(); let witness = Witness::from_bytes(*id_note, &witness)?; - let auth_path: Vec<_> = witness.auth_path(32, &ORCHARD_ROOTS, &OrchardHasher::new()).iter() - .map(|n| { - orchard::tree::MerkleHashOrchard::from_bytes(n).unwrap() - }).collect(); - let merkle_path = orchard::tree::MerklePath::from_parts(witness.position as u32, auth_path.try_into().unwrap()); - orchard_builder.add_spend(orchard_fvk.clone().unwrap(), note, merkle_path).map_err(|e| anyhow!(e.to_string()))?; + let auth_path: Vec<_> = witness + .auth_path(32, &ORCHARD_ROOTS, &OrchardHasher::new()) + .iter() + .map(|n| orchard::tree::MerkleHashOrchard::from_bytes(n).unwrap()) + .collect(); + let merkle_path = orchard::tree::MerklePath::from_parts( + witness.position as u32, + auth_path.try_into().unwrap(), + ); + orchard_builder + .add_spend(orchard_fvk.clone().unwrap(), note, merkle_path) + .map_err(|e| anyhow!(e.to_string()))?; } } } @@ -143,26 +174,40 @@ pub fn build_tx(network: &Network, skeys: &SecretKeys, plan: &TransactionPlan, c } Destination::Sapling(addr) => { let sapling_address = PaymentAddress::from_bytes(addr).unwrap(); - builder.add_sapling_output(Some(sapling_ovk), sapling_address, value, output.memo.clone())?; + builder.add_sapling_output( + Some(sapling_ovk), + sapling_address, + value, + output.memo.clone(), + )?; } Destination::Orchard(addr) => { has_orchard = true; let orchard_address = Address::from_raw_address_bytes(addr).unwrap(); - orchard_builder.add_recipient(orchard_ovk.clone(), orchard_address, - NoteValue::from_raw(output.amount), Some(*output.memo.as_array())).map_err(|_| anyhow!("Orchard::add_recipient"))?; + orchard_builder + .add_recipient( + orchard_ovk.clone(), + orchard_address, + NoteValue::from_raw(output.amount), + Some(*output.memo.as_array()), + ) + .map_err(|_| anyhow!("Orchard::add_recipient"))?; } } } let transparent_bundle = builder.transparent_builder.build(); let mut ctx = get_prover().new_sapling_proving_context(); - let sapling_bundle = builder.sapling_builder + let sapling_bundle = builder + .sapling_builder .build( get_prover(), &mut ctx, &mut rng, - BlockHeight::from_u32(context.height), None - ).unwrap(); + BlockHeight::from_u32(context.height), + None, + ) + .unwrap(); let mut orchard_bundle: Option> = None; if has_orchard { @@ -185,12 +230,15 @@ pub fn build_tx(network: &Network, skeys: &SecretKeys, plan: &TransactionPlan, c let sig_hash = v5_signature_hash(&unauthed_tx, &SignableInput::Shielded, &txid_parts); let sig_hash: [u8; 32] = sig_hash.as_bytes().try_into().unwrap(); - let transparent_bundle = unauthed_tx.transparent_bundle().map(|tb| { - tb.clone().apply_signatures(&unauthed_tx, &txid_parts) - }); + let transparent_bundle = unauthed_tx + .transparent_bundle() + .map(|tb| tb.clone().apply_signatures(&unauthed_tx, &txid_parts)); let sapling_bundle = unauthed_tx.sapling_bundle().map(|sb| { - sb.clone().apply_signatures(get_prover(), &mut ctx, &mut rng, &sig_hash).unwrap().0 + sb.clone() + .apply_signatures(get_prover(), &mut ctx, &mut rng, &sig_hash) + .unwrap() + .0 }); let mut orchard_signing_keys = vec![]; @@ -198,11 +246,15 @@ pub fn build_tx(network: &Network, skeys: &SecretKeys, plan: &TransactionPlan, c orchard_signing_keys.push(SpendAuthorizingKey::from(&sk)); } - let orchard_bundle = unauthed_tx.orchard_bundle() - .map(|ob| { - let proven = ob.clone().create_proof(get_proving_key(), rng.clone()).unwrap(); - proven.apply_signatures(rng.clone(), sig_hash, &orchard_signing_keys).unwrap() - }); + let orchard_bundle = unauthed_tx.orchard_bundle().map(|ob| { + let proven = ob + .clone() + .create_proof(get_proving_key(), rng.clone()) + .unwrap(); + proven + .apply_signatures(rng.clone(), sig_hash, &orchard_signing_keys) + .unwrap() + }); let tx_data: TransactionData = TransactionData::from_parts( @@ -225,20 +277,23 @@ pub fn build_tx(network: &Network, skeys: &SecretKeys, plan: &TransactionPlan, c pub fn get_secret_keys(coin: u8, account: u32) -> anyhow::Result { let c = CoinConfig::get(coin); - let db = c.db.as_ref().unwrap(); - let db = db.lock().unwrap(); + let db = c.db()?; - let transparent_sk = db.get_tsk(account)?.map(|tsk| { - SecretKey::from_str(&tsk).unwrap() - }); + let transparent_sk = db + .get_tsk(account)? + .map(|tsk| SecretKey::from_str(&tsk).unwrap()); let AccountData { sk, .. } = db.get_account_info(account)?; let sapling_sk = sk.ok_or(anyhow!("No secret key"))?; - let sapling_sk = decode_extended_spending_key(c.chain.network().hrp_sapling_extended_spending_key(), &sapling_sk).unwrap(); + let sapling_sk = decode_extended_spending_key( + c.chain.network().hrp_sapling_extended_spending_key(), + &sapling_sk, + ) + .unwrap(); - let orchard_sk = db.get_orchard(account)?.map(|ob| { - SpendingKey::from_bytes(ob.sk).unwrap() - }); + let orchard_sk = db + .get_orchard(account)? + .map(|ob| SpendingKey::from_bytes(ob.sk).unwrap()); let sk = SecretKeys { transparent: transparent_sk, @@ -264,15 +319,13 @@ async fn dummy_test() { log::info!("Height {}", height); const REGTEST_CHANGE: &str = "uregtest1mxy5wq2n0xw57nuxa4lqpl358zw4vzyfgadsn5jungttmqcv6nx6cpx465dtpzjzw0vprjle4j4nqqzxtkuzm93regvgg4xce0un5ec6tedquc469zjhtdpkxz04kunqqyasv4rwvcweh3ue0ku0payn29stl2pwcrghyzscrrju9ar57rn36wgz74nmynwcyw27rjd8yk477l97ez8"; - let mut config = NoteSelectConfig::new(REGTEST_CHANGE); - config.use_transparent = true; - config.privacy_policy = PrivacyPolicy::AnyPool; + let mut config = TransactionBuilderConfig::new(REGTEST_CHANGE); log::info!("Getting signing keys"); let keys = get_secret_keys(0, 1).unwrap(); log::info!("Building signing context"); - let context = Context::from_height(0, height).unwrap(); + let context = TxBuilderContext::from_height(0, height).unwrap(); log::info!("Getting available notes"); let utxos = fetch_utxos(0, 1, height, true, 0).await.unwrap(); @@ -280,16 +333,24 @@ async fn dummy_test() { log::info!("Preparing outputs"); let mut orders = vec![]; - orders.push(decode(1, "tmWXoSBwPoCjJCNZjw4P7heoVMcT2Ronrqq", 10000, MemoBytes::empty()).unwrap()); - orders.push(decode(2, "zregtestsapling1qzy9wafd2axnenul6t6wav76dys6s8uatsq778mpmdvmx4k9myqxsd9m73aqdgc7gwnv53wga4j", 20000, MemoBytes::empty()).unwrap()); - orders.push(decode(3, "uregtest1mzt5lx5s5u8kczlfr82av97kjckmfjfuq8y9849h6cl9chhdekxsm6r9dklracflqwplrnfzm5rucp5txfdm04z5myrde8y3y5rayev8", 30000, MemoBytes::empty()).unwrap()); - orders.push(decode(4, "uregtest1yvucqfqnmq5ldc6fkvuudlsjhxg56hxph9ymmcnmpzpywd752ym8sr5l5d24wqn4enz3gakk6alf5hlpw2cjs3jjrcdae3nksrefyum5x400f9gs3ak9yllcr8czhrlnjufuuy7n5mh", 40000, MemoBytes::empty()).unwrap()); - orders.push(decode(5, "uregtest1wqgc0cm50a7a647qrdglgj62fl40q8njsrcfkt2mzlsmj979rdmsdwuysypc6ewxjxz0zc48kmm35jwx4q6c4fgqwkmmqyhwlep4n2hc0229vf6cahcnesr38y7gyzfx6pa8zg9jvv9", 50000, MemoBytes::empty()).unwrap()); - orders.push(decode(6, "uregtest1usu9eyxgqu48sa8lqug6ccjc7vcam3mt3a5t7jvyxj7pq5dgdtkjgkqzsyh9pfeav9970xddp2c9h5x44drwnz4f0zwc894k3vt380g6kfsg9j9fmnpljye9r56d94njsv40uaam392xvmky2v38dh3yhayz44z6xv402slujuhwy3mg", 60000, MemoBytes::empty()).unwrap()); - orders.push(decode(7, "uregtest1mxy5wq2n0xw57nuxa4lqpl358zw4vzyfgadsn5jungttmqcv6nx6cpx465dtpzjzw0vprjle4j4nqqzxtkuzm93regvgg4xce0un5ec6tedquc469zjhtdpkxz04kunqqyasv4rwvcweh3ue0ku0payn29stl2pwcrghyzscrrju9ar57rn36wgz74nmynwcyw27rjd8yk477l97ez8", 70000, MemoBytes::empty()).unwrap()); + orders.push( + Order::new( + 1, + "tmWXoSBwPoCjJCNZjw4P7heoVMcT2Ronrqq", + 10000, + MemoBytes::empty(), + ) + ); + orders.push(Order::new(2, "zregtestsapling1qzy9wafd2axnenul6t6wav76dys6s8uatsq778mpmdvmx4k9myqxsd9m73aqdgc7gwnv53wga4j", 20000, MemoBytes::empty())); + orders.push(Order::new(3, "uregtest1mzt5lx5s5u8kczlfr82av97kjckmfjfuq8y9849h6cl9chhdekxsm6r9dklracflqwplrnfzm5rucp5txfdm04z5myrde8y3y5rayev8", 30000, MemoBytes::empty())); + orders.push(Order::new(4, "uregtest1yvucqfqnmq5ldc6fkvuudlsjhxg56hxph9ymmcnmpzpywd752ym8sr5l5d24wqn4enz3gakk6alf5hlpw2cjs3jjrcdae3nksrefyum5x400f9gs3ak9yllcr8czhrlnjufuuy7n5mh", 40000, MemoBytes::empty())); + orders.push(Order::new(5, "uregtest1wqgc0cm50a7a647qrdglgj62fl40q8njsrcfkt2mzlsmj979rdmsdwuysypc6ewxjxz0zc48kmm35jwx4q6c4fgqwkmmqyhwlep4n2hc0229vf6cahcnesr38y7gyzfx6pa8zg9jvv9", 50000, MemoBytes::empty())); + orders.push(Order::new(6, "uregtest1usu9eyxgqu48sa8lqug6ccjc7vcam3mt3a5t7jvyxj7pq5dgdtkjgkqzsyh9pfeav9970xddp2c9h5x44drwnz4f0zwc894k3vt380g6kfsg9j9fmnpljye9r56d94njsv40uaam392xvmky2v38dh3yhayz44z6xv402slujuhwy3mg", 60000, MemoBytes::empty())); + orders.push(Order::new(7, "uregtest1mxy5wq2n0xw57nuxa4lqpl358zw4vzyfgadsn5jungttmqcv6nx6cpx465dtpzjzw0vprjle4j4nqqzxtkuzm93regvgg4xce0un5ec6tedquc469zjhtdpkxz04kunqqyasv4rwvcweh3ue0ku0payn29stl2pwcrghyzscrrju9ar57rn36wgz74nmynwcyw27rjd8yk477l97ez8", 70000, MemoBytes::empty())); log::info!("Building tx plan"); - let tx_plan = note_select_with_fee::(&utxos, &mut orders, &config).unwrap(); + let tx_plan = + build_tx_plan::("", 0, &utxos, &mut orders, &config).unwrap(); log::info!("Plan: {}", serde_json::to_string(&tx_plan).unwrap()); log::info!("Building tx"); diff --git a/src/note_selection/fee.rs b/src/note_selection/fee.rs index e066855..5dcc84b 100644 --- a/src/note_selection/fee.rs +++ b/src/note_selection/fee.rs @@ -1,5 +1,5 @@ +use super::types::*; use std::cmp::max; -use crate::note_selection::types::*; const MARGINAL_FEE: u64 = 5000; const GRACE_ACTIONS: u64 = 2; @@ -20,17 +20,23 @@ impl FeeCalculator for FeeZIP327 { n_in[pool] += 1; } for o in outputs { - if !o.is_fee { - let pool = o.destination.pool() as usize; - n_out[pool] += 1; - } + let pool = o.destination.pool() as usize; + n_out[pool] += 1; } - let n_logical_actions = max(n_in[0], n_out[0]) + - max(n_in[1], n_out[1]) + - max(n_in[2], n_out[2]); + let n_logical_actions = + max(n_in[0], n_out[0]) + max(n_in[1], n_out[1]) + max(n_in[2], n_out[2]); - log::info!("fee: {}/{} {}/{} {}/{} = {}", n_in[0], n_out[0], n_in[1], n_out[1], n_in[2], n_out[2], n_logical_actions); + log::info!( + "fee: {}/{} {}/{} {}/{} = {}", + n_in[0], + n_out[0], + n_in[1], + n_out[1], + n_in[2], + n_out[2], + n_logical_actions + ); let fee = MARGINAL_FEE * max(n_logical_actions, GRACE_ACTIONS); fee } @@ -43,4 +49,3 @@ impl FeeCalculator for FeeFlat { 1000 } } - diff --git a/src/note_selection/fill.rs b/src/note_selection/fill.rs deleted file mode 100644 index 745673a..0000000 --- a/src/note_selection/fill.rs +++ /dev/null @@ -1,151 +0,0 @@ -use std::cmp::min; -use zcash_address::{AddressKind, ZcashAddress}; -use zcash_address::unified::{Container, Receiver}; -use zcash_primitives::memo::MemoBytes; -use crate::note_selection::types::{PrivacyPolicy, Fill, Execution, Order, Pool, PoolAllocation, Destination, PoolPrecedence}; - -/// Decode address and return it as an order -/// -pub fn decode(id: u32, address: &str, amount: u64, memo: MemoBytes) -> anyhow::Result { - let address = ZcashAddress::try_from_encoded(address)?; - let mut order = Order::default(); - order.id = id; - match address.kind { - AddressKind::Sprout(_) => {} - AddressKind::Sapling(data) => { - let destination = Destination::Sapling(data); - order.destinations[Pool::Sapling as usize] = Some(destination); - } - AddressKind::Unified(unified_address) => { - for address in unified_address.items() { - match address { - Receiver::Orchard(data) => { - let destination = Destination::Orchard(data); - order.destinations[Pool::Orchard as usize] = Some(destination); - } - Receiver::Sapling(data) => { - let destination = Destination::Sapling(data); - order.destinations[Pool::Sapling as usize] = Some(destination); - } - Receiver::P2pkh(data) => { - let destination = Destination::Transparent(data); - order.destinations[Pool::Transparent as usize] = Some(destination); - } - Receiver::P2sh(_) => {} - Receiver::Unknown { .. } => {} - } - } - } - AddressKind::P2pkh(data) => { - let destination = Destination::Transparent(data); - order.destinations[Pool::Transparent as usize] = Some(destination); - } - AddressKind::P2sh(_) => {} - } - order.amount = amount; - order.memo = memo; - - Ok(order) -} - -pub fn execute_orders(orders: &mut [Order], initial_pool: &PoolAllocation, use_transparent: bool, use_shielded: bool, - privacy_policy: PrivacyPolicy, precedence: &PoolPrecedence) -> anyhow::Result { - let mut allocation: PoolAllocation = PoolAllocation::default(); - let mut fills = vec![]; - - for order in orders.iter_mut() { - let order_precedence = if order.is_fee { precedence } else { order.priority.to_pool_precedence() }; - // log::info!("Order {:?}", order); - // Direct Shielded Fill - s2s, o2o - // t2t only for fees - if use_shielded { - for &pool in order_precedence { - if pool == Pool::Transparent && !order.is_fee { continue } - if order.destinations[pool as usize].is_some() { - fill_order(pool, pool, order, initial_pool, &mut allocation, &mut fills); - } - } - } - - if privacy_policy != PrivacyPolicy::SamePoolOnly { - // Indirect Shielded - z2z: s2o, o2s - for &pool in order_precedence { - if order.destinations[pool as usize].is_none() { continue } - if !use_shielded { continue } - if let Some(from_pool) = pool.other_shielded() { - fill_order(from_pool, pool, order, initial_pool, &mut allocation, &mut fills); - } - } - - if privacy_policy == PrivacyPolicy::AnyPool { - // Other - s2t, o2t, t2s, t2o - for &pool in order_precedence { - if order.destinations[pool as usize].is_none() { continue } - match pool { - Pool::Transparent if use_shielded => { - for &from_pool in precedence { - if from_pool != Pool::Transparent { - fill_order(from_pool, pool, order, initial_pool, &mut allocation, &mut fills); - } - } - } - Pool::Sapling | Pool::Orchard if use_transparent => { - fill_order(Pool::Transparent, pool, order, initial_pool, &mut allocation, &mut fills); - } - _ => {} - }; - } - - // t2t - if use_transparent && order.destinations[Pool::Transparent as usize].is_some() { - fill_order(Pool::Transparent, Pool::Transparent, order, initial_pool, &mut allocation, &mut fills); - } - } - } - } - - let execution = Execution { - allocation, - fills, - }; - - Ok(execution) -} - -fn fill_order(from: Pool, to: Pool, order: &mut Order, initial_pool: &PoolAllocation, - fills: &mut PoolAllocation, executions: &mut Vec) { - let from = from as usize; - let to = to as usize; - let destination = order.destinations[to].as_ref().unwrap(); // Checked by caller - let order_remaining = order.amount - order.filled; - let pool_remaining = initial_pool.0[from] - fills.0[from]; - let amount = min(pool_remaining, order_remaining); - order.filled += amount; - fills.0[from] += amount; - if amount > 0 { - let execution = Fill { - id_order: order.id, - destination: destination.clone(), - amount, - memo: order.memo.clone(), - is_fee: order.is_fee, - }; - log::debug!("{:?}", execution); - executions.push(execution); - } - assert!(order.amount == order.filled || initial_pool.0[from] == fills.0[from]); // fill must be to the max -} - -impl Pool { - fn other_shielded(&self) -> Option { - match self { - Pool::Transparent => None, - Pool::Sapling => Some(Pool::Orchard), - Pool::Orchard => Some(Pool::Sapling), - } - } -} - -#[cfg(test)] -mod tests { -} diff --git a/src/note_selection/optimize.rs b/src/note_selection/optimize.rs new file mode 100644 index 0000000..4bd75c6 --- /dev/null +++ b/src/note_selection/optimize.rs @@ -0,0 +1,306 @@ +use std::str::FromStr; +use anyhow::anyhow; +use zcash_primitives::memo::{Memo, MemoBytes}; +use crate::note_selection::fee::FeeCalculator; +use crate::note_selection::{MAX_ATTEMPTS, TransactionBuilderError}; +use crate::note_selection::TransactionBuilderError::TxTooComplex; +use crate::note_selection::ua::decode; +use super::{Result, types::*}; + +pub fn sum_utxos(utxos: &[UTXO]) -> Result { + let mut pool = PoolAllocation::default(); + for utxo in utxos { + match utxo.source { + Source::Transparent { .. } => { + pool.0[0] += utxo.amount; + } + Source::Sapling { .. } => { + pool.0[1] += utxo.amount; + } + Source::Orchard { .. } => { + pool.0[2] += utxo.amount; + } + } + } + Ok(pool) +} + +pub fn group_orders(orders: &[Order], fee: u64) -> Result<(Vec, OrderGroupAmounts)> { + let mut order_info = vec![]; + for order in orders { + let mut group_type = 0; + for i in 0..3 { + if order.destinations[i].is_some() { + group_type |= 1 << i; + } + } + order_info.push(OrderInfo { + group_type, + amount: order.amount, + }); + } + + let mut t0 = 0u64; + let mut s0 = 0u64; + let mut o0 = 0u64; + let mut x = 0u64; + for info in order_info.iter_mut() { + if info.group_type != 1 { + info.group_type &= 6; // unselect transparent outputs except for t-addr + } + match info.group_type { + 1 => { + t0 += info.amount; + } + 2 => { + s0 += info.amount; + } + 4 => { + o0 += info.amount; + } + 6 => { + x += info.amount; + } + _ => unreachable!() + } + } + log::debug!("{} {} {} {}", t0, s0, o0, x); + let amounts = OrderGroupAmounts { + t0, + s0, + o0, + x, + fee, + }; + Ok((order_info, amounts)) +} + +pub fn allocate_funds(amounts: &OrderGroupAmounts, initial: &PoolAllocation) -> Result { + log::debug!("{:?}", initial); + + let OrderGroupAmounts { t0, s0, o0, x , fee} = *amounts; + let (t0, s0, o0, x, fee) = (t0 as i64, s0 as i64, o0 as i64, x as i64, fee as i64); + + let sum = t0 + s0 + o0 + x + fee; + let tmax = initial.0[0] as i64; + let smax = initial.0[1] as i64; + let omax = initial.0[2] as i64; + + let mut s1; + let mut o1; + let mut s2; + let mut o2; + let mut t2 = sum - smax - omax; + if t2 > 0 { + if t2 > tmax { + return Err(TransactionBuilderError::NotEnoughFunds) + } + // Not enough shielded notes. Use them all before using transparent notes + s2 = smax; + o2 = omax; + let d_s = (t0 + fee - t2) / 2; + let d_o = t0 + fee - t2 - d_s; // handle case when t0+fee-t2 is odd + s1 = s2 - d_s - s0; + o1 = o2 - d_o - o0; + + if s1 < 0 { + s1 = 0; + o1 = x; + } + else if o1 < 0 { + o1 = 0; + s1 = x; + } + } + else { + t2 = 0; + let d_s = (t0 + fee) / 2; + let d_o = t0 + fee - d_s; + + // Solve relaxed problem + let inp = sum / 2; + s2 = inp; + o2 = sum - inp; + s1 = s2 - d_s - s0; + o1 = o2 - d_o - o0; + + // Check solution validity + if s1 < 0 { + s1 = 0; + o1 = x; + s2 = s0 + d_s; + o2 = o0 + d_o + x; + } else if o1 < 0 { + o1 = 0; + s1 = x; + o2 = o0 + d_o; + s2 = s0 + d_s + x; + } + + assert!(s2 >= 0); + assert!(o2 >= 0); + + // Check account balances + + if s2 > smax { + s2 = smax; + o2 = sum - s2; + } + if o2 > omax { + o2 = omax; + s2 = sum - o2; + } + } + + assert!(s1 >= 0); + assert!(o1 >= 0); + assert!(t2 >= 0); + assert!(s2 >= 0); + assert!(o2 >= 0); + assert!(t2 <= tmax); + assert!(s2 <= smax); + assert!(o2 <= omax); + + assert_eq!(sum, t2 + s2 + o2); + assert_eq!(x, s1 + o1); + + log::debug!("{} {}", s1, o1); + log::debug!("{} {} {}", t2, s2, o2); + + let fund_allocation = FundAllocation { + s1: s1 as u64, + o1: o1 as u64, + t2: t2 as u64, + s2: s2 as u64, + o2: o2 as u64, + }; + Ok(fund_allocation) +} + +pub fn fill(orders: &[Order], order_infos: &[OrderInfo], amounts: &OrderGroupAmounts, allocation: &FundAllocation) -> Result> { + assert_eq!(orders.len(), order_infos.len()); + let mut fills = vec![]; + let mut f = 0f64; + if amounts.x != 0 { + f = allocation.s1 as f64 / amounts.x as f64; + } + for (order, info) in orders.iter().zip(order_infos) { + match info.group_type { + 1 | 2 | 4 => { + let fill = Fill { + id_order: Some(order.id), + destination: order.destinations[ilog2(info.group_type)].unwrap(), + amount: order.amount, + memo: order.memo.clone(), + }; + fills.push(fill); + } + 6 => { + let fill1 = Fill { + id_order: Some(order.id), + destination: order.destinations[1].unwrap(), + amount: (order.amount as f64 * f).round() as u64, + memo: order.memo.clone(), + }; + let fill2 = Fill { + id_order: Some(order.id), + destination: order.destinations[2].unwrap(), + amount: order.amount - fill1.amount, + memo: order.memo.clone(), + }; + if fill1.amount != 0 { fills.push(fill1); } + if fill2.amount != 0 { fills.push(fill2); } + } + _ => unreachable!() + } + } + + Ok(fills) +} + +pub fn select_inputs(utxos: &[UTXO], allocation: &FundAllocation) -> Result<(Vec, PoolAllocation)> { + let mut needed = [allocation.t2, allocation.s2, allocation.o2]; + let mut change = [0u64; 3]; + let mut inputs = vec![]; + for utxo in utxos { + let idx = match utxo.source { + Source::Transparent { .. } => 0, + Source::Sapling { .. } => 1, + Source::Orchard { .. } => 2, + }; + if needed[idx] > 0 { + let available = utxo.amount; + let a = available.min(needed[idx]); + inputs.push(utxo.clone()); + needed[idx] -= a; + change[idx] += available - a; + } + } + + Ok((inputs, PoolAllocation(change))) +} + +pub fn outputs_for_change(change_destinations: &[Option; 3], change: &PoolAllocation) -> Result> { + let mut change_fills = vec![]; + for i in 0..3 { + let destination = change_destinations[i]; + if let Some(destination) = destination { + let change_fill = Fill { + id_order: None, + destination, + amount: change.0[i], + memo: MemoBytes::empty(), + }; + if change_fill.amount != 0 { change_fills.push(change_fill); } + } else { + return Err(anyhow!("No change address").into()) + } + } + Ok(change_fills) +} + +pub fn build_tx_plan(fvk: &str, height: u32, utxos: &[UTXO], orders: &[Order], config: &TransactionBuilderConfig) -> Result { + let mut fee = 0; + + for _ in 0..MAX_ATTEMPTS { + let balances = sum_utxos(utxos)?; + let (groups, amounts) = group_orders(&orders, fee)?; + let allocation = allocate_funds(&amounts, &balances)?; + + let OrderGroupAmounts { s0, o0, .. } = amounts; + let FundAllocation { s1, o1, s2, o2, .. } = allocation; + let (s0, o0, s1, o1, s2, o2) = (s0 as i64, o0 as i64, s1 as i64, o1 as i64, s2 as i64, o2 as i64); + let net_chg = [s0 + s1 - s2, o0 + o1 - o2]; + + let mut fills = fill(&orders, &groups, &amounts, &allocation)?; + + let (notes, change) = select_inputs(&utxos, &allocation)?; + let change_destinations = decode(&config.change_address)?; + let change_outputs = outputs_for_change(&change_destinations, &change).unwrap(); + fills.extend(change_outputs); + + let updated_fee = F::calculate_fee(¬es, &fills); + if updated_fee == fee { + let tx_plan = TransactionPlan { + fvk: fvk.to_string(), + height, + spends: notes, + outputs: fills, + net_chg, + fee + }; + return Ok(tx_plan) + } + fee = updated_fee; + } + Err(TxTooComplex) +} + +fn ilog2(u: usize) -> usize { + match u { + 1 => 0, + 2 => 1, + 4 => 2, + _ => unreachable!(), + } +} diff --git a/src/note_selection/select.rs b/src/note_selection/select.rs deleted file mode 100644 index 0b813e2..0000000 --- a/src/note_selection/select.rs +++ /dev/null @@ -1,151 +0,0 @@ -use std::cmp::min; -use std::slice; -use zcash_primitives::memo::MemoBytes; -use crate::note_selection::decode; -use crate::note_selection::fee::FeeCalculator; -use crate::note_selection::fill::execute_orders; -use crate::note_selection::types::{NoteSelectConfig, Order, PoolAllocation, UTXO, Destination, TransactionPlan, Fill, PoolPrecedence, PoolPriority}; - -pub fn select_notes(allocation: &PoolAllocation, utxos: &[UTXO]) -> anyhow::Result> { - let mut allocation = allocation.clone(); - - let mut selected = vec![]; - for utxo in utxos { - let pool = utxo.source.pool() as usize; - if allocation.0[pool] > 0 { - let amount = min(allocation.0[pool], utxo.amount); - selected.push(utxo.clone()); - allocation.0[pool] -= amount; - } - } - Ok(selected) -} - -struct OrderExecutor { - pub pool_available: PoolAllocation, - pub pool_used: PoolAllocation, - pub config: NoteSelectConfig, - pub fills: Vec, -} - -impl OrderExecutor { - pub fn new(initial_pool: PoolAllocation, config: NoteSelectConfig) -> Self { - OrderExecutor { - pool_available: initial_pool, - pool_used: PoolAllocation::default(), - config, - fills: vec![], - } - } - - pub fn execute(&mut self, orders: &mut [Order], precedence: &PoolPrecedence) -> anyhow::Result { - let order_execution = execute_orders(orders, &self.pool_available, - self.config.use_transparent, self.config.use_shielded, - self.config.privacy_policy.clone(), precedence)?; // calculate an execution plan without considering the fee - self.fills.extend(order_execution.fills); - self.pool_available = self.pool_available - order_execution.allocation; - self.pool_used = self.pool_used + order_execution.allocation; - let fully_filled = orders.iter().all(|o| o.amount == o.filled); - Ok(fully_filled) - } - - pub fn add(&mut self, fill: Fill) { - self.fills.push(fill); - } - - pub fn select_notes(&self, utxos: &[UTXO]) -> anyhow::Result> { - select_notes(&self.pool_used, &utxos) - } -} - -const ANY_DESTINATION: [Option; 3] = [Some(Destination::Transparent([0u8; 20])), Some(Destination::Sapling([0u8; 43])), Some(Destination::Orchard([0u8; 43]))]; -const MAX_ATTEMPTS: usize = 10; -/// Select notes from the `utxos` that can pay for the `orders` -/// -pub fn note_select_with_fee(utxos: &[UTXO], orders: &mut [Order], config: &NoteSelectConfig) -> anyhow::Result { - let initial_pool = PoolAllocation::from(&*utxos); // amount of funds available in each pool - let mut fee = 0; - let mut n_attempts = 0; - let change_order = decode(u32::MAX, &config.change_address, 0, MemoBytes::empty())?; - let change_destinations = change_order.destinations; - - let mut plan = loop { - for o in orders.iter_mut() { - o.filled = 0; - } - let mut executor = OrderExecutor::new(initial_pool, config.clone()); - if !executor.execute(orders, &config.precedence)? { - anyhow::bail!("Unsufficient Funds") - } - if fee == 0 { - let notes = executor.select_notes(utxos)?; - fee = F::calculate_fee(¬es, &executor.fills); - log::debug!("base fee: {}", fee); - } - - // Favor the pools that are already used, because it may cause lower fees - let pool_needed = executor.pool_used; - let prec_1 = config.precedence.iter().filter(|&&p| pool_needed.0[p as usize] != 0); - let prec_2 = config.precedence.iter().filter(|&&p| pool_needed.0[p as usize] == 0); - let fee_precedence: PoolPrecedence = prec_1.chain(prec_2).cloned().collect::>().try_into().unwrap(); - log::debug!("Fee precedence: {:?}", fee_precedence); - let mut fee_order = Order { - id: u32::MAX, - destinations: ANY_DESTINATION, - priority: PoolPriority::OS, // ignored for fees - amount: fee, - memo: MemoBytes::empty(), - is_fee: true, // do not include in fee calculation - filled: 0, - }; - - if !executor.execute(slice::from_mut(&mut fee_order), &fee_precedence)? { - anyhow::bail!("Unsufficient Funds [fees]") - } - - // Let's figure out the change - let pool_needed = executor.pool_used; - let total_needed = pool_needed.total(); - - let notes = executor.select_notes(utxos)?; - let pool_spent = PoolAllocation::from(&*notes); - let total_spent = pool_spent.total(); - let change = pool_spent - pool_needed; // must be >= 0 because the note selection covers the fills - - log::debug!("pool_needed: {:?} {}", pool_needed, total_needed); - log::debug!("pool_spent: {:?} {}", pool_spent, total_spent); - log::debug!("change: {:?}", change); - - for pool in 0..3 { - if change.0[pool] != 0 { - executor.add(Fill { - id_order: u32::MAX, - destination: change_destinations[pool].unwrap(), - amount: change.0[pool], - memo: MemoBytes::empty(), - is_fee: false - }) - } - } - - let notes = executor.select_notes(utxos)?; - let new_fee = F::calculate_fee(¬es, &executor.fills); - log::debug!("new fee: {}", new_fee); - - if new_fee == fee || n_attempts == MAX_ATTEMPTS { - let plan = TransactionPlan { - spends: notes.clone(), - outputs: executor.fills.clone(), - fee: 0, - }; - break plan; - } - fee = new_fee; // retry with the new fee - n_attempts += 1; - }; - - plan.fee = plan.outputs.iter().filter_map(|f| if f.is_fee { Some(f.amount) } else { None }).sum(); - plan.outputs = plan.outputs.into_iter().filter(|f| !f.is_fee).collect(); - - Ok(plan) -} diff --git a/src/note_selection/ser.rs b/src/note_selection/ser.rs new file mode 100644 index 0000000..1831e60 --- /dev/null +++ b/src/note_selection/ser.rs @@ -0,0 +1,23 @@ +use serde::{Serialize, Deserialize}; +use serde_with::serde_as; +use zcash_primitives::memo::MemoBytes; + +#[derive(Serialize, Deserialize)] +#[serde_as] +#[serde(remote = "MemoBytes")] +pub struct MemoBytesProxy( + #[serde_as(as = "serde_with::hex::Hex")] + #[serde(getter = "get_memo_bytes")] + pub Vec, +); + +fn get_memo_bytes(memo: &MemoBytes) -> Vec { + memo.as_slice().to_vec() +} + +impl From for MemoBytes { + fn from(p: MemoBytesProxy) -> MemoBytes { + MemoBytes::from_bytes(&p.0).unwrap() + } +} + diff --git a/src/note_selection/tests.rs b/src/note_selection/tests.rs index d22f5dc..2c308dd 100644 --- a/src/note_selection/tests.rs +++ b/src/note_selection/tests.rs @@ -1,87 +1,23 @@ +use serde::Serialize; +use assert_matches::assert_matches; use serde_json::Value; -use zcash_primitives::memo::Memo; -use crate::{CoinConfig, init_test}; -use crate::api::payment::RecipientMemo; -use crate::unified::UnifiedAddressType; -use super::{*, types::*}; - -// must have T+S+O receivers -const CHANGE_ADDRESS: &str = "u1pncsxa8jt7aq37r8uvhjrgt7sv8a665hdw44rqa28cd9t6qqmktzwktw772nlle6skkkxwmtzxaan3slntqev03g70tzpky3c58hfgvfjkcky255cwqgfuzdjcktfl7pjalt5sl33se75pmga09etn9dplr98eq2g8cgmvgvx6jx2a2xhy39x96c6rumvlyt35whml87r064qdzw30e"; -const UA_TSO: &str = "uregtest1mxy5wq2n0xw57nuxa4lqpl358zw4vzyfgadsn5jungttmqcv6nx6cpx465dtpzjzw0vprjle4j4nqqzxtkuzm93regvgg4xce0un5ec6tedquc469zjhtdpkxz04kunqqyasv4rwvcweh3ue0ku0payn29stl2pwcrghyzscrrju9ar57rn36wgz74nmynwcyw27rjd8yk477l97ez8"; -const UA_O: &str = "uregtest1mzt5lx5s5u8kczlfr82av97kjckmfjfuq8y9849h6cl9chhdekxsm6r9dklracflqwplrnfzm5rucp5txfdm04z5myrde8y3y5rayev8"; - -#[tokio::test] -async fn test_fetch_utxo() { - init_test(); - let utxos = fetch_utxos(0, 1, 235, true, 0).await.unwrap(); - - for utxo in utxos.iter() { - log::info!("{:?}", utxo); - } - - assert_eq!(utxos[0].amount, 624999000); -} - -#[test] -fn test_ua() { - init_test(); - let c = CoinConfig::get(0); - let db = c.db().unwrap(); - let address = crate::get_unified_address(c.chain.network(), &db, 1, - Some(UnifiedAddressType { transparent: true, sapling: true, orchard: false })).unwrap(); // use ua settings from db - println!("{}", address); -} - -#[tokio::test] -async fn test_payment() { - init_test(); - let config = NoteSelectConfig::new(CHANGE_ADDRESS); - - let recipients = vec![ - RecipientMemo { - address: UA_O.to_string(), - amount: 89000, - memo: Memo::Empty.into(), - max_amount_per_note: 0, - } - ]; - let tx_plan = prepare_multi_payment(0, 1, 205, - &recipients, &config, 3, - ).await.unwrap(); - - let tx_json = serde_json::to_string(&tx_plan).unwrap(); - println!("{}", tx_json); - - // expected: s2o because the recipient ua has only an orchard receiver - assert_eq!(tx_plan.outputs[0].destination.pool(), Pool::Orchard); - assert_eq!(tx_plan.outputs[0].amount, 89000); - assert_eq!(tx_plan.outputs[1].destination.pool(), Pool::Sapling); // change goes back to sapling - assert_eq!(tx_plan.outputs[1].amount, 624900000); - // fee = 10000 per zip-317 - assert_eq!(tx_plan.fee, 10000); - - assert_eq!(tx_plan.spends[0].amount, tx_plan.outputs[0].amount + tx_plan.outputs[1].amount + tx_plan.fee); -} - -macro_rules! order { - ($id:expr, $q:expr, $destinations:expr) => { - Order { - id: $id, - amount: $q * 1000, - destinations: $destinations, - priority: PoolPriority::OS, - filled: 0, - is_fee: false, - memo: MemoBytes::empty(), - } - }; -} +use zcash_primitives::memo::MemoBytes; +use crate::note_selection::build_tx_plan; +use crate::note_selection::fee::{FeeCalculator, FeeZIP327}; +use crate::note_selection::optimize::{outputs_for_change, select_inputs}; +use crate::note_selection::ua::decode; +use super::types::*; +use super::optimize::{allocate_funds, fill, group_orders}; +use super::TransactionBuilderError::NotEnoughFunds; macro_rules! utxo { ($id:expr, $q:expr) => { UTXO { amount: $q * 1000, - source: Source::Transparent { txid: [0u8; 32], index: $id }, + source: Source::Transparent { + txid: [0u8; 32], + index: $id, + }, } }; } @@ -115,9 +51,24 @@ macro_rules! orchard { }; } +macro_rules! order { + ($id:expr, $q:expr, $destinations:expr) => { + Order { + id: $id, + amount: $q * 1000, + destinations: $destinations, + memo: MemoBytes::empty(), + } + }; +} + macro_rules! t { ($id: expr, $q:expr) => { - order!($id, $q, [Some(Destination::Transparent([0u8; 20])), None, None]) + order!( + $id, + $q, + [Some(Destination::Transparent([0u8; 20])), None, None] + ) }; } @@ -135,216 +86,650 @@ macro_rules! o { macro_rules! ts { ($id: expr, $q:expr) => { - order!($id, $q, [Some(Destination::Transparent([0u8; 20])), Some(Destination::Sapling([0u8; 43])), None]) + order!( + $id, + $q, + [ + Some(Destination::Transparent([0u8; 20])), + Some(Destination::Sapling([0u8; 43])), + None + ] + ) }; } macro_rules! to { ($id: expr, $q:expr) => { - order!($id, $q, [Some(Destination::Transparent([0u8; 20])), None, Some(Destination::Orchard([0u8; 43]))]) + order!( + $id, + $q, + [ + Some(Destination::Transparent([0u8; 20])), + None, + Some(Destination::Orchard([0u8; 43])) + ] + ) }; } macro_rules! so { ($id: expr, $q:expr) => { - order!($id, $q, [None, Some(Destination::Sapling([0u8; 43])), Some(Destination::Orchard([0u8; 43]))]) + order!( + $id, + $q, + [ + None, + Some(Destination::Sapling([0u8; 43])), + Some(Destination::Orchard([0u8; 43])) + ] + ) }; } macro_rules! tso { ($id: expr, $q:expr) => { - order!($id, $q, [Some(Destination::Transparent([0u8; 20])), Some(Destination::Sapling([0u8; 43])), Some(Destination::Orchard([0u8; 43]))]) + order!( + $id, + $q, + [ + Some(Destination::Transparent([0u8; 20])), + Some(Destination::Sapling([0u8; 43])), + Some(Destination::Orchard([0u8; 43])) + ] + ) }; } #[test] -fn test_example1() { - let _ = env_logger::try_init(); - let mut config = NoteSelectConfig::new(CHANGE_ADDRESS); - config.use_transparent = true; - config.privacy_policy = PrivacyPolicy::AnyPool; +#[ignore] +fn test_select() { + env_logger::init(); - let utxos = [utxo!(1, 5), utxo!(2, 7), sapling!(3, 12), orchard!(4, 10)]; - let mut orders = [t!(1, 10)]; + // Exhaustive test of every combination of T/S/O/S+O recipients + // with every combination of assets in sender's account + let mut c = 0usize; + for t in 0..=10 { + for s in 0..=10 { + for o in 0..=10 { + for so in 0..=10 { + for fee in 0..=10 { + let amounts = OrderGroupAmounts { + t0: t * 10_000, + s0: s * 10_000, + o0: o * 10_000, + x: so * 10_000, + fee: fee * 1000, + }; + for t in 0..=10 { + for s in 0..=10 { + for o in 0..=10 { + let _ = allocate_funds(&amounts, &&PoolAllocation([t * 20_000, s * 20_000, o * 20_000])); + c += 1; + } + } + } + } + } + } + } + } - let tx_plan = note_select_with_fee::(&utxos, &mut orders, &config).unwrap(); - println!("{}", serde_json::to_string(&tx_plan).unwrap()); - - let tx_plan_json = serde_json::to_value(&tx_plan).unwrap(); - let expected: Value = serde_json::from_str(r#"{"spends":[{"source":{"Transparent":{"txid":"0000000000000000000000000000000000000000000000000000000000000000","index":1}},"amount":5000},{"source":{"Transparent":{"txid":"0000000000000000000000000000000000000000000000000000000000000000","index":2}},"amount":7000},{"source":{"Sapling":{"id_note":3,"diversifier":"0000000000000000000000","rseed":"0000000000000000000000000000000000000000000000000000000000000000","witness":""}},"amount":12000},{"source":{"Orchard":{"id_note":4,"diversifier":"0000000000000000000000","rseed":"0000000000000000000000000000000000000000000000000000000000000000","rho":"0000000000000000000000000000000000000000000000000000000000000000","witness":""}},"amount":10000}],"outputs":[{"id_order":1,"destination":{"Transparent":"0000000000000000000000000000000000000000"},"amount":10000,"memo":[246]},{"id_order":4294967295,"destination":{"Orchard":"2b6dca785c846b3752d13150e1c8f197ba9c8ead0a8bee1b3a52df0ad866362941e32d1b69d438b257cf82"},"amount":4000,"memo":[246]}],"fee":20000}"#).unwrap(); - assert_eq!(tx_plan_json, expected); + println!("{} tests", c); } #[test] -fn test_example2() { - let _ = env_logger::try_init(); - let mut config = NoteSelectConfig::new(CHANGE_ADDRESS); - config.privacy_policy = PrivacyPolicy::AnyPool; - - let utxos = [utxo!(1, 5), utxo!(2, 7), sapling!(3, 12), orchard!(4, 10), orchard!(5, 10)]; - let mut orders = [t!(1, 10)]; - - let tx_plan = note_select_with_fee::(&utxos, &mut orders, &config).unwrap(); - println!("{}", serde_json::to_string(&tx_plan).unwrap()); - - let tx_plan_json = serde_json::to_value(&tx_plan).unwrap(); - let expected: Value = serde_json::from_str(r#"{"spends":[{"source":{"Transparent":{"txid":"0000000000000000000000000000000000000000000000000000000000000000","index":1}},"amount":5000},{"source":{"Transparent":{"txid":"0000000000000000000000000000000000000000000000000000000000000000","index":2}},"amount":7000},{"source":{"Sapling":{"id_note":3,"diversifier":"0000000000000000000000","rseed":"0000000000000000000000000000000000000000000000000000000000000000","witness":""}},"amount":12000},{"source":{"Orchard":{"id_note":4,"diversifier":"0000000000000000000000","rseed":"0000000000000000000000000000000000000000000000000000000000000000","rho":"0000000000000000000000000000000000000000000000000000000000000000","witness":""}},"amount":10000}],"outputs":[{"id_order":1,"destination":{"Transparent":"0000000000000000000000000000000000000000"},"amount":10000,"memo":[246]},{"id_order":4294967295,"destination":{"Orchard":"2b6dca785c846b3752d13150e1c8f197ba9c8ead0a8bee1b3a52df0ad866362941e32d1b69d438b257cf82"},"amount":4000,"memo":[246]}],"fee":20000}"#).unwrap(); - assert_eq!(tx_plan_json, expected); +fn test_t2t() { + let r = allocate_funds(&OrderGroupAmounts { + t0: 100, + s0: 0, + o0: 0, + x: 0, + fee: 10 + }, &PoolAllocation([150, 0, 0])).unwrap(); + assert_eq!(r, FundAllocation { + s1: 0, + o1: 0, + t2: 110, + s2: 0, + o2: 0 + }) } #[test] -fn test_example3() { +fn test_t2zs() { + let r = allocate_funds(&OrderGroupAmounts { + t0: 0, + s0: 100, + o0: 0, + x: 0, + fee: 10 + }, &PoolAllocation([150, 0, 0])).unwrap(); + assert_eq!(r, FundAllocation { + s1: 0, + o1: 0, + t2: 110, + s2: 0, + o2: 0 + }) +} + +#[test] +fn test_t2zo() { + let r = allocate_funds(&OrderGroupAmounts { + t0: 0, + s0: 0, + o0: 100, + x: 0, + fee: 10 + }, &PoolAllocation([150, 0, 0])).unwrap(); + assert_eq!(r, FundAllocation { + s1: 0, + o1: 0, + t2: 110, + s2: 0, + o2: 0 + }) +} + +#[test] +fn test_t2ua() { + let r = allocate_funds(&OrderGroupAmounts { + t0: 0, + s0: 0, + o0: 0, + x: 100, + fee: 10 + }, &PoolAllocation([150, 0, 0])).unwrap(); + assert_eq!(r, FundAllocation { + s1: 50, + o1: 50, + t2: 110, + s2: 0, + o2: 0 + }) +} + +#[test] +fn test_zs2zs() { + let r = allocate_funds(&OrderGroupAmounts { + t0: 0, + s0: 100, + o0: 0, + x: 0, + fee: 10 + }, &PoolAllocation([0, 150, 0])).unwrap(); + assert_eq!(r, FundAllocation { + s1: 0, + o1: 0, + t2: 0, + s2: 110, + o2: 0 + }) +} + +#[test] +fn test_zo2zo() { + let r = allocate_funds(&OrderGroupAmounts { + t0: 0, + s0: 0, + o0: 100, + x: 0, + fee: 10 + }, &PoolAllocation([0, 0, 150])).unwrap(); + assert_eq!(r, FundAllocation { + s1: 0, + o1: 0, + t2: 0, + s2: 0, + o2: 110 + }) +} + +#[test] +fn test_ua2zs() { + let r = allocate_funds(&OrderGroupAmounts { + t0: 0, + s0: 100, + o0: 0, + x: 0, + fee: 10 + }, &PoolAllocation([0, 150, 150])).unwrap(); + assert_eq!(r, FundAllocation { + s1: 0, + o1: 0, + t2: 0, + s2: 105, + o2: 5, + }) // net change is (-5, -5) which is better than (-10, 0) +} + +#[test] +fn test_ua2zo() { + let r = allocate_funds(&OrderGroupAmounts { + t0: 0, + s0: 0, + o0: 100, + x: 0, + fee: 10 + }, &PoolAllocation([0, 150, 150])).unwrap(); + assert_eq!(r, FundAllocation { + s1: 0, + o1: 0, + t2: 0, + s2: 5, + o2: 105, + }) // net change is (-5, -5) which is better than (-10, 0) +} + +#[test] +fn test_ua2t() { + let r = allocate_funds(&OrderGroupAmounts { + t0: 100, + s0: 0, + o0: 0, + x: 0, + fee: 10 + }, &PoolAllocation([0, 150, 150])).unwrap(); + assert_eq!(r, FundAllocation { + s1: 0, + o1: 0, + t2: 0, + s2: 55, + o2: 55, + }) // split equally between sapling & orchard +} + +#[test] +fn test_zs2t() { + let r = allocate_funds(&OrderGroupAmounts { + t0: 100, + s0: 0, + o0: 0, + x: 0, + fee: 10 + }, &PoolAllocation([0, 150, 0])).unwrap(); + assert_eq!(r, FundAllocation { + s1: 0, + o1: 0, + t2: 0, + s2: 110, + o2: 0, + }) +} + +#[test] +fn test_zo2t() { + let r = allocate_funds(&OrderGroupAmounts { + t0: 100, + s0: 0, + o0: 0, + x: 0, + fee: 10 + }, &PoolAllocation([0, 0, 150])).unwrap(); + assert_eq!(r, FundAllocation { + s1: 0, + o1: 0, + t2: 0, + s2: 0, + o2: 110, + }) +} + +#[test] +fn test_zo2zs() { + let r = allocate_funds(&OrderGroupAmounts { + t0: 0, + s0: 100, + o0: 0, + x: 0, + fee: 10 + }, &PoolAllocation([0, 0, 150])).unwrap(); + assert_eq!(r, FundAllocation { + s1: 0, + o1: 0, + t2: 0, + s2: 0, + o2: 110, + }) +} + +#[test] +fn test_zs2zo() { + let r = allocate_funds(&OrderGroupAmounts { + t0: 0, + s0: 0, + o0: 100, + x: 0, + fee: 10 + }, &PoolAllocation([0, 150, 0])).unwrap(); + assert_eq!(r, FundAllocation { + s1: 0, + o1: 0, + t2: 0, + s2: 110, + o2: 0, + }) +} + +#[test] +fn test_ua2ua() { + let r = allocate_funds(&OrderGroupAmounts { + t0: 0, + s0: 0, + o0: 0, + x: 100, + fee: 10 + }, &PoolAllocation([0, 150, 150])).unwrap(); + assert_eq!(r, FundAllocation { + s1: 50, + o1: 50, + t2: 0, + s2: 55, + o2: 55, + }) +} + +#[test] +fn test_tzs2zs() { + let r = allocate_funds(&OrderGroupAmounts { + t0: 0, + s0: 100, + o0: 0, + x: 0, + fee: 10 + }, &PoolAllocation([150, 10, 10])).unwrap(); + assert_eq!(r, FundAllocation { + s1: 0, + o1: 0, + t2: 90, // must use t because not enough zs & zo + s2: 10, + o2: 10, + }) +} + +#[test] +fn test_tzs2ua() { + let r = allocate_funds(&OrderGroupAmounts { + t0: 0, + s0: 0, + o0: 0, + x: 100, + fee: 10 + }, &PoolAllocation([150, 10, 10])).unwrap(); + assert_eq!(r, FundAllocation { + s1: 50, + o1: 50, // split equally to minimize net change + t2: 90, // must use t because not enough zs & zo + s2: 10, + o2: 10, + }) +} + +#[test] +fn test_neg_ua2ua() { + let r = allocate_funds(&OrderGroupAmounts { + t0: 0, + s0: 0, + o0: 0, + x: 100, + fee: 10 + }, &PoolAllocation([10, 10, 10])); + assert_matches!(r, Err(NotEnoughFunds)) +} + +#[test] +fn test_odd_ua2ua() { + let r = allocate_funds(&OrderGroupAmounts { + t0: 1, + s0: 1, + o0: 1, + x: 1, + fee: 1 + }, &PoolAllocation([10, 10, 10])).unwrap(); + assert_eq!(r, FundAllocation { + s1: 0, + o1: 1, + t2: 0, + s2: 2, + o2: 3, + }) +} + +#[test] +fn test_fill() { let _ = env_logger::try_init(); - let mut config = NoteSelectConfig::new(CHANGE_ADDRESS); - config.use_transparent = true; - config.privacy_policy = PrivacyPolicy::AnyPool; - config.precedence = [ Pool::Sapling, Pool::Orchard, Pool::Transparent ]; + let orders = vec![ + t!(1, 10), + s!(2, 20), + o!(3, 30), + ts!(4, 40), + to!(5, 50), + so!(6, 60), + tso!(7, 70), + ]; + let (groups, amounts) = group_orders(&orders, 0).unwrap(); + assert_eq!(amounts, OrderGroupAmounts { + t0: 10_000, + s0: 60_000, + o0: 80_000, + x: 130_000, + fee: 0 + }); + let allocation = allocate_funds(&amounts, &PoolAllocation([200_000, 200_000, 200_000])).unwrap(); - let utxos = [utxo!(1, 100), sapling!(2, 160), orchard!(3, 70), orchard!(4, 50)]; - let mut orders = [t!(1, 10), s!(2, 20), o!(3, 30), ts!(4, 40), to!(5, 50), so!(6, 60), tso!(7, 70)]; + let fills = fill(&orders, &groups, &amounts, &allocation).unwrap(); + log::info!("{:?}", allocation); + log::info!("{:?}", fills); - let tx_plan = note_select_with_fee::(&utxos, &mut orders, &config).unwrap(); - println!("{}", serde_json::to_string(&tx_plan).unwrap()); + assert_eq!(fills[5].amount + fills[6].amount, 60_000); + assert_eq!(fills[7].amount + fills[8].amount, 70_000); + assert_eq!(fills[1].amount + fills[3].amount + fills[5].amount + fills[7].amount, fills[2].amount + fills[4].amount + fills[6].amount + fills[8].amount); +} - let tx_plan_json = serde_json::to_value(&tx_plan).unwrap(); - let expected: Value = serde_json::from_str(r#"{"spends":[{"source":{"Transparent":{"txid":"0000000000000000000000000000000000000000000000000000000000000000","index":1}},"amount":100000},{"source":{"Sapling":{"id_note":2,"diversifier":"0000000000000000000000","rseed":"0000000000000000000000000000000000000000000000000000000000000000","witness":""}},"amount":160000},{"source":{"Orchard":{"id_note":3,"diversifier":"0000000000000000000000","rseed":"0000000000000000000000000000000000000000000000000000000000000000","rho":"0000000000000000000000000000000000000000000000000000000000000000","witness":""}},"amount":70000},{"source":{"Orchard":{"id_note":4,"diversifier":"0000000000000000000000","rseed":"0000000000000000000000000000000000000000000000000000000000000000","rho":"0000000000000000000000000000000000000000000000000000000000000000","witness":""}},"amount":50000}],"outputs":[{"id_order":1,"destination":{"Transparent":"0000000000000000000000000000000000000000"},"amount":10000,"memo":[246]},{"id_order":2,"destination":{"Sapling":"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},"amount":20000,"memo":[246]},{"id_order":3,"destination":{"Orchard":"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},"amount":30000,"memo":[246]},{"id_order":4,"destination":{"Sapling":"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},"amount":40000,"memo":[246]},{"id_order":5,"destination":{"Orchard":"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},"amount":50000,"memo":[246]},{"id_order":6,"destination":{"Orchard":"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},"amount":40000,"memo":[246]},{"id_order":6,"destination":{"Sapling":"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},"amount":20000,"memo":[246]},{"id_order":7,"destination":{"Sapling":"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},"amount":70000,"memo":[246]},{"id_order":4294967295,"destination":{"Transparent":"c7b7b3d299bd173ea278d792b1bd5fbdd11afe34"},"amount":55000,"memo":[246]}],"fee":45000}"#).unwrap(); +#[test] +fn test_select_utxo() { + let _ = env_logger::try_init(); + let allocation = FundAllocation { s1: 75000, o1: 55000, t2: 0, s2: 140000, o2: 140000 }; + let mut utxos = vec![]; + for i in 0..30 { + if i < 10 { + utxos.push(utxo!(i, 25)); + } + else if i < 20 { + utxos.push(sapling!(i, 25)); + } + else { + utxos.push(orchard!(i, 25)); + } + } + let (_inputs, change) = select_inputs(&utxos, &allocation).unwrap(); + + assert_eq!(change.0, [0, 10000, 10000]); +} + +const CHANGE_ADDRESS: &str = "u1pncsxa8jt7aq37r8uvhjrgt7sv8a665hdw44rqa28cd9t6qqmktzwktw772nlle6skkkxwmtzxaan3slntqev03g70tzpky3c58hfgvfjkcky255cwqgfuzdjcktfl7pjalt5sl33se75pmga09etn9dplr98eq2g8cgmvgvx6jx2a2xhy39x96c6rumvlyt35whml87r064qdzw30e"; + +#[test] +fn test_change_fills() { + let _ = env_logger::try_init(); + let destinations = decode(CHANGE_ADDRESS).unwrap(); + let outputs = outputs_for_change(&destinations, &&PoolAllocation([0, 10000, 10000])).unwrap(); + log::info!("{:?}", outputs); +} + +#[test] +fn test_fees() { + let _ = env_logger::try_init(); + let utxos = utxos(); + let orders = vec![ + t!(1, 10), + s!(2, 20), + o!(3, 30), + ts!(4, 40), + to!(5, 50), + so!(6, 60), + tso!(7, 70), + ]; + let (groups, amounts) = group_orders(&orders, 0).unwrap(); + let allocation = allocate_funds(&amounts, &PoolAllocation([200_000, 200_000, 200_000])).unwrap(); + let fills = fill(&orders, &groups, &amounts, &allocation).unwrap(); + + let fees = FeeZIP327::calculate_fee( + &utxos, + &fills); + assert_eq!(fees, 150_000); +} + +#[test] +fn test_tx_plan() { + let _ = env_logger::try_init(); + let utxos = utxos(); + let orders = vec![ + t!(1, 10), + s!(2, 20), + o!(3, 30), + ts!(4, 40), + to!(5, 50), + so!(6, 60), + tso!(7, 70), + ]; + let tx_plan = build_tx_plan::("", 0, &utxos, &orders, + &TransactionBuilderConfig { change_address: CHANGE_ADDRESS.to_string() }).unwrap(); + let simple_plan: SimpleTxPlan = tx_plan.into(); + let plan = serde_json::to_string(&simple_plan).unwrap(); + log::info!("{}", plan); + + let tx_plan_json = serde_json::to_value(&simple_plan).unwrap(); + let expected: Value = serde_json::from_str(r#"{ + "inputs": [{ + "pool": 1, + "amount": 25000 + }, { + "pool": 1, + "amount": 25000 + }, { + "pool": 1, + "amount": 25000 + }, { + "pool": 1, + "amount": 25000 + }, { + "pool": 1, + "amount": 25000 + }, { + "pool": 1, + "amount": 25000 + }, { + "pool": 1, + "amount": 25000 + }, { + "pool": 1, + "amount": 25000 + }, { + "pool": 2, + "amount": 25000 + }, { + "pool": 2, + "amount": 25000 + }, { + "pool": 2, + "amount": 25000 + }, { + "pool": 2, + "amount": 25000 + }, { + "pool": 2, + "amount": 25000 + }, { + "pool": 2, + "amount": 25000 + }, { + "pool": 2, + "amount": 25000 + }, { + "pool": 2, + "amount": 25000 + }], + "outputs": [{ + "pool": 0, + "amount": 10000 + }, { + "pool": 1, + "amount": 20000 + }, { + "pool": 2, + "amount": 30000 + }, { + "pool": 1, + "amount": 40000 + }, { + "pool": 2, + "amount": 50000 + }, { + "pool": 1, + "amount": 34615 + }, { + "pool": 2, + "amount": 25385 + }, { + "pool": 1, + "amount": 40385 + }, { + "pool": 2, + "amount": 29615 + }, { + "pool": 1, + "amount": 17500 + }, { + "pool": 2, + "amount": 17500 + }], + "fee": 85000 + }"#).unwrap(); assert_eq!(tx_plan_json, expected); } -/// A simple t2t -/// -#[test] -fn test_example4() { - let _ = env_logger::try_init(); - let mut config = NoteSelectConfig::new(CHANGE_ADDRESS); - config.use_transparent = true; - config.use_shielded = false; - config.privacy_policy = PrivacyPolicy::AnyPool; - - let utxos = [utxo!(1, 50), sapling!(2, 50), orchard!(3, 50)]; - let mut orders = [t!(1, 10)]; - - let tx_plan = note_select_with_fee::(&utxos, &mut orders, &config).unwrap(); - println!("{}", serde_json::to_string(&tx_plan).unwrap()); - - let tx_plan_json = serde_json::to_value(&tx_plan).unwrap(); - let expected: Value = serde_json::from_str(r#"{"spends":[{"source":{"Transparent":{"txid":"0000000000000000000000000000000000000000000000000000000000000000","index":1}},"amount":50000}],"outputs":[{"id_order":1,"destination":{"Transparent":"0000000000000000000000000000000000000000"},"amount":10000,"memo":[246]},{"id_order":4294967295,"destination":{"Transparent":"c7b7b3d299bd173ea278d792b1bd5fbdd11afe34"},"amount":30000,"memo":[246]}],"fee":10000}"#).unwrap(); - assert_eq!(tx_plan_json, expected); +#[derive(Serialize)] +struct SimpleTxPlan { + inputs: Vec, + outputs: Vec, + fee: u64, } -/// A simple z2z -/// -#[test] -fn test_example5() { - let _ = env_logger::try_init(); - let config = NoteSelectConfig::new(CHANGE_ADDRESS); - - // z2z are preferred over t2z, so we can keep the t-notes - let utxos = [utxo!(1, 50), sapling!(2, 50), orchard!(3, 50)]; - let mut orders = [s!(1, 10)]; - - let tx_plan = note_select_with_fee::(&utxos, &mut orders, &config).unwrap(); - println!("{}", serde_json::to_string(&tx_plan).unwrap()); - - let tx_plan_json = serde_json::to_value(&tx_plan).unwrap(); - let expected: Value = serde_json::from_str(r#"{"spends":[{"source":{"Sapling":{"id_note":2,"diversifier":"0000000000000000000000","rseed":"0000000000000000000000000000000000000000000000000000000000000000","witness":""}},"amount":50000}],"outputs":[{"id_order":1,"destination":{"Sapling":"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},"amount":10000,"memo":[246]},{"id_order":4294967295,"destination":{"Sapling":"9fae6f28c245e095abf8c6730098e110bb67ae3e73302406b2b9c6d6b672ca9e64e14ef0560062a91dd429"},"amount":30000,"memo":[246]}],"fee":10000}"#).unwrap(); - assert_eq!(tx_plan_json, expected); +#[derive(Serialize)] +struct SimpleTxIO { + pool: u8, + amount: u64, } -/// A simple z2z -/// -#[test] -fn test_example5b() { - let _ = env_logger::try_init(); - let config = NoteSelectConfig::new(CHANGE_ADDRESS); - - // z2z are preferred over t2z, so we can keep the t-notes - let utxos = [utxo!(1, 50), sapling!(2, 50), orchard!(3, 50)]; - let mut orders = [o!(1, 10)]; - - let tx_plan = note_select_with_fee::(&utxos, &mut orders, &config).unwrap(); - println!("{}", serde_json::to_string(&tx_plan).unwrap()); - - let tx_plan_json = serde_json::to_value(&tx_plan).unwrap(); - let expected: Value = serde_json::from_str(r#"{"spends":[{"source":{"Orchard":{"id_note":3,"diversifier":"0000000000000000000000","rseed":"0000000000000000000000000000000000000000000000000000000000000000","rho":"0000000000000000000000000000000000000000000000000000000000000000","witness":""}},"amount":50000}],"outputs":[{"id_order":1,"destination":{"Orchard":"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},"amount":10000,"memo":[246]},{"id_order":4294967295,"destination":{"Orchard":"2b6dca785c846b3752d13150e1c8f197ba9c8ead0a8bee1b3a52df0ad866362941e32d1b69d438b257cf82"},"amount":30000,"memo":[246]}],"fee":10000}"#).unwrap(); - assert_eq!(tx_plan_json, expected); -} - /// A simple z2t sapling -/// -#[test] -fn test_example6() { - let _ = env_logger::try_init(); - let mut config = NoteSelectConfig::new(CHANGE_ADDRESS); - config.privacy_policy = PrivacyPolicy::AnyPool; - - let utxos = [utxo!(1, 50), sapling!(2, 50), orchard!(3, 50)]; - // Change the destination to t - let mut orders = [t!(1, 10)]; - - let tx_plan = note_select_with_fee::(&utxos, &mut orders, &config).unwrap(); - println!("{}", serde_json::to_string(&tx_plan).unwrap()); - - let tx_plan_json = serde_json::to_value(&tx_plan).unwrap(); - let expected: Value = serde_json::from_str(r#"{"spends":[{"source":{"Sapling":{"id_note":2,"diversifier":"0000000000000000000000","rseed":"0000000000000000000000000000000000000000000000000000000000000000","witness":""}},"amount":50000}],"outputs":[{"id_order":1,"destination":{"Transparent":"0000000000000000000000000000000000000000"},"amount":10000,"memo":[246]},{"id_order":4294967295,"destination":{"Sapling":"9fae6f28c245e095abf8c6730098e110bb67ae3e73302406b2b9c6d6b672ca9e64e14ef0560062a91dd429"},"amount":30000,"memo":[246]}],"fee":10000}"#).unwrap(); - assert_eq!(tx_plan_json, expected); - } - -/// A simple o2t -/// -#[test] -fn test_example7() { - let _ = env_logger::try_init(); - let mut config = NoteSelectConfig::new(CHANGE_ADDRESS); - config.precedence = [ Pool::Orchard, Pool::Sapling, Pool::Transparent ]; - config.privacy_policy = PrivacyPolicy::AnyPool; - - let utxos = [utxo!(1, 50), sapling!(2, 50), orchard!(3, 50)]; - // Change the destination to t - let mut orders = [t!(1, 10)]; - - let tx_plan = note_select_with_fee::(&utxos, &mut orders, &config).unwrap(); - println!("{}", serde_json::to_string(&tx_plan).unwrap()); - - let tx_plan_json = serde_json::to_value(&tx_plan).unwrap(); - let expected: Value = serde_json::from_str(r#"{"spends":[{"source":{"Orchard":{"id_note":3,"diversifier":"0000000000000000000000","rseed":"0000000000000000000000000000000000000000000000000000000000000000","rho":"0000000000000000000000000000000000000000000000000000000000000000","witness":""}},"amount":50000}],"outputs":[{"id_order":1,"destination":{"Transparent":"0000000000000000000000000000000000000000"},"amount":10000,"memo":[246]},{"id_order":4294967295,"destination":{"Orchard":"2b6dca785c846b3752d13150e1c8f197ba9c8ead0a8bee1b3a52df0ad866362941e32d1b69d438b257cf82"},"amount":30000,"memo":[246]}],"fee":10000}"#).unwrap(); - assert_eq!(tx_plan_json, expected); +impl From for SimpleTxPlan { + fn from(p: TransactionPlan) -> Self { + SimpleTxPlan { + inputs: p.spends.iter().map(|utxo| SimpleTxIO { + pool: utxo.source.pool() as u8, + amount: utxo.amount, + }).collect(), + outputs: p.outputs.iter().map(|utxo| SimpleTxIO { + pool: utxo.destination.pool() as u8, + amount: utxo.amount, + }).collect(), + fee: p.fee, + } + } } -/// A simple t2z -/// -#[test] -fn test_example8() { - let _ = env_logger::try_init(); - let mut config = NoteSelectConfig::new(CHANGE_ADDRESS); - config.privacy_policy = PrivacyPolicy::AnyPool; - config.use_transparent = true; - config.use_shielded = false; - - let utxos = [utxo!(1, 50), sapling!(2, 50), orchard!(3, 50)]; - let mut orders = [s!(1, 10)]; - - let tx_plan = note_select_with_fee::(&utxos, &mut orders, &config).unwrap(); - println!("{}", serde_json::to_string(&tx_plan).unwrap()); - - let tx_plan_json = serde_json::to_value(&tx_plan).unwrap(); - let expected: Value = serde_json::from_str(r#"{"spends":[{"source":{"Transparent":{"txid":"0000000000000000000000000000000000000000000000000000000000000000","index":1}},"amount":50000}],"outputs":[{"id_order":1,"destination":{"Sapling":"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},"amount":10000,"memo":[246]},{"id_order":4294967295,"destination":{"Transparent":"c7b7b3d299bd173ea278d792b1bd5fbdd11afe34"},"amount":30000,"memo":[246]}],"fee":10000}"#).unwrap(); - assert_eq!(tx_plan_json, expected); -} - -/// A simple z2z (Sapling/Orchard) -/// -#[test] -fn test_example9() { - let _ = env_logger::try_init(); - let config = NoteSelectConfig::new(CHANGE_ADDRESS); - - let utxos = [utxo!(1, 50), sapling!(2, 50)]; - let mut orders = [o!(1, 10)]; - - let tx_plan = note_select_with_fee::(&utxos, &mut orders, &config).unwrap(); - println!("{}", serde_json::to_string(&tx_plan).unwrap()); - - let tx_plan_json = serde_json::to_value(&tx_plan).unwrap(); - let expected: Value = serde_json::from_str(r#"{"spends":[{"source":{"Sapling":{"id_note":2,"diversifier":"0000000000000000000000","rseed":"0000000000000000000000000000000000000000000000000000000000000000","witness":""}},"amount":50000}],"outputs":[{"id_order":1,"destination":{"Orchard":"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},"amount":10000,"memo":[246]},{"id_order":4294967295,"destination":{"Sapling":"9fae6f28c245e095abf8c6730098e110bb67ae3e73302406b2b9c6d6b672ca9e64e14ef0560062a91dd429"},"amount":30000,"memo":[246]}],"fee":10000}"#).unwrap(); - assert_eq!(tx_plan_json, expected); +fn utxos() -> Vec { + let mut utxos = vec![]; + for i in 0..30 { + if i < 10 { + utxos.push(utxo!(i, 25)); + } + else if i < 20 { + utxos.push(sapling!(i, 25)); + } + else { + utxos.push(orchard!(i, 25)); + } + } + utxos } diff --git a/src/note_selection/types.rs b/src/note_selection/types.rs index ae2d53b..da8c482 100644 --- a/src/note_selection/types.rs +++ b/src/note_selection/types.rs @@ -1,44 +1,58 @@ -use std::ops::{Add, Sub}; -use zcash_primitives::memo::MemoBytes; -use serde::Serialize; +use serde::{Serialize, Deserialize}; use serde_with::serde_as; -use serde_hex::{SerHex,Strict}; +use serde_hex::{SerHex, Strict}; +use zcash_primitives::memo::MemoBytes; +use crate::note_selection::ua::decode; +use super::ser::MemoBytesProxy; -#[derive(Clone, PartialEq, Debug)] -pub enum PrivacyPolicy { - SamePoolOnly, - SamePoolTypeOnly, - AnyPool, +pub struct TransactionBuilderConfig { + pub change_address: String, +} + +impl TransactionBuilderConfig { + pub fn new(change_address: &str) -> Self { + TransactionBuilderConfig { + change_address: change_address.to_string(), + } + } } #[serde_as] -#[derive(Clone, Serialize, Debug)] +#[derive(Clone, Serialize, Deserialize, Debug)] pub enum Source { Transparent { - #[serde(with = "SerHex::")] txid: [u8; 32], + #[serde(with = "SerHex::")] + txid: [u8; 32], index: u32, }, Sapling { id_note: u32, - #[serde(with = "SerHex::")] diversifier: [u8; 11], - #[serde(with = "SerHex::")] rseed: [u8; 32], - #[serde_as(as = "serde_with::hex::Hex")] witness: Vec, + #[serde(with = "SerHex::")] + diversifier: [u8; 11], + #[serde(with = "SerHex::")] + rseed: [u8; 32], + #[serde_as(as = "serde_with::hex::Hex")] + witness: Vec, }, Orchard { id_note: u32, - #[serde(with = "SerHex::")] diversifier: [u8; 11], - #[serde(with = "SerHex::")] rseed: [u8; 32], - #[serde(with = "SerHex::")] rho: [u8; 32], - #[serde_as(as = "serde_with::hex::Hex")] witness: Vec, + #[serde(with = "SerHex::")] + diversifier: [u8; 11], + #[serde(with = "SerHex::")] + rseed: [u8; 32], + #[serde(with = "SerHex::")] + rho: [u8; 32], + #[serde_as(as = "serde_with::hex::Hex")] + witness: Vec, }, } -#[derive(Clone, Copy, Serialize, Debug)] +#[derive(Clone, Copy, Serialize, Deserialize, Debug)] #[serde_as] pub enum Destination { Transparent(#[serde(with = "SerHex::")] [u8; 20]), // MD5 - Sapling(#[serde(with = "SerHex::")] [u8; 43]), // Diversifier + Jubjub Point - Orchard(#[serde(with = "SerHex::")] [u8; 43]), // Diviersifer + Pallas Point + Sapling(#[serde(with = "SerHex::")] [u8; 43]), // Diversifier + Jubjub Point + Orchard(#[serde(with = "SerHex::")] [u8; 43]), // Diviersifer + Pallas Point } #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -48,178 +62,98 @@ pub enum Pool { Orchard = 2, } -#[derive(Serialize, Debug)] -pub struct Order { - pub id: u32, - pub destinations: [Option; 3], - pub priority: PoolPriority, - pub amount: u64, - #[serde(with = "MemoBytesProxy")] pub memo: MemoBytes, - pub is_fee: bool, - - pub filled: u64, // mutable -} - -#[derive(Serialize)] -#[serde_as] -#[serde(remote = "MemoBytes")] -struct MemoBytesProxy( - #[serde_as(as = "serde_with::hex::Hex")] - #[serde(getter = "get_memo_bytes")] - pub Vec -); - -fn get_memo_bytes(memo: &MemoBytes) -> Vec { - memo.as_slice().to_vec() -} - -impl From for MemoBytes { - fn from(p: MemoBytesProxy) -> MemoBytes { - MemoBytes::from_bytes(&p.0).unwrap() - } -} - -impl Default for Order { - fn default() -> Self { - Order { - id: 0, - destinations: [None; 3], - priority: PoolPriority::OS, - amount: 0, - memo: MemoBytes::empty(), - is_fee: false, - filled: 0 - } - } -} - -#[derive(Clone, Debug, Serialize)] -pub struct Fill { - pub id_order: u32, - pub destination: Destination, - pub amount: u64, - #[serde(with = "MemoBytesProxy")] pub memo: MemoBytes, - #[serde(skip)] - pub is_fee: bool, -} - -#[derive(Debug)] -pub struct Execution { - pub allocation: PoolAllocation, - pub fills: Vec, -} - -#[derive(Serialize, Default)] -pub struct TransactionPlan { - pub spends: Vec, - pub outputs: Vec, - pub fee: u64, -} - #[derive(Clone, Copy, Debug, Default)] pub struct PoolAllocation(pub [u64; 3]); -pub type PoolPrecedence = [Pool; 3]; - -#[derive(Clone, Serialize, Debug)] +#[derive(Clone, Serialize, Deserialize, Debug)] pub struct UTXO { pub source: Source, pub amount: u64, } -impl PoolAllocation { - pub fn total(&self) -> u64 { - self.0.iter().sum() - } +#[derive(Serialize, Debug)] +pub struct Order { + pub id: u32, + pub destinations: [Option; 3], + pub amount: u64, + #[serde(with = "MemoBytesProxy")] pub memo: MemoBytes, } -impl From<&[UTXO]> for PoolAllocation { - fn from(utxos: &[UTXO]) -> Self { - let mut allocation = PoolAllocation::default(); - for utxo in utxos { - let pool = utxo.source.pool() as usize; - allocation.0[pool] += utxo.amount; - } - allocation - } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Fill { + pub id_order: Option, + pub destination: Destination, + pub amount: u64, + #[serde(with = "MemoBytesProxy")] pub memo: MemoBytes, } -impl Add for PoolAllocation { - type Output = PoolAllocation; - - fn add(self, rhs: Self) -> Self::Output { - let mut res = PoolAllocation::default(); - for i in 0..3 { - res.0[i] = self.0[i] + rhs.0[i]; - } - res - } +#[derive(Clone, Deserialize)] +pub struct RecipientShort { + pub address: String, + pub amount: u64, } -impl Sub for PoolAllocation { - type Output = PoolAllocation; - - fn sub(self, rhs: Self) -> Self::Output { - let mut res = PoolAllocation::default(); - for i in 0..3 { - res.0[i] = self.0[i] - rhs.0[i]; - } - res - } +#[derive(Serialize, Deserialize, Default)] +pub struct TransactionPlan { + pub fvk: String, + pub height: u32, + pub spends: Vec, + pub outputs: Vec, + pub fee: u64, + pub net_chg: [i64; 2], } -#[derive(Clone)] -pub struct NoteSelectConfig { - pub privacy_policy: PrivacyPolicy, - pub use_transparent: bool, - pub use_shielded: bool, - pub precedence: PoolPrecedence, - pub change_address: String, +#[derive(PartialEq, Debug)] +pub struct OrderGroupAmounts { + pub t0: u64, + pub s0: u64, + pub o0: u64, + pub x: u64, + pub fee: u64, } -impl NoteSelectConfig { - pub fn new(change_address: &str) -> Self { - NoteSelectConfig { - privacy_policy: PrivacyPolicy::SamePoolTypeOnly, - use_transparent: false, - use_shielded: true, - precedence: [ Pool::Transparent, Pool::Sapling, Pool::Orchard ], // We prefer to keep our orchard notes - change_address: change_address.to_string() - } - } +pub struct OrderInfo { + pub group_type: usize, + pub amount: u64, +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct FundAllocation { + pub s1: u64, + pub o1: u64, + pub t2: u64, + pub s2: u64, + pub o2: u64, } impl Source { - pub fn pool(&self) -> Pool { + pub fn pool(&self) -> usize { match self { - Source::Transparent { .. } => Pool::Transparent, - Source::Sapling { .. } => Pool::Sapling, - Source::Orchard { .. } => Pool::Orchard, + Source::Transparent { .. } => 0, + Source::Sapling { .. } => 1, + Source::Orchard { .. } => 2, } } } impl Destination { - pub fn pool(&self) -> Pool { + pub fn pool(&self) -> usize { match self { - Destination::Transparent { .. } => Pool::Transparent, - Destination::Sapling { .. } => Pool::Sapling, - Destination::Orchard { .. } => Pool::Orchard, + Destination::Transparent { .. } => 0, + Destination::Sapling { .. } => 1, + Destination::Orchard { .. } => 2, } } } -#[derive(Clone, Copy, Serialize, Debug)] -pub enum PoolPriority { - SO = 1, - OS = 2, -} - -impl PoolPriority { - pub fn to_pool_precedence(&self) -> &'static PoolPrecedence { - match self { - PoolPriority::SO => &[ Pool::Sapling, Pool::Orchard, Pool::Transparent ], - PoolPriority::OS => &[ Pool::Orchard, Pool::Sapling, Pool::Transparent ], +impl Order { + pub fn new(id: u32, address: &str, amount: u64, memo: MemoBytes) -> Self { + let destinations = decode(address).unwrap(); + Order { + id, + destinations, + amount, + memo, } } } diff --git a/src/note_selection/ua.rs b/src/note_selection/ua.rs new file mode 100644 index 0000000..c85a6ab --- /dev/null +++ b/src/note_selection/ua.rs @@ -0,0 +1,42 @@ +use zcash_address::{AddressKind, ZcashAddress}; +use zcash_address::unified::{Container, Receiver}; +use super::types::*; + +pub fn decode(address: &str) -> anyhow::Result<[Option; 3]> { + let mut destinations: [Option; 3] = [None; 3]; + let address = ZcashAddress::try_from_encoded(address)?; + match address.kind { + AddressKind::Sprout(_) => {} + AddressKind::Sapling(data) => { + let destination = Destination::Sapling(data); + destinations[Pool::Sapling as usize] = Some(destination); + } + AddressKind::Unified(unified_address) => { + for address in unified_address.items() { + match address { + Receiver::Orchard(data) => { + let destination = Destination::Orchard(data); + destinations[Pool::Orchard as usize] = Some(destination); + } + Receiver::Sapling(data) => { + let destination = Destination::Sapling(data); + destinations[Pool::Sapling as usize] = Some(destination); + } + Receiver::P2pkh(data) => { + let destination = Destination::Transparent(data); + destinations[Pool::Transparent as usize] = Some(destination); + } + Receiver::P2sh(_) => {} + Receiver::Unknown { .. } => {} + } + } + } + AddressKind::P2pkh(data) => { + let destination = Destination::Transparent(data); + destinations[Pool::Transparent as usize] = Some(destination); + } + AddressKind::P2sh(_) => {} + } + + Ok(destinations) +} diff --git a/src/note_selection/utxo.rs b/src/note_selection/utxo.rs index 854d1c5..05914da 100644 --- a/src/note_selection/utxo.rs +++ b/src/note_selection/utxo.rs @@ -1,8 +1,14 @@ -use crate::CoinConfig; use crate::note_selection::types::Source; use crate::note_selection::UTXO; +use crate::CoinConfig; -pub async fn fetch_utxos(coin: u8, account: u32, last_height: u32, use_transparent_inputs: bool, anchor_offset: u32) -> anyhow::Result> { +pub async fn fetch_utxos( + coin: u8, + account: u32, + last_height: u32, + use_transparent_inputs: bool, + anchor_offset: u32, +) -> anyhow::Result> { let mut utxos = vec![]; if use_transparent_inputs { utxos.extend(get_transparent_utxos(coin, account).await?); @@ -17,25 +23,29 @@ pub async fn fetch_utxos(coin: u8, account: u32, last_height: u32, use_transpare async fn get_transparent_utxos(coin: u8, account: u32) -> anyhow::Result> { let coin = CoinConfig::get(coin); - let db = coin.db.as_ref().unwrap(); - let db = db.lock().unwrap(); - let taddr = db.get_taddr(account)?; + let taddr = { + let db = coin.db.as_ref().unwrap(); + let db = db.lock().unwrap(); + db.get_taddr(account)? + }; if let Some(taddr) = taddr { let mut client = coin.connect_lwd().await?; let utxos = crate::taddr::get_utxos(&mut client, &taddr, account).await?; - let utxos: Vec<_> = utxos.iter().map(|utxo| { - let source = Source::Transparent { - txid: utxo.txid.clone().try_into().unwrap(), - index: utxo.index as u32, - }; - UTXO { - source, - amount: utxo.value_zat as u64, - } - }).collect(); + let utxos: Vec<_> = utxos + .iter() + .map(|utxo| { + let source = Source::Transparent { + txid: utxo.txid.clone().try_into().unwrap(), + index: utxo.index as u32, + }; + UTXO { + source, + amount: utxo.value_zat as u64, + } + }) + .collect(); Ok(utxos) - } - else { + } else { Ok(vec![]) } } diff --git a/src/orchard.rs b/src/orchard.rs index 1e4db23..facf191 100644 --- a/src/orchard.rs +++ b/src/orchard.rs @@ -7,12 +7,12 @@ lazy_static! { } mod hash; -mod note; mod key; +mod note; -pub use note::{OrchardDecrypter, OrchardViewKey, DecryptedOrchardNote}; -pub use hash::{ORCHARD_ROOTS, OrchardHasher}; +pub use hash::{OrchardHasher, ORCHARD_ROOTS}; pub use key::{derive_orchard_keys, OrchardKeyBytes}; +pub use note::{DecryptedOrchardNote, OrchardDecrypter, OrchardViewKey}; pub fn get_proving_key() -> &'static ProvingKey { if !PROVING_KEY.filled() { diff --git a/src/orchard/hash.rs b/src/orchard/hash.rs index 9436f3a..0a2fef3 100644 --- a/src/orchard/hash.rs +++ b/src/orchard/hash.rs @@ -1,15 +1,15 @@ #![allow(non_snake_case)] +use crate::sync::{Hasher, Node}; +use crate::Hash; use group::cofactor::CofactorCurveAffine; use halo2_gadgets::sinsemilla::primitives::SINSEMILLA_S; use halo2_proofs::arithmetic::{CurveAffine, CurveExt}; -use halo2_proofs::pasta::EpAffine; use halo2_proofs::pasta::group::ff::PrimeField; use halo2_proofs::pasta::group::Curve; use halo2_proofs::pasta::pallas::{self, Affine, Point}; +use halo2_proofs::pasta::EpAffine; use lazy_static::lazy_static; -use crate::Hash; -use crate::sync::{Hasher, Node}; pub const Q_PERSONALIZATION: &str = "z.cash:SinsemillaQ"; pub const MERKLE_CRH_PERSONALIZATION: &str = "z.cash:Orchard-MerkleCRH"; @@ -100,8 +100,12 @@ impl Hasher for OrchardHasher { Point::batch_normalize(extended, &mut hash_affine); hash_affine .iter() - .map(|p| - p.coordinates().map(|c| *c.x()).unwrap_or_else(pallas::Base::zero).to_repr()) + .map(|p| { + p.coordinates() + .map(|c| *c.x()) + .unwrap_or_else(pallas::Base::zero) + .to_repr() + }) .collect() } } diff --git a/src/orchard/key.rs b/src/orchard/key.rs index 8e2822f..1071fde 100644 --- a/src/orchard/key.rs +++ b/src/orchard/key.rs @@ -1,6 +1,6 @@ use bip39::{Language, Mnemonic, Seed}; -use orchard::Address; use orchard::keys::{FullViewingKey, Scope, SpendingKey}; +use orchard::Address; pub struct OrchardKeyBytes { pub sk: [u8; 32], @@ -18,14 +18,10 @@ impl OrchardKeyBytes { pub fn derive_orchard_keys(coin_type: u32, seed: &str, account_index: u32) -> OrchardKeyBytes { let mnemonic = Mnemonic::from_phrase(seed, Language::English).unwrap(); let seed = Seed::new(&mnemonic, ""); - let sk = SpendingKey::from_zip32_seed( - seed.as_bytes(), - coin_type, - account_index, - ).unwrap(); + let sk = SpendingKey::from_zip32_seed(seed.as_bytes(), coin_type, account_index).unwrap(); let fvk = FullViewingKey::from(&sk); OrchardKeyBytes { sk: sk.to_bytes().clone(), - fvk: fvk.to_bytes() + fvk: fvk.to_bytes(), } } diff --git a/src/orchard/note.rs b/src/orchard/note.rs index e3eef85..42235a8 100644 --- a/src/orchard/note.rs +++ b/src/orchard/note.rs @@ -1,12 +1,14 @@ +use crate::chain::Nf; +use crate::db::ReceivedNote; +use crate::sync::{ + CompactOutputBytes, DecryptedNote, Node, OutputPosition, TrialDecrypter, ViewKey, +}; +use crate::{CompactTx, DbAdapterBuilder}; use orchard::keys::Scope; use orchard::note_encryption::OrchardDomain; -use zcash_primitives::consensus::{BlockHeight, Parameters}; -use crate::chain::Nf; -use crate::{CompactTx, DbAdapterBuilder}; -use crate::db::ReceivedNote; -use crate::sync::{CompactOutputBytes, DecryptedNote, Node, OutputPosition, TrialDecrypter, ViewKey}; use zcash_note_encryption; use zcash_params::coin::CoinType; +use zcash_primitives::consensus::{BlockHeight, Parameters}; #[derive(Clone, Debug)] pub struct OrchardViewKey { @@ -33,13 +35,19 @@ pub struct DecryptedOrchardNote { } impl DecryptedNote for DecryptedOrchardNote { - fn from_parts(vk: OrchardViewKey, note: orchard::Note, pa: orchard::Address, output_position: OutputPosition, cmx: Node) -> Self { + fn from_parts( + vk: OrchardViewKey, + note: orchard::Note, + pa: orchard::Address, + output_position: OutputPosition, + cmx: Node, + ) -> Self { DecryptedOrchardNote { vk, note, pa, output_position, - cmx + cmx, } } @@ -62,7 +70,7 @@ impl DecryptedNote for DecryptedOrchardNote { rcm: self.note.rseed().as_bytes().to_vec(), nf: self.note.nullifier(&self.vk.fvk).to_bytes().to_vec(), rho: Some(self.note.rho().to_bytes().to_vec()), - spent: None + spent: None, } } } @@ -72,24 +80,27 @@ pub struct OrchardDecrypter { pub network: N, } -impl OrchardDecrypter { +impl OrchardDecrypter { pub fn new(network: N) -> Self { - OrchardDecrypter { - network, - } + OrchardDecrypter { network } } } -impl TrialDecrypter for OrchardDecrypter { +impl TrialDecrypter + for OrchardDecrypter +{ fn domain(&self, _height: BlockHeight, cob: &CompactOutputBytes) -> OrchardDomain { OrchardDomain::for_nullifier(orchard::note::Nullifier::from_bytes(&cob.nullifier).unwrap()) } fn spends(&self, vtx: &CompactTx) -> Vec { - vtx.actions.iter().map(|co| { - let nf: [u8; 32] = co.nullifier.clone().try_into().unwrap(); - Nf(nf) - }).collect() + vtx.actions + .iter() + .map(|co| { + let nf: [u8; 32] = co.nullifier.clone().try_into().unwrap(); + Nf(nf) + }) + .collect() } fn outputs(&self, vtx: &CompactTx) -> Vec { @@ -104,14 +115,17 @@ pub fn test_decrypt() -> anyhow::Result<()> { // let mut cmx = hex::decode("df45e00eb39e4c281e2804a366d3010b7f663724472d12637e0a749e6ce22719").unwrap(); // let ciphertext = hex::decode("d9bc6ee09b0afde5dd69bfdf4b667a38da3e1084e84eb6752d54800b9f5110203b60496ab5313dba3f2acb9ef30bcaf68fbfcc59").unwrap(); - let nullifier = hex::decode("ea1b97cc83d326db4130433022f68dd32a0bc707448b19b0980e4e6404412b29").unwrap(); - let epk = hex::decode("e2f666e905666f29bb678c694602b2768bea655c0f2b18f9c342ad8b64b18c0c").unwrap(); - let cmx = hex::decode("4a95dbf0d1d0cac1376a0b8fb0fc2ed2843d0e2670dd976a63386b293f30de25").unwrap(); + let nullifier = + hex::decode("ea1b97cc83d326db4130433022f68dd32a0bc707448b19b0980e4e6404412b29").unwrap(); + let epk = + hex::decode("e2f666e905666f29bb678c694602b2768bea655c0f2b18f9c342ad8b64b18c0c").unwrap(); + let cmx = + hex::decode("4a95dbf0d1d0cac1376a0b8fb0fc2ed2843d0e2670dd976a63386b293f30de25").unwrap(); let ciphertext = hex::decode("73640095a90bb03d14f687d6acf4822618a3def1da3b71a588da1c68e25042f7c9aa759778e73aa2bb39d1061e51c1e8cf5e0bce").unwrap(); let db_builder = DbAdapterBuilder { coin_type: CoinType::Zcash, - db_path: "./zec.db".to_string() + db_path: "./zec.db".to_string(), }; let db = db_builder.build()?; let keys = db.get_orchard_fvks()?.first().unwrap().clone(); @@ -121,11 +135,16 @@ pub fn test_decrypt() -> anyhow::Result<()> { nullifier: nullifier.clone().try_into().unwrap(), epk: epk.try_into().unwrap(), cmx: cmx.try_into().unwrap(), - ciphertext: ciphertext.try_into().unwrap() + ciphertext: ciphertext.try_into().unwrap(), }; - let domain = OrchardDomain::for_nullifier(orchard::note::Nullifier::from_bytes(&nullifier.try_into().unwrap()).unwrap()); - let r = zcash_note_encryption::try_compact_note_decryption(&domain, &fvk.to_ivk(Scope::External), &output); + let domain = OrchardDomain::for_nullifier( + orchard::note::Nullifier::from_bytes(&nullifier.try_into().unwrap()).unwrap(), + ); + let r = zcash_note_encryption::try_compact_note_decryption( + &domain, + &fvk.to_ivk(Scope::External), + &output, + ); println!("{:?}", r); Ok(()) } - diff --git a/src/pay.rs b/src/pay.rs index 30dd1d5..b92b4d7 100644 --- a/src/pay.rs +++ b/src/pay.rs @@ -1,6 +1,7 @@ use crate::db::SpendableNote; // use crate::wallet::RecipientMemo; use crate::api::payment::RecipientMemo; +use crate::chain::get_latest_height; use crate::coinconfig::CoinConfig; use crate::{GetAddressUtxosReply, Hash, RawTransaction}; use anyhow::anyhow; @@ -12,7 +13,10 @@ use serde::{Deserialize, Serialize}; use std::sync::mpsc; use tonic::Request; use zcash_client_backend::address::RecipientAddress; -use zcash_client_backend::encoding::{decode_extended_full_viewing_key, decode_payment_address, encode_extended_full_viewing_key, encode_payment_address}; +use zcash_client_backend::encoding::{ + decode_extended_full_viewing_key, decode_payment_address, encode_extended_full_viewing_key, + encode_payment_address, +}; use zcash_params::coin::{get_coin_chain, CoinChain, CoinType}; use zcash_primitives::consensus::{BlockHeight, Parameters}; use zcash_primitives::keys::OutgoingViewingKey; @@ -25,7 +29,6 @@ use zcash_primitives::transaction::builder::{Builder, Progress}; use zcash_primitives::transaction::components::amount::{DEFAULT_FEE, MAX_MONEY}; use zcash_primitives::transaction::components::{Amount, OutPoint, TxOut as ZTxOut}; use zcash_primitives::zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}; -use crate::chain::get_latest_height; #[derive(Serialize, Deserialize, Debug)] pub struct Tx { @@ -350,7 +353,8 @@ impl Tx { let fvk = decode_extended_full_viewing_key( chain.network().hrp_sapling_extended_full_viewing_key(), &txin.fvk, - ).map_err(|_| anyhow!("Bech32 Decode Error"))?; + ) + .map_err(|_| anyhow!("Bech32 Decode Error"))?; if fvk != efvk { anyhow::bail!("Incorrect account - Secret key mismatch") } @@ -439,4 +443,3 @@ pub fn get_tx_summary(tx: &Tx) -> anyhow::Result { } Ok(TxSummary { recipients }) } - diff --git a/src/sapling.rs b/src/sapling.rs index e7c1196..184be4f 100644 --- a/src/sapling.rs +++ b/src/sapling.rs @@ -1,7 +1,7 @@ -use std::io::Read; use group::GroupEncoding; use jubjub::{ExtendedNielsPoint, ExtendedPoint, SubgroupPoint}; use lazy_static::lazy_static; +use std::io::Read; use zcash_params::GENERATORS; lazy_static! { @@ -11,8 +11,8 @@ lazy_static! { mod hash; mod note; -pub use note::{SaplingDecrypter, SaplingViewKey, DecryptedSaplingNote}; pub use hash::{SaplingHasher, SAPLING_ROOTS}; +pub use note::{DecryptedSaplingNote, SaplingDecrypter, SaplingViewKey}; fn read_generators_bin() -> Vec { let mut generators_bin = GENERATORS; @@ -31,4 +31,3 @@ fn read_generators_bin() -> Vec { } gens } - diff --git a/src/sapling/hash.rs b/src/sapling/hash.rs index 095bc40..e444c66 100644 --- a/src/sapling/hash.rs +++ b/src/sapling/hash.rs @@ -1,11 +1,11 @@ +use super::GENERATORS_EXP; +use crate::sync::{Hasher, Node}; +use crate::Hash; use ff::PrimeField; use group::Curve; use jubjub::{AffinePoint, ExtendedPoint, Fr}; use lazy_static::lazy_static; use zcash_primitives::constants::PEDERSEN_HASH_CHUNKS_PER_GENERATOR; -use crate::sync::{Hasher, Node}; -use crate::Hash; -use super::GENERATORS_EXP; lazy_static! { pub static ref SAPLING_ROOTS: Vec = { @@ -45,10 +45,7 @@ fn accumulate_generator(acc: &Fr, idx_generator: u32) -> ExtendedPoint { pub fn hash_combine(depth: u8, left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] { let p = hash_combine_inner(depth, left, right); - p - .to_affine() - .get_u() - .to_repr() + p.to_affine().get_u().to_repr() } pub fn hash_combine_inner(depth: u8, left: &[u8; 32], right: &[u8; 32]) -> ExtendedPoint { @@ -95,13 +92,12 @@ pub fn hash_combine_inner(depth: u8, left: &[u8; 32], right: &[u8; 32]) -> Exten v = v >> bit_offset & 0x07; // keep 3 bits accumulate_scalar(&mut acc, &mut cur, v as u8); - if (i+3) % PEDERSEN_HASH_CHUNKS_PER_GENERATOR as u32 == 0 { + if (i + 3) % PEDERSEN_HASH_CHUNKS_PER_GENERATOR as u32 == 0 { hash += accumulate_generator(&acc, idx_generator); idx_generator += 1; acc = Fr::zero(); cur = Fr::one(); - } - else { + } else { cur = cur.double().double().double(); // 2^4 * cur } bit_offset += 3; @@ -136,20 +132,17 @@ impl Hasher for SaplingHasher { fn normalize(&self, extended: &[Self::Extended]) -> Vec { let mut hash_affine = vec![AffinePoint::identity(); extended.len()]; ExtendedPoint::batch_normalize(extended, &mut hash_affine); - hash_affine - .iter() - .map(|p| p.get_u().to_repr()) - .collect() + hash_affine.iter().map(|p| p.get_u().to_repr()).collect() } } #[cfg(test)] mod tests { - use std::convert::TryInto; - use rand::RngCore; - use rand::rngs::OsRng; use crate::hash::pedersen_hash; use crate::sapling::hash::hash_combine; + use rand::rngs::OsRng; + use rand::RngCore; + use std::convert::TryInto; #[test] fn test_hash1() { diff --git a/src/sapling/note.rs b/src/sapling/note.rs index 9fdd113..f504409 100644 --- a/src/sapling/note.rs +++ b/src/sapling/note.rs @@ -1,15 +1,15 @@ -use std::convert::TryInto; +use crate::chain::Nf; +use crate::db::ReceivedNote; +use crate::sync::Node; +use crate::sync::{CompactOutputBytes, DecryptedNote, OutputPosition, TrialDecrypter, ViewKey}; +use crate::CompactTx; use ff::PrimeField; +use std::convert::TryInto; use zcash_note_encryption::Domain; use zcash_primitives::consensus::{BlockHeight, Parameters}; use zcash_primitives::sapling::note_encryption::{PreparedIncomingViewingKey, SaplingDomain}; use zcash_primitives::sapling::{PaymentAddress, SaplingIvk}; use zcash_primitives::zip32::ExtendedFullViewingKey; -use crate::chain::Nf; -use crate::CompactTx; -use crate::db::ReceivedNote; -use crate::sync::Node; -use crate::sync::{CompactOutputBytes, DecryptedNote, OutputPosition, TrialDecrypter, ViewKey}; #[derive(Clone)] pub struct SaplingViewKey { @@ -18,8 +18,10 @@ pub struct SaplingViewKey { pub ivk: SaplingIvk, } -impl ViewKey> for SaplingViewKey { - fn account(&self) -> u32 { self.account } +impl ViewKey> for SaplingViewKey { + fn account(&self) -> u32 { + self.account + } fn ivk(&self) -> as Domain>::IncomingViewingKey { PreparedIncomingViewingKey::new(&self.ivk) } @@ -33,8 +35,14 @@ pub struct DecryptedSaplingNote { pub cmx: Node, } -impl DecryptedNote, SaplingViewKey> for DecryptedSaplingNote { - fn from_parts(vk: SaplingViewKey, note: zcash_primitives::sapling::Note, pa: PaymentAddress, output_position: OutputPosition, cmx: Node) -> Self { +impl DecryptedNote, SaplingViewKey> for DecryptedSaplingNote { + fn from_parts( + vk: SaplingViewKey, + note: zcash_primitives::sapling::Note, + pa: PaymentAddress, + output_position: OutputPosition, + cmx: Node, + ) -> Self { DecryptedSaplingNote { vk, note, @@ -63,34 +71,37 @@ impl DecryptedNote, SaplingViewKey> for Decrypt rcm: self.note.rcm().to_repr().to_vec(), nf: self.note.nf(&viewing_key.nk, position).to_vec(), rho: None, - spent: None + spent: None, } } } #[derive(Clone)] -pub struct SaplingDecrypter { +pub struct SaplingDecrypter { pub network: N, } -impl SaplingDecrypter { +impl SaplingDecrypter { pub fn new(network: N) -> Self { - SaplingDecrypter { - network, - } + SaplingDecrypter { network } } } -impl TrialDecrypter, SaplingViewKey, DecryptedSaplingNote> for SaplingDecrypter { +impl TrialDecrypter, SaplingViewKey, DecryptedSaplingNote> + for SaplingDecrypter +{ fn domain(&self, height: BlockHeight, _cob: &CompactOutputBytes) -> SaplingDomain { SaplingDomain::::for_height(self.network.clone(), height) } fn spends(&self, vtx: &CompactTx) -> Vec { - vtx.spends.iter().map(|co| { - let nf: [u8; 32] = co.nf.clone().try_into().unwrap(); - Nf(nf) - }).collect() + vtx.spends + .iter() + .map(|co| { + let nf: [u8; 32] = co.nf.clone().try_into().unwrap(); + Nf(nf) + }) + .collect() } fn outputs(&self, vtx: &CompactTx) -> Vec { diff --git a/src/scan.rs b/src/scan.rs index 6e32edb..a7fed99 100644 --- a/src/scan.rs +++ b/src/scan.rs @@ -2,26 +2,29 @@ use crate::chain::get_latest_height; use crate::db::AccountViewKey; use serde::Serialize; -use crate::transaction::{get_transaction_details, GetTransactionDetailRequest, retrieve_tx_info}; -use crate::{connect_lightwalletd, CompactBlock, CompactSaplingOutput, CompactTx, DbAdapterBuilder, CoinConfig}; -use crate::chain::{DecryptNode, download_chain}; +use crate::chain::{download_chain, DecryptNode}; +use crate::transaction::{get_transaction_details, retrieve_tx_info, GetTransactionDetailRequest}; +use crate::{ + connect_lightwalletd, CoinConfig, CompactBlock, CompactSaplingOutput, CompactTx, + DbAdapterBuilder, +}; use anyhow::anyhow; use lazy_static::lazy_static; +use orchard::note_encryption::OrchardDomain; use std::collections::HashMap; use std::sync::Arc; -use orchard::note_encryption::OrchardDomain; use tokio::runtime::{Builder, Runtime}; use tokio::sync::mpsc; use tokio::sync::Mutex; use zcash_client_backend::encoding::decode_extended_full_viewing_key; use zcash_primitives::consensus::{Network, Parameters}; -use zcash_primitives::sapling::Note; -use zcash_primitives::sapling::note_encryption::SaplingDomain; use crate::orchard::{DecryptedOrchardNote, OrchardDecrypter, OrchardHasher, OrchardViewKey}; use crate::sapling::{DecryptedSaplingNote, SaplingDecrypter, SaplingHasher, SaplingViewKey}; use crate::sync::{Synchronizer, WarpProcessor}; +use zcash_primitives::sapling::note_encryption::SaplingDomain; +use zcash_primitives::sapling::Note; pub struct Blocks(pub Vec, pub usize); @@ -55,11 +58,23 @@ pub struct TxIdHeight { index: u32, } -type SaplingSynchronizer = Synchronizer, SaplingViewKey, DecryptedSaplingNote, - SaplingDecrypter, SaplingHasher>; +type SaplingSynchronizer = Synchronizer< + Network, + SaplingDomain, + SaplingViewKey, + DecryptedSaplingNote, + SaplingDecrypter, + SaplingHasher, +>; -type OrchardSynchronizer = Synchronizer, OrchardHasher>; +type OrchardSynchronizer = Synchronizer< + Network, + OrchardDomain, + OrchardViewKey, + DecryptedOrchardNote, + OrchardDecrypter, + OrchardHasher, +>; pub async fn sync_async<'a>( coin: u8, @@ -95,11 +110,23 @@ pub async fn sync_async<'a>( let mut height = start_height; let (blocks_tx, mut blocks_rx) = mpsc::channel::(1); tokio::spawn(async move { - download_chain(&mut client, start_height, end_height, prev_hash, max_cost, cancel, blocks_tx).await?; + download_chain( + &mut client, + start_height, + end_height, + prev_hash, + max_cost, + cancel, + blocks_tx, + ) + .await?; Ok::<_, anyhow::Error>(()) }); - let db_builder = DbAdapterBuilder { coin_type: c.coin_type, db_path: db_path.clone() }; + let db_builder = DbAdapterBuilder { + coin_type: c.coin_type, + db_path: db_path.clone(), + }; while let Some(blocks) = blocks_rx.recv().await { let first_block = blocks.0.first().unwrap(); // cannot be empty because blocks are not log::info!("Height: {}", first_block.height); diff --git a/src/sync.rs b/src/sync.rs index f7b4111..56c8c55 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -1,23 +1,31 @@ +use crate::chain::Nf; +use crate::db::{DbAdapterBuilder, ReceivedNote, ReceivedNoteShort}; +use crate::{CompactBlock, DbAdapter}; +use anyhow::Result; +use rayon::prelude::*; use std::collections::HashMap; use std::convert::TryInto; use std::marker::PhantomData; -use anyhow::Result; -use rayon::prelude::*; use zcash_note_encryption::BatchDomain; use zcash_primitives::consensus::Parameters; -use crate::{CompactBlock, DbAdapter}; -use crate::chain::Nf; -use crate::db::{DbAdapterBuilder, ReceivedNote, ReceivedNoteShort}; pub mod tree; pub mod trial_decrypt; -pub use trial_decrypt::{ViewKey, DecryptedNote, TrialDecrypter, CompactOutputBytes, OutputPosition}; -pub use tree::{Hasher, Node, WarpProcessor, Witness, CTree}; use crate::sync::tree::TreeCheckpoint; +pub use tree::{CTree, Hasher, Node, WarpProcessor, Witness}; +pub use trial_decrypt::{ + CompactOutputBytes, DecryptedNote, OutputPosition, TrialDecrypter, ViewKey, +}; -pub struct Synchronizer, VK: ViewKey, DN: DecryptedNote, - TD: TrialDecrypter, H: Hasher> { +pub struct Synchronizer< + N: Parameters, + D: BatchDomain, + VK: ViewKey, + DN: DecryptedNote, + TD: TrialDecrypter, + H: Hasher, +> { pub decrypter: TD, pub warper: WarpProcessor, pub vks: Vec, @@ -31,13 +39,22 @@ pub struct Synchronizer, } -impl + Sync + Send, - VK: ViewKey + Sync + Send, - DN: DecryptedNote + Sync, - TD: TrialDecrypter + Sync, - H: Hasher> Synchronizer { - pub fn new(decrypter: TD, warper: WarpProcessor, vks: Vec, db: DbAdapterBuilder, shielded_pool: String) -> Self { +impl< + N: Parameters + Sync, + D: BatchDomain + Sync + Send, + VK: ViewKey + Sync + Send, + DN: DecryptedNote + Sync, + TD: TrialDecrypter + Sync, + H: Hasher, + > Synchronizer +{ + pub fn new( + decrypter: TD, + warper: WarpProcessor, + vks: Vec, + db: DbAdapterBuilder, + shielded_pool: String, + ) -> Self { Synchronizer { decrypter, warper, @@ -49,13 +66,14 @@ impl Result<()> { let db = self.db.build()?; - let TreeCheckpoint { tree, witnesses } = db.get_tree_by_name(height, &self.shielded_pool)?; + let TreeCheckpoint { tree, witnesses } = + db.get_tree_by_name(height, &self.shielded_pool)?; self.tree = tree; self.witnesses = witnesses; self.note_position = self.tree.get_position(); @@ -67,7 +85,9 @@ impl Result<()> { - if blocks.is_empty() { return Ok(()) } + if blocks.is_empty() { + return Ok(()); + } let decrypter = self.decrypter.clone(); let decrypted_blocks: Vec<_> = blocks .par_iter() @@ -81,21 +101,36 @@ impl , SaplingViewKey, DecryptedSaplingNote, - SaplingDecrypter, SaplingHasher>; + type SaplingSynchronizer = Synchronizer< + Network, + SaplingDomain, + SaplingViewKey, + DecryptedSaplingNote, + SaplingDecrypter, + SaplingHasher, + >; #[test] fn test() { @@ -166,18 +214,20 @@ mod tests { decrypter: SaplingDecrypter::new(*network), warper: WarpProcessor::new(SaplingHasher::default()), vks: vec![], - db: DbAdapterBuilder { coin_type: coin.coin_type, db_path: coin.db_path.as_ref().unwrap().to_owned() }, + db: DbAdapterBuilder { + coin_type: coin.coin_type, + db_path: coin.db_path.as_ref().unwrap().to_owned(), + }, shielded_pool: "sapling".to_string(), tree: CTree::new(), witnesses: vec![], note_position: 0, nullifiers: HashMap::new(), - _phantom: Default::default() + _phantom: Default::default(), }; synchronizer.initialize(1000).unwrap(); synchronizer.process(&vec![]).unwrap(); } - } diff --git a/src/sync/tree.rs b/src/sync/tree.rs index 6d3b68c..961b126 100644 --- a/src/sync/tree.rs +++ b/src/sync/tree.rs @@ -1,10 +1,10 @@ +use crate::Hash; +use byteorder::WriteBytesExt; +use group::Curve; use rayon::prelude::*; use std::io::{Read, Write}; use std::marker::PhantomData; -use byteorder::WriteBytesExt; -use group::Curve; use zcash_encoding::{Optional, Vector}; -use crate::Hash; pub type Node = [u8; 32]; @@ -157,7 +157,12 @@ impl Witness { } } - pub fn auth_path(&self, height: usize, empty_roots: &[Node], hasher: &H) -> Vec { + pub fn auth_path( + &self, + height: usize, + empty_roots: &[Node], + hasher: &H, + ) -> Vec { let mut filled_iter = self.filled.iter(); let mut cursor_used = false; let mut next_filler = move |depth: usize| { @@ -261,7 +266,7 @@ struct CTreeBuilder { hasher: H, } -impl Builder for CTreeBuilder { +impl Builder for CTreeBuilder { type Context = (); type Output = CTree; @@ -309,11 +314,10 @@ impl Builder for CTreeBuilder { fn up(&mut self) { let h = if self.left.is_some() && self.right.is_some() { - Some(self.hasher.node_combine( - self.depth, - &self.left.unwrap(), - &self.right.unwrap(), - )) + Some( + self.hasher + .node_combine(self.depth, &self.left.unwrap(), &self.right.unwrap()), + ) } else { None }; @@ -333,7 +337,9 @@ impl Builder for CTreeBuilder { } fn finished(&self) -> bool { - self.depth as usize >= self.prev_tree.parents.len() && self.left.is_none() && self.right.is_none() + self.depth as usize >= self.prev_tree.parents.len() + && self.left.is_none() + && self.right.is_none() } fn finalize(self, _context: &()) -> CTree { @@ -345,7 +351,7 @@ impl Builder for CTreeBuilder { } } -impl CTreeBuilder { +impl CTreeBuilder { fn new(prev_tree: &CTree, len: usize, first_block: bool, hasher: H) -> Self { let start = prev_tree.get_position(); CTreeBuilder { @@ -380,11 +386,7 @@ impl CTreeBuilder { } #[inline(always)] - fn get<'a>( - commitments: &'a [Node], - index: usize, - offset: &'a Option, - ) -> &'a Node { + fn get<'a>(commitments: &'a [Node], index: usize, offset: &'a Option) -> &'a Node { Self::get_opt(commitments, index, offset).unwrap() } @@ -409,8 +411,7 @@ fn combine_level( let nn = n / 2; let next_level = if nn > 100 { batch_level_combine(commitments, offset, nn, depth, hasher) - } - else { + } else { single_level_combine(commitments, offset, nn, depth, hasher) }; @@ -418,7 +419,13 @@ fn combine_level( nn } -fn batch_level_combine(commitments: &mut [Node], offset: Option, nn: usize, depth: u8, hasher: &H) -> Vec { +fn batch_level_combine( + commitments: &mut [Node], + offset: Option, + nn: usize, + depth: u8, + hasher: &H, +) -> Vec { let hash_extended: Vec<_> = (0..nn) .into_par_iter() .map(|i| { @@ -432,7 +439,13 @@ fn batch_level_combine(commitments: &mut [Node], offset: Option hasher.normalize(&hash_extended) } -fn single_level_combine(commitments: &mut [Node], offset: Option, nn: usize, depth: u8, hasher: &H) -> Vec { +fn single_level_combine( + commitments: &mut [Node], + offset: Option, + nn: usize, + depth: u8, + hasher: &H, +) -> Vec { (0..nn) .into_par_iter() .map(|i| { @@ -449,10 +462,10 @@ struct WitnessBuilder { witness: Witness, p: usize, inside: bool, - _phantom: PhantomData + _phantom: PhantomData, } -impl WitnessBuilder { +impl WitnessBuilder { fn new(tree_builder: &CTreeBuilder, prev_witness: &Witness, count: usize) -> Self { let position = prev_witness.position; // log::info!("Witness::new - {} {},{}", position, tree_builder.start, tree_builder.start + count); @@ -466,7 +479,7 @@ impl WitnessBuilder { } } -impl Builder for WitnessBuilder { +impl Builder for WitnessBuilder { type Context = CTreeBuilder; type Output = Witness; @@ -514,7 +527,9 @@ impl Builder for WitnessBuilder { if tree.right.is_none() { self.witness.filled.push(*p1); } - } else if depth as usize > tree.parents.len() || tree.parents[depth as usize - 1].is_none() { + } else if depth as usize > tree.parents.len() + || tree.parents[depth as usize - 1].is_none() + { self.witness.filled.push(*p1); } } @@ -577,7 +592,7 @@ pub struct WarpProcessor { hasher: H, } -impl WarpProcessor { +impl WarpProcessor { pub fn new(hasher: H) -> WarpProcessor { WarpProcessor { prev_tree: CTree::new(), @@ -599,9 +614,7 @@ impl WarpProcessor { return; } self.prev_witnesses.extend_from_slice(new_witnesses); - let (t, ws) = self.advance_tree( - nodes, - ); + let (t, ws) = self.advance_tree(nodes); self.first_block = false; self.prev_tree = t; self.prev_witnesses = ws; @@ -616,12 +629,15 @@ impl WarpProcessor { } } - fn advance_tree( - &self, - mut commitments: &mut [Node], - ) -> (CTree, Vec) { - let mut builder = CTreeBuilder::::new(&self.prev_tree, commitments.len(), self.first_block, self.hasher.clone()); - let mut witness_builders: Vec<_> = self.prev_witnesses + fn advance_tree(&self, mut commitments: &mut [Node]) -> (CTree, Vec) { + let mut builder = CTreeBuilder::::new( + &self.prev_tree, + commitments.len(), + self.first_block, + self.hasher.clone(), + ); + let mut witness_builders: Vec<_> = self + .prev_witnesses .iter() .map(|witness| WitnessBuilder::new(&builder, witness, commitments.len())) .collect(); diff --git a/src/sync/trial_decrypt.rs b/src/sync/trial_decrypt.rs index e146f99..848c51f 100644 --- a/src/sync/trial_decrypt.rs +++ b/src/sync/trial_decrypt.rs @@ -1,15 +1,15 @@ -use std::collections::HashMap; use crate::chain::Nf; +use crate::db::ReceivedNote; +use crate::sync::tree::Node; +use crate::{CompactBlock, CompactOrchardAction, CompactSaplingOutput, CompactTx}; +use orchard::note_encryption::OrchardDomain; +use std::collections::HashMap; use std::convert::TryInto; use std::marker::PhantomData; use std::time::Instant; -use orchard::note_encryption::OrchardDomain; use zcash_note_encryption::batch::try_compact_note_decryption; -use zcash_note_encryption::{BatchDomain, COMPACT_NOTE_SIZE, EphemeralKeyBytes, ShieldedOutput}; +use zcash_note_encryption::{BatchDomain, EphemeralKeyBytes, ShieldedOutput, COMPACT_NOTE_SIZE}; use zcash_primitives::consensus::{BlockHeight, Parameters}; -use crate::{CompactBlock, CompactOrchardAction, CompactSaplingOutput, CompactTx}; -use crate::db::ReceivedNote; -use crate::sync::tree::Node; pub struct DecryptedBlock> { pub height: u32, @@ -44,7 +44,13 @@ pub struct OutputPosition { } pub trait DecryptedNote: Send + Sync { - fn from_parts(vk: VK, note: D::Note, pa: D::Recipient, output_position: OutputPosition, cmx: Node) -> Self; + fn from_parts( + vk: VK, + note: D::Note, + pa: D::Recipient, + output_position: OutputPosition, + cmx: Node, + ) -> Self; fn position(&self, block_offset: usize) -> usize; fn cmx(&self) -> Node; fn to_received_note(&self, position: u64) -> ReceivedNote; @@ -85,9 +91,17 @@ impl From<&CompactSaplingOutput> for CompactOutputBytes { fn from(co: &CompactSaplingOutput) -> Self { CompactOutputBytes { nullifier: [0u8; 32], - epk: if co.epk.is_empty() { [0u8; 32] } else { co.epk.clone().try_into().unwrap() } , + epk: if co.epk.is_empty() { + [0u8; 32] + } else { + co.epk.clone().try_into().unwrap() + }, cmx: co.cmu.clone().try_into().unwrap(), // cannot be filtered out - ciphertext: if co.ciphertext.is_empty() { [0u8; 52] } else { co.ciphertext.clone().try_into().unwrap() }, + ciphertext: if co.ciphertext.is_empty() { + [0u8; 52] + } else { + co.ciphertext.clone().try_into().unwrap() + }, } } } @@ -96,18 +110,25 @@ impl From<&CompactOrchardAction> for CompactOutputBytes { fn from(co: &CompactOrchardAction) -> Self { CompactOutputBytes { nullifier: co.nullifier.clone().try_into().unwrap(), - epk: if co.ephemeral_key.is_empty() { [0u8; 32] } else { co.ephemeral_key.clone().try_into().unwrap() } , + epk: if co.ephemeral_key.is_empty() { + [0u8; 32] + } else { + co.ephemeral_key.clone().try_into().unwrap() + }, cmx: co.cmx.clone().try_into().unwrap(), // cannot be filtered out - ciphertext: if co.ciphertext.is_empty() { [0u8; 52] } else { co.ciphertext.clone().try_into().unwrap() }, + ciphertext: if co.ciphertext.is_empty() { + [0u8; 52] + } else { + co.ciphertext.clone().try_into().unwrap() + }, } } } - pub struct CompactShieldedOutput(CompactOutputBytes, OutputPosition); impl> ShieldedOutput -for CompactShieldedOutput + for CompactShieldedOutput { fn ephemeral_key(&self) -> EphemeralKeyBytes { EphemeralKeyBytes(self.0.epk) @@ -120,12 +141,14 @@ for CompactShieldedOutput } } -pub trait TrialDecrypter, VK: ViewKey, DN: DecryptedNote>: Clone { - fn decrypt_notes( - &self, - block: &CompactBlock, - vks: &[VK], - ) -> DecryptedBlock { +pub trait TrialDecrypter< + N: Parameters, + D: BatchDomain, + VK: ViewKey, + DN: DecryptedNote, +>: Clone +{ + fn decrypt_notes(&self, block: &CompactBlock, vks: &[VK]) -> DecryptedBlock { let height = BlockHeight::from_u32(block.height as u32); let mut count_outputs = 0u32; let mut spends: Vec = vec![]; @@ -161,33 +184,31 @@ pub trait TrialDecrypter Vec; fn outputs(&self, vtx: &CompactTx) -> Vec; } - diff --git a/src/transaction.rs b/src/transaction.rs index 0c4fd42..fd722ec 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -1,11 +1,12 @@ -use std::collections::HashMap; use crate::contact::{Contact, ContactDecoder}; +use crate::unified::orchard_as_unified; use crate::{AccountData, CoinConfig, CompactTxStreamerClient, DbAdapter, Hash, TxFilter}; -use std::convert::TryFrom; -use serde::Serialize; use orchard::keys::{FullViewingKey, IncomingViewingKey, OutgoingViewingKey, Scope}; use orchard::note_encryption::OrchardDomain; use orchard::value::ValueCommitment; +use serde::Serialize; +use std::collections::HashMap; +use std::convert::TryFrom; use tonic::transport::Channel; use tonic::Request; use zcash_address::{ToAddress, ZcashAddress}; @@ -16,10 +17,11 @@ use zcash_note_encryption::{try_note_decryption, try_output_recovery_with_ovk}; use zcash_params::coin::get_branch; use zcash_primitives::consensus::{BlockHeight, Network, Parameters}; use zcash_primitives::memo::{Memo, MemoBytes}; -use zcash_primitives::sapling::note_encryption::{PreparedIncomingViewingKey, try_sapling_note_decryption, try_sapling_output_recovery}; +use zcash_primitives::sapling::note_encryption::{ + try_sapling_note_decryption, try_sapling_output_recovery, PreparedIncomingViewingKey, +}; use zcash_primitives::sapling::SaplingIvk; use zcash_primitives::transaction::Transaction; -use crate::unified::orchard_as_unified; #[derive(Debug)] pub struct ContactRef { @@ -48,7 +50,15 @@ pub async fn get_transaction_details(coin: u8) -> anyhow::Result<()> { let mut details = vec![]; for req in reqs.iter() { - let tx_details = retrieve_tx_info(network, &mut client, req.height, req.id_tx, &req.txid, &keys[&req.account]).await?; + let tx_details = retrieve_tx_info( + network, + &mut client, + req.height, + req.id_tx, + &req.txid, + &keys[&req.account], + ) + .await?; log::info!("{:?}", tx_details); details.push(tx_details); } @@ -65,7 +75,12 @@ pub async fn get_transaction_details(coin: u8) -> anyhow::Result<()> { Ok(()) } -async fn fetch_raw_transaction(network: &Network, client: &mut CompactTxStreamerClient, height: u32, txid: &Hash) -> anyhow::Result { +async fn fetch_raw_transaction( + network: &Network, + client: &mut CompactTxStreamerClient, + height: u32, + txid: &Hash, +) -> anyhow::Result { let consensus_branch_id = get_branch(network, height); let tx_filter = TxFilter { block: None, @@ -83,7 +98,7 @@ async fn fetch_raw_transaction(network: &Network, client: &mut CompactTxStreamer #[derive(Clone)] pub struct DecryptionKeys { sapling_keys: (SaplingIvk, zcash_primitives::keys::OutgoingViewingKey), - orchard_keys: Option<(IncomingViewingKey, OutgoingViewingKey)> + orchard_keys: Option<(IncomingViewingKey, OutgoingViewingKey)>, } pub fn decode_transaction( @@ -122,18 +137,28 @@ pub fn decode_transaction( let mut contact_decoder = ContactDecoder::new(sapling_bundle.shielded_outputs.len()); for output in sapling_bundle.shielded_outputs.iter() { let pivk = PreparedIncomingViewingKey::new(&sapling_ivk); - if let Some((_note, pa, memo)) = try_sapling_note_decryption(network, height, &pivk, output) { + if let Some((_note, pa, memo)) = + try_sapling_note_decryption(network, height, &pivk, output) + { let memo = Memo::try_from(memo)?; if zaddress.is_none() { - zaddress = Some(encode_payment_address(network.hrp_sapling_payment_address(), &pa)); + zaddress = Some(encode_payment_address( + network.hrp_sapling_payment_address(), + &pa, + )); } if memo != Memo::Empty { tx_memo = memo; } } - if let Some((_note, pa, memo, ..)) = try_sapling_output_recovery(network, height, &sapling_ovk, output) { + if let Some((_note, pa, memo, ..)) = + try_sapling_output_recovery(network, height, &sapling_ovk, output) + { let _ = contact_decoder.add_memo(&memo); // ignore memo that is not for contacts, if we cannot decode it with ovk, we didn't make create this memo - zaddress = Some(encode_payment_address(network.hrp_sapling_payment_address(), &pa)); + zaddress = Some(encode_payment_address( + network.hrp_sapling_payment_address(), + &pa, + )); let memo = Memo::try_from(memo)?; if memo != Memo::Empty { tx_memo = memo; @@ -147,7 +172,8 @@ pub fn decode_transaction( if let Some((orchard_ivk, orchard_ovk)) = decryption_keys.orchard_keys.clone() { for action in orchard_bundle.actions().iter() { let domain = OrchardDomain::for_action(action); - if let Some((_note, pa, memo)) = try_note_decryption(&domain, &orchard_ivk, action) { + if let Some((_note, pa, memo)) = try_note_decryption(&domain, &orchard_ivk, action) + { let memo = Memo::try_from(MemoBytes::from_bytes(&memo)?)?; if oaddress.is_none() { oaddress = Some(orchard_as_unified(network, &pa).encode()); @@ -156,8 +182,13 @@ pub fn decode_transaction( tx_memo = memo; } } - if let Some((_note, pa, memo, ..)) = try_output_recovery_with_ovk(&domain, &orchard_ovk, action, - action.cv_net(), &action.encrypted_note().out_ciphertext) { + if let Some((_note, pa, memo, ..)) = try_output_recovery_with_ovk( + &domain, + &orchard_ovk, + action, + action.cv_net(), + &action.encrypted_note().out_ciphertext, + ) { let memo = Memo::try_from(MemoBytes::from_bytes(&memo)?)?; oaddress = Some(orchard_as_unified(network, &pa).encode()); if memo != Memo::Empty { @@ -186,9 +217,15 @@ pub fn decode_transaction( Ok(tx_details) } -fn get_decryption_keys(network: &Network, account: u32, db: &DbAdapter) -> anyhow::Result { +fn get_decryption_keys( + network: &Network, + account: u32, + db: &DbAdapter, +) -> anyhow::Result { let AccountData { fvk, .. } = db.get_account_info(account)?; - let fvk = decode_extended_full_viewing_key(network.hrp_sapling_extended_full_viewing_key(), &fvk).unwrap(); + let fvk = + decode_extended_full_viewing_key(network.hrp_sapling_extended_full_viewing_key(), &fvk) + .unwrap(); let (sapling_ivk, sapling_ovk) = (fvk.fvk.vk.ivk(), fvk.fvk.ovk); let okey = db.get_orchard(account)?; @@ -209,7 +246,7 @@ pub async fn retrieve_tx_info( height: u32, id_tx: u32, txid: &Hash, - decryption_keys: &DecryptionKeys + decryption_keys: &DecryptionKeys, ) -> anyhow::Result { let transaction = fetch_raw_transaction(network, client, height, txid).await?; let tx_details = decode_transaction(network, height, id_tx, transaction, &decryption_keys)?; diff --git a/src/unified.rs b/src/unified.rs index cd723c3..21e0511 100644 --- a/src/unified.rs +++ b/src/unified.rs @@ -1,13 +1,15 @@ +use crate::{AccountData, DbAdapter}; use anyhow::anyhow; -use orchard::Address; use orchard::keys::{FullViewingKey, Scope}; -use zcash_address::{ToAddress, unified, ZcashAddress}; +use orchard::Address; use zcash_address::unified::{Container, Encoding, Receiver}; -use zcash_client_backend::encoding::{AddressCodec, decode_payment_address, encode_payment_address}; +use zcash_address::{unified, ToAddress, ZcashAddress}; +use zcash_client_backend::encoding::{ + decode_payment_address, encode_payment_address, AddressCodec, +}; use zcash_primitives::consensus::{Network, Parameters}; use zcash_primitives::legacy::TransparentAddress; use zcash_primitives::sapling::PaymentAddress; -use crate::{AccountData, DbAdapter}; pub struct UnifiedAddressType { pub transparent: bool, @@ -24,9 +26,13 @@ pub struct DecodedUA { impl std::fmt::Display for DecodedUA { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "DecodedUA: {:?} {:?} {:?}", + write!( + f, + "DecodedUA: {:?} {:?} {:?}", self.transparent.as_ref().map(|a| a.encode(&self.network)), - self.sapling.as_ref().map(|a| encode_payment_address(self.network.hrp_sapling_payment_address(), a)), + self.sapling + .as_ref() + .map(|a| encode_payment_address(self.network.hrp_sapling_payment_address(), a)), self.orchard.as_ref().map(|a| { let ua = unified::Address(vec![Receiver::Orchard(a.to_raw_address_bytes())]); ua.encode(&self.network.address_network().unwrap()) @@ -35,8 +41,15 @@ impl std::fmt::Display for DecodedUA { } } -pub fn get_unified_address(network: &Network, db: &DbAdapter, account: u32, tpe: Option) -> anyhow::Result { - let tpe = tpe.ok_or(anyhow!("")).or_else(|_| db.get_ua_settings(account))?; +pub fn get_unified_address( + network: &Network, + db: &DbAdapter, + account: u32, + tpe: Option, +) -> anyhow::Result { + let tpe = tpe + .ok_or(anyhow!("")) + .or_else(|_| db.get_ua_settings(account))?; let mut rcvs = vec![]; if tpe.transparent { @@ -50,7 +63,7 @@ pub fn get_unified_address(network: &Network, db: &DbAdapter, account: u32, tpe: } } if tpe.sapling { - let AccountData { address , .. } = db.get_account_info(account)?; + let AccountData { address, .. } = db.get_account_info(account)?; let pa = decode_payment_address(network.hrp_sapling_payment_address(), &address).unwrap(); let rcv = Receiver::Sapling(pa.to_bytes()); rcvs.push(rcv); @@ -75,7 +88,7 @@ pub fn decode_unified_address(network: &Network, ua: &str) -> anyhow::Result