This commit is contained in:
Hanh 2022-11-06 20:50:51 +08:00
parent f23cd2376f
commit 4cafff16a4
46 changed files with 2130 additions and 1225 deletions

View File

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

View File

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

View File

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

View File

@ -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<String> {
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<String> {
pub fn get_unified_address(
coin: u8,
id_account: u32,
t: bool,
s: bool,
o: bool,
) -> anyhow::Result<String> {
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)

View File

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

View File

@ -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<i64> {
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())

View File

@ -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<RecipientShort> for RecipientMemo {
address: r.address,
amount: r.amount,
memo: Memo::Empty,
max_amount_per_note: 0
max_amount_per_note: 0,
}
}
}

View File

@ -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<TransactionPlan> {
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::<FeeZIP327>(&fvk, last_height, &utxos, &orders, config)?;
Ok(tx_plan)
}

View File

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

View File

@ -355,7 +355,10 @@ fn decrypt_notes<'a, N: Parameters>(
let mut count_outputs = 0u32;
let mut spends: Vec<Nf> = vec![];
let mut notes: Vec<DecryptedNote> = 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<N>, AccountOutput<N>)> = vec![];
for (tx_index, vtx) in block.vtx.iter().enumerate() {
for cs in vtx.spends.iter() {

View File

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

218
src/db.rs
View File

@ -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<String>,
}
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> {
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<Vec<OrchardViewKey>> {
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<u8> = 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<u8> = 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<u8> = 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<TreeCheckpoint> {
let tree = self.connection.query_row(
&format!("SELECT tree FROM {}_tree WHERE height = ?1", shielded_pool),
[height], |row| {
let tree: Vec<u8> = row.get(0)?;
Ok(tree)
}).optional()?;
pub fn get_tree_by_name(
&self,
height: u32,
shielded_pool: &str,
) -> anyhow::Result<TreeCheckpoint> {
let tree = self
.connection
.query_row(
&format!("SELECT tree FROM {}_tree WHERE height = ?1", shielded_pool),
[height],
|row| {
let tree: Vec<u8> = 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<Vec<ReceivedNoteShort>> {
pub fn get_unspent_nullifiers(&self) -> anyhow::Result<Vec<ReceivedNoteShort>> {
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<Vec<UTXO>> {
pub fn get_unspent_received_notes(
&self,
account: u32,
anchor_height: u32,
) -> anyhow::Result<Vec<UTXO>> {
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<Option<u32>> {
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<Option<OrchardKeyBytes>> {
let key = self.connection.query_row("SELECT sk, fvk FROM orchard_addrs WHERE account = ?1", params![account], |row| {
let sk: Vec<u8> = row.get(0)?;
let fvk: Vec<u8> = 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<u8> = row.get(0)?;
let fvk: Vec<u8> = 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<UnifiedAddressType> {
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<Vec<GetTransactionDetailRequest>> {
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() {

View File

@ -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<u32> {
let version: Option<u32> = 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(())
}

View File

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

View File

@ -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<ExtendedNielsPoint> = read_generators_bin();

View File

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

View File

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

16
src/main/opt.rs Normal file
View File

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

View File

@ -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<bool> = 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::<Config>())
@ -182,7 +185,13 @@ pub fn get_address() -> Result<String, Error> {
#[get("/unified_address?<t>&<s>&<o>")]
pub fn get_unified_address(t: u8, s: u8, o: u8) -> Result<String, Error> {
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<String, Error> {
Ok(tx_id)
}
#[post(
"/get_tx_plan?<confirmations>",
data = "<recipients>"
)]
pub async fn get_tx_plan(
confirmations: u32,
recipients: Json<Vec<RecipientShort>>,
) -> Result<Json<TransactionPlan>, 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 = "<tx_plan>")]
pub async fn build_from_plan(tx_plan: Json<TransactionPlan>) -> Result<String, Error> {
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<String, Error> {
let address = warp_api_ffi::api::account::new_diversified_address()?;

View File

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

View File

@ -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<T> = std::result::Result<T, TransactionBuilderError>;
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<Vec<Order>> {
let orders: Result<Vec<_>> = 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<TransactionPlan>
{
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::<FeeZIP327>(&utxos, &mut orders, config)?;
Ok(tx_plan)
}

View File

@ -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<SecretKey>,
@ -41,24 +43,24 @@ pub struct SecretKeys {
pub orchard: Option<SpendingKey>,
}
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<Self> {
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<Vec<u8>> {
pub fn build_tx(
network: &Network,
skeys: &SecretKeys,
plan: &TransactionPlan,
context: TxBuilderContext,
mut rng: impl RngCore + CryptoRng + Clone,
) -> anyhow::Result<Vec<u8>> {
let secp = Secp256k1::<All>::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<Bundle<_, Amount>> = 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<zcash_primitives::transaction::Authorized> =
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<SecretKeys> {
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::<FeeFlat>(&utxos, &mut orders, &config).unwrap();
let tx_plan =
build_tx_plan::<FeeFlat>("", 0, &utxos, &mut orders, &config).unwrap();
log::info!("Plan: {}", serde_json::to_string(&tx_plan).unwrap());
log::info!("Building tx");

View File

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

View File

@ -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<Order> {
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<Execution> {
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<Fill>) {
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<Self> {
match self {
Pool::Transparent => None,
Pool::Sapling => Some(Pool::Orchard),
Pool::Orchard => Some(Pool::Sapling),
}
}
}
#[cfg(test)]
mod tests {
}

View File

@ -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<PoolAllocation> {
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<OrderInfo>, 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<FundAllocation> {
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<Vec<Fill>> {
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<UTXO>, 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<Destination>; 3], change: &PoolAllocation) -> Result<Vec<Fill>> {
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<F: FeeCalculator>(fvk: &str, height: u32, utxos: &[UTXO], orders: &[Order], config: &TransactionBuilderConfig) -> Result<TransactionPlan> {
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(&notes, &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!(),
}
}

View File

@ -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<Vec<UTXO>> {
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<Fill>,
}
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<bool> {
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<Vec<UTXO>> {
select_notes(&self.pool_used, &utxos)
}
}
const ANY_DESTINATION: [Option<Destination>; 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<F: FeeCalculator>(utxos: &[UTXO], orders: &mut [Order], config: &NoteSelectConfig) -> anyhow::Result<TransactionPlan> {
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(&notes, &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::<Vec<_>>().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(&notes, &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)
}

23
src/note_selection/ser.rs Normal file
View File

@ -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<u8>,
);
fn get_memo_bytes(memo: &MemoBytes) -> Vec<u8> {
memo.as_slice().to_vec()
}
impl From<MemoBytesProxy> for MemoBytes {
fn from(p: MemoBytesProxy) -> MemoBytes {
MemoBytes::from_bytes(&p.0).unwrap()
}
}

View File

@ -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::<FeeZIP327>(&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::<FeeZIP327>(&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::<FeeZIP327>(&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::<FeeZIP327>("", 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::<FeeZIP327>(&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<SimpleTxIO>,
outputs: Vec<SimpleTxIO>,
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::<FeeZIP327>(&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::<FeeZIP327>(&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::<FeeZIP327>(&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::<FeeZIP327>(&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<TransactionPlan> 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::<FeeZIP327>(&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::<FeeZIP327>(&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<UTXO> {
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
}

View File

@ -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::<Strict>")] txid: [u8; 32],
#[serde(with = "SerHex::<Strict>")]
txid: [u8; 32],
index: u32,
},
Sapling {
id_note: u32,
#[serde(with = "SerHex::<Strict>")] diversifier: [u8; 11],
#[serde(with = "SerHex::<Strict>")] rseed: [u8; 32],
#[serde_as(as = "serde_with::hex::Hex")] witness: Vec<u8>,
#[serde(with = "SerHex::<Strict>")]
diversifier: [u8; 11],
#[serde(with = "SerHex::<Strict>")]
rseed: [u8; 32],
#[serde_as(as = "serde_with::hex::Hex")]
witness: Vec<u8>,
},
Orchard {
id_note: u32,
#[serde(with = "SerHex::<Strict>")] diversifier: [u8; 11],
#[serde(with = "SerHex::<Strict>")] rseed: [u8; 32],
#[serde(with = "SerHex::<Strict>")] rho: [u8; 32],
#[serde_as(as = "serde_with::hex::Hex")] witness: Vec<u8>,
#[serde(with = "SerHex::<Strict>")]
diversifier: [u8; 11],
#[serde(with = "SerHex::<Strict>")]
rseed: [u8; 32],
#[serde(with = "SerHex::<Strict>")]
rho: [u8; 32],
#[serde_as(as = "serde_with::hex::Hex")]
witness: Vec<u8>,
},
}
#[derive(Clone, Copy, Serialize, Debug)]
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
#[serde_as]
pub enum Destination {
Transparent(#[serde(with = "SerHex::<Strict>")] [u8; 20]), // MD5
Sapling(#[serde(with = "SerHex::<Strict>")] [u8; 43]), // Diversifier + Jubjub Point
Orchard(#[serde(with = "SerHex::<Strict>")] [u8; 43]), // Diviersifer + Pallas Point
Sapling(#[serde(with = "SerHex::<Strict>")] [u8; 43]), // Diversifier + Jubjub Point
Orchard(#[serde(with = "SerHex::<Strict>")] [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<Destination>; 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<u8>
);
fn get_memo_bytes(memo: &MemoBytes) -> Vec<u8> {
memo.as_slice().to_vec()
}
impl From<MemoBytesProxy> 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<Fill>,
}
#[derive(Serialize, Default)]
pub struct TransactionPlan {
pub spends: Vec<UTXO>,
pub outputs: Vec<Fill>,
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<Destination>; 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<u32>,
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<UTXO>,
pub outputs: Vec<Fill>,
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,
}
}
}

42
src/note_selection/ua.rs Normal file
View File

@ -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<Destination>; 3]> {
let mut destinations: [Option<Destination>; 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)
}

View File

@ -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<Vec<UTXO>> {
pub async fn fetch_utxos(
coin: u8,
account: u32,
last_height: u32,
use_transparent_inputs: bool,
anchor_offset: u32,
) -> anyhow::Result<Vec<UTXO>> {
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<Vec<UTXO>> {
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![])
}
}

View File

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

View File

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

View File

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

View File

@ -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<OrchardDomain, OrchardViewKey> 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<OrchardDomain, OrchardViewKey> 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<N> {
pub network: N,
}
impl <N> OrchardDecrypter<N> {
impl<N> OrchardDecrypter<N> {
pub fn new(network: N) -> Self {
OrchardDecrypter {
network,
}
OrchardDecrypter { network }
}
}
impl <N: Parameters> TrialDecrypter<N, OrchardDomain, OrchardViewKey, DecryptedOrchardNote> for OrchardDecrypter<N> {
impl<N: Parameters> TrialDecrypter<N, OrchardDomain, OrchardViewKey, DecryptedOrchardNote>
for OrchardDecrypter<N>
{
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<Nf> {
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<CompactOutputBytes> {
@ -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(())
}

View File

@ -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<TxSummary> {
}
Ok(TxSummary { recipients })
}

View File

@ -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<ExtendedNielsPoint> {
let mut generators_bin = GENERATORS;
@ -31,4 +31,3 @@ fn read_generators_bin() -> Vec<ExtendedNielsPoint> {
}
gens
}

View File

@ -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<Hash> = {
@ -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<Node> {
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() {

View File

@ -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 <P: Parameters> ViewKey<SaplingDomain<P>> for SaplingViewKey {
fn account(&self) -> u32 { self.account }
impl<P: Parameters> ViewKey<SaplingDomain<P>> for SaplingViewKey {
fn account(&self) -> u32 {
self.account
}
fn ivk(&self) -> <SaplingDomain<P> as Domain>::IncomingViewingKey {
PreparedIncomingViewingKey::new(&self.ivk)
}
@ -33,8 +35,14 @@ pub struct DecryptedSaplingNote {
pub cmx: Node,
}
impl <P: Parameters> DecryptedNote<SaplingDomain<P>, SaplingViewKey> for DecryptedSaplingNote {
fn from_parts(vk: SaplingViewKey, note: zcash_primitives::sapling::Note, pa: PaymentAddress, output_position: OutputPosition, cmx: Node) -> Self {
impl<P: Parameters> DecryptedNote<SaplingDomain<P>, 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 <P: Parameters> DecryptedNote<SaplingDomain<P>, 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<N> {
pub struct SaplingDecrypter<N> {
pub network: N,
}
impl <N> SaplingDecrypter<N> {
impl<N> SaplingDecrypter<N> {
pub fn new(network: N) -> Self {
SaplingDecrypter {
network,
}
SaplingDecrypter { network }
}
}
impl <N: Parameters> TrialDecrypter<N, SaplingDomain<N>, SaplingViewKey, DecryptedSaplingNote> for SaplingDecrypter<N> {
impl<N: Parameters> TrialDecrypter<N, SaplingDomain<N>, SaplingViewKey, DecryptedSaplingNote>
for SaplingDecrypter<N>
{
fn domain(&self, height: BlockHeight, _cob: &CompactOutputBytes) -> SaplingDomain<N> {
SaplingDomain::<N>::for_height(self.network.clone(), height)
}
fn spends(&self, vtx: &CompactTx) -> Vec<Nf> {
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<CompactOutputBytes> {

View File

@ -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<CompactBlock>, pub usize);
@ -55,11 +58,23 @@ pub struct TxIdHeight {
index: u32,
}
type SaplingSynchronizer = Synchronizer<Network, SaplingDomain<Network>, SaplingViewKey, DecryptedSaplingNote,
SaplingDecrypter<Network>, SaplingHasher>;
type SaplingSynchronizer = Synchronizer<
Network,
SaplingDomain<Network>,
SaplingViewKey,
DecryptedSaplingNote,
SaplingDecrypter<Network>,
SaplingHasher,
>;
type OrchardSynchronizer = Synchronizer<Network, OrchardDomain, OrchardViewKey, DecryptedOrchardNote,
OrchardDecrypter<Network>, OrchardHasher>;
type OrchardSynchronizer = Synchronizer<
Network,
OrchardDomain,
OrchardViewKey,
DecryptedOrchardNote,
OrchardDecrypter<Network>,
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::<Blocks>(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);

View File

@ -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<N: Parameters, D: BatchDomain<ExtractedCommitmentBytes = [u8; 32]>, VK: ViewKey<D>, DN: DecryptedNote<D, VK>,
TD: TrialDecrypter<N, D, VK, DN>, H: Hasher> {
pub struct Synchronizer<
N: Parameters,
D: BatchDomain<ExtractedCommitmentBytes = [u8; 32]>,
VK: ViewKey<D>,
DN: DecryptedNote<D, VK>,
TD: TrialDecrypter<N, D, VK, DN>,
H: Hasher,
> {
pub decrypter: TD,
pub warper: WarpProcessor<H>,
pub vks: Vec<VK>,
@ -31,13 +39,22 @@ pub struct Synchronizer<N: Parameters, D: BatchDomain<ExtractedCommitmentBytes =
pub _phantom: PhantomData<(N, D, DN)>,
}
impl <N: Parameters + Sync,
D: BatchDomain<ExtractedCommitmentBytes = [u8; 32]> + Sync + Send,
VK: ViewKey<D> + Sync + Send,
DN: DecryptedNote<D, VK> + Sync,
TD: TrialDecrypter<N, D, VK, DN> + Sync,
H: Hasher> Synchronizer<N, D, VK, DN, TD, H> {
pub fn new(decrypter: TD, warper: WarpProcessor<H>, vks: Vec<VK>, db: DbAdapterBuilder, shielded_pool: String) -> Self {
impl<
N: Parameters + Sync,
D: BatchDomain<ExtractedCommitmentBytes = [u8; 32]> + Sync + Send,
VK: ViewKey<D> + Sync + Send,
DN: DecryptedNote<D, VK> + Sync,
TD: TrialDecrypter<N, D, VK, DN> + Sync,
H: Hasher,
> Synchronizer<N, D, VK, DN, TD, H>
{
pub fn new(
decrypter: TD,
warper: WarpProcessor<H>,
vks: Vec<VK>,
db: DbAdapterBuilder,
shielded_pool: String,
) -> Self {
Synchronizer {
decrypter,
warper,
@ -49,13 +66,14 @@ impl <N: Parameters + Sync,
nullifiers: HashMap::default(),
tree: CTree::new(),
witnesses: vec![],
_phantom: Default::default()
_phantom: Default::default(),
}
}
pub fn initialize(&mut self, height: u32) -> 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 <N: Parameters + Sync,
}
pub fn process(&mut self, blocks: &[CompactBlock]) -> 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 <N: Parameters + Sync,
let mut new_witnesses = vec![];
for decb in decrypted_blocks.iter() {
for dectx in decb.txs.iter() {
let id_tx = DbAdapter::store_transaction(&dectx.tx_id, dectx.account, dectx.height, dectx.timestamp, dectx.tx_index as u32, &db_tx)?;
let id_tx = DbAdapter::store_transaction(
&dectx.tx_id,
dectx.account,
dectx.height,
dectx.timestamp,
dectx.tx_index as u32,
&db_tx,
)?;
let mut balance: i64 = 0;
for decn in dectx.notes.iter() {
let position = decn.position(self.note_position);
let rn: ReceivedNote = decn.to_received_note(position as u64);
let id_note = DbAdapter::store_received_note(&rn, id_tx, position, &db_tx)?;
let nf = Nf(rn.nf.try_into().unwrap());
self.nullifiers.insert(nf, ReceivedNoteShort {
id: id_note,
account: rn.account,
self.nullifiers.insert(
nf,
value: rn.value
});
ReceivedNoteShort {
id: id_note,
account: rn.account,
nf,
value: rn.value,
},
);
let witness = Witness::new(position, id_note, &decn.cmx());
log::info!("Witness {} {} {}", witness.position, witness.id_note, hex::encode(witness.cmx));
log::info!(
"Witness {} {} {}",
witness.position,
witness.id_note,
hex::encode(witness.cmx)
);
new_witnesses.push(witness);
balance += rn.value as i64;
}
@ -112,7 +147,14 @@ impl <N: Parameters + Sync,
for (tx_index, tx) in b.vtx.iter().enumerate() {
for sp in self.decrypter.spends(tx).iter() {
if let Some(rn) = self.nullifiers.get(sp) {
let id_tx = DbAdapter::store_transaction(&tx.hash, rn.account, b.height as u32, b.time, tx_index as u32, &db_tx)?;
let id_tx = DbAdapter::store_transaction(
&tx.hash,
rn.account,
b.height as u32,
b.time,
tx_index as u32,
&db_tx,
)?;
DbAdapter::add_value(id_tx, -(rn.value as i64), &db_tx)?;
DbAdapter::mark_spent(rn.id, b.height as u32, &db_tx)?;
self.nullifiers.remove(sp);
@ -143,19 +185,25 @@ impl <N: Parameters + Sync,
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use zcash_primitives::consensus::Network;
use zcash_primitives::sapling::note_encryption::SaplingDomain;
use super::Synchronizer;
use crate::coinconfig::COIN_CONFIG;
use crate::db::DbAdapterBuilder;
use crate::init_coin;
use crate::sapling::{DecryptedSaplingNote, SaplingDecrypter, SaplingHasher, SaplingViewKey};
use crate::sync::CTree;
use crate::sync::tree::WarpProcessor;
use super::Synchronizer;
use crate::sync::CTree;
use std::collections::HashMap;
use zcash_primitives::consensus::Network;
use zcash_primitives::sapling::note_encryption::SaplingDomain;
type SaplingSynchronizer = Synchronizer<Network, SaplingDomain<Network>, SaplingViewKey, DecryptedSaplingNote,
SaplingDecrypter<Network>, SaplingHasher>;
type SaplingSynchronizer = Synchronizer<
Network,
SaplingDomain<Network>,
SaplingViewKey,
DecryptedSaplingNote,
SaplingDecrypter<Network>,
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();
}
}

View File

@ -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<H: Hasher>(&self, height: usize, empty_roots: &[Node], hasher: &H) -> Vec<Node> {
pub fn auth_path<H: Hasher>(
&self,
height: usize,
empty_roots: &[Node],
hasher: &H,
) -> Vec<Node> {
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<H: Hasher> {
hasher: H,
}
impl <H: Hasher> Builder for CTreeBuilder<H> {
impl<H: Hasher> Builder for CTreeBuilder<H> {
type Context = ();
type Output = CTree;
@ -309,11 +314,10 @@ impl <H: Hasher> Builder for CTreeBuilder<H> {
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 <H: Hasher> Builder for CTreeBuilder<H> {
}
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 <H: Hasher> Builder for CTreeBuilder<H> {
}
}
impl <H: Hasher> CTreeBuilder<H> {
impl<H: Hasher> CTreeBuilder<H> {
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 <H: Hasher> CTreeBuilder<H> {
}
#[inline(always)]
fn get<'a>(
commitments: &'a [Node],
index: usize,
offset: &'a Option<Node>,
) -> &'a Node {
fn get<'a>(commitments: &'a [Node], index: usize, offset: &'a Option<Node>) -> &'a Node {
Self::get_opt(commitments, index, offset).unwrap()
}
@ -409,8 +411,7 @@ fn combine_level<H: Hasher>(
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<H: Hasher>(
nn
}
fn batch_level_combine<H: Hasher>(commitments: &mut [Node], offset: Option<Node>, nn: usize, depth: u8, hasher: &H) -> Vec<Node> {
fn batch_level_combine<H: Hasher>(
commitments: &mut [Node],
offset: Option<Node>,
nn: usize,
depth: u8,
hasher: &H,
) -> Vec<Node> {
let hash_extended: Vec<_> = (0..nn)
.into_par_iter()
.map(|i| {
@ -432,7 +439,13 @@ fn batch_level_combine<H: Hasher>(commitments: &mut [Node], offset: Option<Node>
hasher.normalize(&hash_extended)
}
fn single_level_combine<H: Hasher>(commitments: &mut [Node], offset: Option<Node>, nn: usize, depth: u8, hasher: &H) -> Vec<Node> {
fn single_level_combine<H: Hasher>(
commitments: &mut [Node],
offset: Option<Node>,
nn: usize,
depth: u8,
hasher: &H,
) -> Vec<Node> {
(0..nn)
.into_par_iter()
.map(|i| {
@ -449,10 +462,10 @@ struct WitnessBuilder<H: Hasher> {
witness: Witness,
p: usize,
inside: bool,
_phantom: PhantomData<H>
_phantom: PhantomData<H>,
}
impl <H: Hasher> WitnessBuilder<H> {
impl<H: Hasher> WitnessBuilder<H> {
fn new(tree_builder: &CTreeBuilder<H>, 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 <H: Hasher> WitnessBuilder<H> {
}
}
impl <H: Hasher> Builder for WitnessBuilder<H> {
impl<H: Hasher> Builder for WitnessBuilder<H> {
type Context = CTreeBuilder<H>;
type Output = Witness;
@ -514,7 +527,9 @@ impl <H: Hasher> Builder for WitnessBuilder<H> {
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<H> {
hasher: H,
}
impl <H: Hasher> WarpProcessor<H> {
impl<H: Hasher> WarpProcessor<H> {
pub fn new(hasher: H) -> WarpProcessor<H> {
WarpProcessor {
prev_tree: CTree::new(),
@ -599,9 +614,7 @@ impl <H: Hasher> WarpProcessor<H> {
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 <H: Hasher> WarpProcessor<H> {
}
}
fn advance_tree(
&self,
mut commitments: &mut [Node],
) -> (CTree, Vec<Witness>) {
let mut builder = CTreeBuilder::<H>::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<Witness>) {
let mut builder = CTreeBuilder::<H>::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();

View File

@ -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<D: BatchDomain, VK, DN: DecryptedNote<D, VK>> {
pub height: u32,
@ -44,7 +44,13 @@ pub struct OutputPosition {
}
pub trait DecryptedNote<D: BatchDomain, VK>: 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<D: BatchDomain<ExtractedCommitmentBytes = [u8; 32]>> ShieldedOutput<D, COMPACT_NOTE_SIZE>
for CompactShieldedOutput
for CompactShieldedOutput
{
fn ephemeral_key(&self) -> EphemeralKeyBytes {
EphemeralKeyBytes(self.0.epk)
@ -120,12 +141,14 @@ for CompactShieldedOutput
}
}
pub trait TrialDecrypter<N: Parameters, D: BatchDomain<ExtractedCommitmentBytes = [u8; 32]>, VK: ViewKey<D>, DN: DecryptedNote<D, VK>>: Clone {
fn decrypt_notes(
&self,
block: &CompactBlock,
vks: &[VK],
) -> DecryptedBlock<D, VK, DN> {
pub trait TrialDecrypter<
N: Parameters,
D: BatchDomain<ExtractedCommitmentBytes = [u8; 32]>,
VK: ViewKey<D>,
DN: DecryptedNote<D, VK>,
>: Clone
{
fn decrypt_notes(&self, block: &CompactBlock, vks: &[VK]) -> DecryptedBlock<D, VK, DN> {
let height = BlockHeight::from_u32(block.height as u32);
let mut count_outputs = 0u32;
let mut spends: Vec<Nf> = vec![];
@ -161,33 +184,31 @@ pub trait TrialDecrypter<N: Parameters, D: BatchDomain<ExtractedCommitmentBytes
}
let start = Instant::now();
let notes_decrypted =
try_compact_note_decryption(&vvks, &outputs);
let notes_decrypted = try_compact_note_decryption(&vvks, &outputs);
let elapsed = start.elapsed().as_millis() as usize;
for (pos, opt_note) in notes_decrypted.iter().enumerate() {
if let Some(((note, pa), _)) = opt_note {
let vk = &vks[pos / outputs.len()];
if let Some(((note, pa), vk_index)) = opt_note {
let vk = &vks[*vk_index];
let account = vk.account();
let output = &outputs[pos % outputs.len()];
let tx_index = output.1.1.tx_index;
let tx_index = output.1 .1.tx_index;
let tx_key = (account, tx_index);
let tx = txs.entry(tx_key).or_insert_with(||
DecryptedTx {
account,
height: block.height as u32,
timestamp: block.time,
tx_index,
tx_id: block.vtx[tx_index].hash.clone(),
notes: vec![],
_phantom: PhantomData::default(),
let tx = txs.entry(tx_key).or_insert_with(|| DecryptedTx {
account,
height: block.height as u32,
timestamp: block.time,
tx_index,
tx_id: block.vtx[tx_index].hash.clone(),
notes: vec![],
_phantom: PhantomData::default(),
});
tx.notes.push(DN::from_parts(
vk.clone(),
note.clone(),
pa.clone(),
output.1.1.clone(),
output.1.0.cmx,
output.1 .1.clone(),
output.1 .0.cmx,
));
}
}
@ -206,4 +227,3 @@ pub trait TrialDecrypter<N: Parameters, D: BatchDomain<ExtractedCommitmentBytes
fn spends(&self, vtx: &CompactTx) -> Vec<Nf>;
fn outputs(&self, vtx: &CompactTx) -> Vec<CompactOutputBytes>;
}

View File

@ -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<Channel>, height: u32, txid: &Hash) -> anyhow::Result<Transaction> {
async fn fetch_raw_transaction(
network: &Network,
client: &mut CompactTxStreamerClient<Channel>,
height: u32,
txid: &Hash,
) -> anyhow::Result<Transaction> {
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<DecryptionKeys> {
fn get_decryption_keys(
network: &Network,
account: u32,
db: &DbAdapter,
) -> anyhow::Result<DecryptionKeys> {
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<TransactionDetails> {
let transaction = fetch_raw_transaction(network, client, height, txid).await?;
let tx_details = decode_transaction(network, height, id_tx, transaction, &decryption_keys)?;

View File

@ -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<UnifiedAddressType>) -> anyhow::Result<String> {
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<UnifiedAddressType>,
) -> anyhow::Result<String> {
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<Dec
network: network.clone(),
transparent: None,
sapling: None,
orchard: None
orchard: None,
};
let network = network.address_network().unwrap();
let (a_network, ua) = unified::Address::decode(ua)?;

View File

@ -7,7 +7,7 @@ curl -X GET http://localhost:8000/balance
curl -X GET http://localhost:8000/tx_history
curl -X GET http://localhost:8000/new_diversified_address
curl -X POST 'http://localhost:8000/sync?offset=0'
curl -X POST 'http://localhost:8000/rewind?height=1600000'
curl -X POST 'http://localhost:8000/rewind?height=1800000'
# Sync to latest height - 20 to calculate the witnesses for later
curl -X POST 'http://localhost:8000/sync?offset=20'
curl -X POST http://localhost:8000/mark_synced
@ -16,3 +16,8 @@ curl -X GET http://localhost:8000/parse_payment_uri?uri=zcash%3Azs1hn7qwpjz6p5n2
curl -X POST -H 'Content-Type: application/json' -d '{"recipients": [{"address": "zs1hn7qwpjz6p5n24hjhks73y6vn0tpk3c2cfu8wzgtgl4j9ht8ycjgjr47c94scce3uahaje9jkxn", "amount": 100000, "memo": "Hello", "reply_to": false, "subject": "hello", "max_amount_per_note": 0}], "confirmations": 10}' http://localhost:8000/create_offline_tx
// offline signing and pay need an account with a secret key
curl -X POST -H 'Content-Type: application/json' -d '{"coin": 0, "name": "test", "key": "bleak regret excuse hold divide novel rain clutch once used another visual forward small tumble artefact jewel bundle kid wolf universe focus weekend melt"}' http://localhost:8000/new_account
curl -X POST 'http://localhost:8000/rewind?height=1800000'
curl -X POST 'http://localhost:8000/sync?offset=0'
curl 'http://localhost:8000/unified_address?t=0&s=0&o=1'