diff --git a/src/api/account.rs b/src/api/account.rs index 343af69..b2b558d 100644 --- a/src/api/account.rs +++ b/src/api/account.rs @@ -294,3 +294,25 @@ pub async fn import_sync_data(coin: u8, file: &str) -> anyhow::Result<()> { retrieve_tx_info(c.coin_type, &mut client, c.db_path.as_ref().unwrap(), &ids).await?; Ok(()) } + +/// Get the Unified address +/// # Arguments +/// * `coin`: 0 for zcash, 1 for ycash +/// * `id_account`: account id as returned from [new_account] +/// +/// The address depends on the UA settings and may include transparent, sapling & orchard receivers +pub fn get_unified_address(coin: u8, id_account: u32) -> anyhow::Result { + let c = CoinConfig::get(coin); + let db = c.db()?; + let address = crate::get_unified_address(c.chain.network(), &db, id_account)?; + Ok(address) +} + +/// Decode a unified address into its receivers +/// +/// For testing only. The format of the returned value is subject to change +pub fn decode_unified_address(coin: u8, address: &str) -> anyhow::Result { + let c = CoinConfig::get(coin); + let res = crate::decode_unified_address(c.chain.network(), address)?; + Ok(res.to_string()) +} diff --git a/src/db.rs b/src/db.rs index 7aabdd0..e578bc4 100644 --- a/src/db.rs +++ b/src/db.rs @@ -10,14 +10,17 @@ 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_primitives::consensus::{Network, NetworkUpgrade, Parameters}; use zcash_primitives::merkle_tree::IncrementalWitness; use zcash_primitives::sapling::{Diversifier, Node, Note, Rseed, SaplingIvk}; use zcash_primitives::zip32::{DiversifierIndex, ExtendedFullViewingKey}; -use crate::orchard::derive_orchard_keys; +use crate::orchard::{derive_orchard_keys, OrchardKeyBytes, OrchardViewKey}; +use crate::sapling::SaplingViewKey; use crate::sync; +use crate::unified::UnifiedAddressType; mod migration; @@ -36,6 +39,7 @@ pub struct DbAdapter { pub db_path: String, } +#[derive(Debug)] pub struct ReceivedNote { pub account: u32, pub height: u32, @@ -207,33 +211,51 @@ impl DbAdapter { Ok(()) } - pub fn get_fvks(&self) -> anyhow::Result> { + pub fn get_sapling_fvks(&self) -> anyhow::Result> { let mut statement = self .connection - .prepare("SELECT id_account, ivk, sk FROM accounts")?; + .prepare("SELECT id_account, ivk FROM accounts")?; let rows = statement.query_map([], |row| { let account: u32 = row.get(0)?; let ivk: String = row.get(1)?; - let sk: Option = row.get(2)?; let fvk = decode_extended_full_viewing_key( self.network().hrp_sapling_extended_full_viewing_key(), &ivk, ) .unwrap(); let ivk = fvk.fvk.vk.ivk(); - Ok(( + Ok(SaplingViewKey { account, - AccountViewKey { - fvk, - ivk, - viewonly: sk.is_none(), - }, - )) + fvk, + ivk + }) })?; - let mut fvks: HashMap = HashMap::new(); + let mut fvks = vec![]; for r in rows { let row = r?; - fvks.insert(row.0, row.1); + fvks.push(row); + } + Ok(fvks) + } + + pub fn get_orchard_fvks(&self) -> anyhow::Result> { + let mut statement = self.connection.prepare("SELECT account, fvk FROM orchard_addrs")?; + let rows = statement.query_map([], |row| { + let account: u32 = row.get(0)?; + let fvk: Vec = row.get(1)?; + let fvk: [u8; 96] = fvk.try_into().unwrap(); + let fvk = FullViewingKey::from_bytes(&fvk).unwrap(); + let vk = + OrchardViewKey { + account, + fvk, + }; + Ok(vk) + })?; + let mut fvks = vec![]; + for r in rows { + let row = r?; + fvks.push(row); } Ok(fvks) } @@ -271,6 +293,8 @@ 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_witnesses WHERE height > ?1", params![height], @@ -397,10 +421,9 @@ impl DbAdapter { position: usize, db_tx: &Transaction, ) -> anyhow::Result { - log::debug!("+received_note {}", id_tx); + log::info!("+received_note {} {:?}", id_tx, note); db_tx.execute("INSERT INTO received_notes(account, tx, height, position, output_index, diversifier, value, rcm, rho, nf, spent) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11) - ON CONFLICT DO NOTHING", params![note.account, id_tx, note.height, position as u32, note.output_index, note.diversifier, note.value as i64, note.rcm, note.rho, note.nf, note.spent])?; + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", params![note.account, id_tx, note.height, position as u32, note.output_index, note.diversifier, note.value as i64, note.rcm, note.rho, note.nf, note.spent])?; let id_note: u32 = db_tx .query_row( "SELECT id_note FROM received_notes WHERE tx = ?1 AND output_index = ?2", @@ -913,6 +936,18 @@ impl DbAdapter { Ok(()) } + pub fn get_orchard(&self, account: u32) -> anyhow::Result> { + let key = self.connection.query_row("SELECT sk, fvk FROM orchard_addrs WHERE account = ?1", params![account], |row| { + let sk: Vec = row.get(0)?; + let fvk: Vec = row.get(1)?; + Ok(OrchardKeyBytes { + sk: sk.try_into().unwrap(), + fvk: fvk.try_into().unwrap(), + }) + }).optional()?; + Ok(key) + } + 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) ON CONFLICT DO NOTHING", @@ -921,6 +956,20 @@ impl DbAdapter { Ok(()) } + pub fn get_ua_settings(&self, account: u32) -> anyhow::Result { + let tpe = self.connection.query_row("SELECT transparent, sapling, orchard FROM ua_settings WHERE account = ?1", params![account], |row| { + let transparent: bool = row.get(0)?; + let sapling: bool = row.get(1)?; + let orchard: bool = row.get(2)?; + Ok(UnifiedAddressType { + transparent, + sapling, + orchard + }) + })?; + Ok(tpe) + } + pub fn store_historical_prices( &mut self, prices: &[Quote], diff --git a/src/lib.rs b/src/lib.rs index d8e0bcf..d94caf5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -95,6 +95,7 @@ mod prices; mod scan; mod taddr; mod transaction; +mod unified; // mod ua; mod zip32; // mod wallet; @@ -132,6 +133,8 @@ 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}; + #[cfg(feature = "ledger_sapling")] pub use crate::ledger::sapling::build_tx_ledger; diff --git a/src/main/rpc.rs b/src/main/rpc.rs index 94f63ea..a001d13 100644 --- a/src/main/rpc.rs +++ b/src/main/rpc.rs @@ -86,6 +86,8 @@ async fn main() -> anyhow::Result<()> { get_backup, get_balance, get_address, + get_unified_address, + decode_unified_address, get_tx_history, pay, mark_synced, @@ -177,6 +179,20 @@ pub fn get_address() -> Result { Ok(address) } +#[get("/unified_address")] +pub fn get_unified_address() -> Result { + let c = CoinConfig::get_active(); + let address = warp_api_ffi::api::account::get_unified_address(c.coin, c.id_account)?; + Ok(address) +} + +#[post("/decode_unified_address?
")] +pub fn decode_unified_address(address: String) -> Result { + let c = CoinConfig::get_active(); + let result = warp_api_ffi::api::account::decode_unified_address(c.coin, &address)?; + Ok(result) +} + #[get("/backup")] pub fn get_backup(config: &State) -> Result, Error> { if !config.allow_backup { diff --git a/src/orchard.rs b/src/orchard.rs index ad5d257..6e9ff8c 100644 --- a/src/orchard.rs +++ b/src/orchard.rs @@ -4,4 +4,4 @@ mod key; pub use note::{OrchardDecrypter, OrchardViewKey, DecryptedOrchardNote}; pub use hash::OrchardHasher; -pub use key::derive_orchard_keys; +pub use key::{derive_orchard_keys, OrchardKeyBytes}; diff --git a/src/orchard/key.rs b/src/orchard/key.rs index 8485eb5..8e2822f 100644 --- a/src/orchard/key.rs +++ b/src/orchard/key.rs @@ -1,15 +1,25 @@ -use bip39::{Language, Mnemonic}; -use orchard::keys::{FullViewingKey, SpendingKey}; +use bip39::{Language, Mnemonic, Seed}; +use orchard::Address; +use orchard::keys::{FullViewingKey, Scope, SpendingKey}; pub struct OrchardKeyBytes { pub sk: [u8; 32], pub fvk: [u8; 96], } +impl OrchardKeyBytes { + pub fn get_address(&self, index: usize) -> Address { + let fvk = FullViewingKey::from_bytes(&self.fvk).unwrap(); + let address = fvk.address_at(index, Scope::External); + address + } +} + 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( - mnemonic.entropy(), + seed.as_bytes(), coin_type, account_index, ).unwrap(); diff --git a/src/orchard/note.rs b/src/orchard/note.rs index ff95f43..8d6c504 100644 --- a/src/orchard/note.rs +++ b/src/orchard/note.rs @@ -1,13 +1,15 @@ +use orchard::keys::Scope; use orchard::note_encryption::OrchardDomain; use zcash_primitives::consensus::{BlockHeight, Parameters}; use crate::chain::Nf; -use crate::CompactTx; +use crate::{CompactTx, DbAdapterBuilder}; use crate::db::ReceivedNote; use crate::sync::{CompactOutputBytes, DecryptedNote, Node, OutputPosition, TrialDecrypter, ViewKey}; use zcash_note_encryption; use zcash_primitives::sapling::Nullifier; +use zcash_params::coin::CoinType; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct OrchardViewKey { pub account: u32, pub fvk: orchard::keys::FullViewingKey, @@ -50,7 +52,7 @@ impl DecryptedNote for DecryptedOrchardNote { self.cmx } - fn to_received_note(&self, position: u64) -> ReceivedNote { + fn to_received_note(&self, _position: u64) -> ReceivedNote { ReceivedNote { account: self.vk.account, height: self.output_position.height, @@ -94,3 +96,36 @@ impl TrialDecrypter anyhow::Result<()> { + // let mut nullifier = hex::decode("951ab285b0f4df3ff24f24470dbb8bafa3b5caeeb204fc4465f7ea9c3d5a980a").unwrap(); + // let mut epk = hex::decode("182d698c3bb8b168d5f9420f1c2e32d94b4dbc0826181c1783ea47fedd31b710").unwrap(); + // let mut cmx = hex::decode("df45e00eb39e4c281e2804a366d3010b7f663724472d12637e0a749e6ce22719").unwrap(); + // let ciphertext = hex::decode("d9bc6ee09b0afde5dd69bfdf4b667a38da3e1084e84eb6752d54800b9f5110203b60496ab5313dba3f2acb9ef30bcaf68fbfcc59").unwrap(); + + let mut nullifier = hex::decode("ea1b97cc83d326db4130433022f68dd32a0bc707448b19b0980e4e6404412b29").unwrap(); + let mut epk = hex::decode("e2f666e905666f29bb678c694602b2768bea655c0f2b18f9c342ad8b64b18c0c").unwrap(); + let mut 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() + }; + let db = db_builder.build()?; + let keys = db.get_orchard_fvks()?.first().unwrap().clone(); + let fvk = keys.fvk; + + let output = CompactOutputBytes { + nullifier: nullifier.clone().try_into().unwrap(), + epk: epk.try_into().unwrap(), + cmx: cmx.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); + println!("{:?}", r); + Ok(()) +} + diff --git a/src/scan.rs b/src/scan.rs index 7bff4b1..bdba79b 100644 --- a/src/scan.rs +++ b/src/scan.rs @@ -91,29 +91,8 @@ pub async fn sync_async<'a>( let db = DbAdapter::new(coin_type, &db_path)?; let height = db.get_db_height()?; let hash = db.get_db_hash(height)?; - let vks = db.get_fvks()?; - let sapling_vks: Vec<_> = vks.iter().map(|(&account, ak)| { - SaplingViewKey { - account, - fvk: ak.fvk.clone(), - ivk: ak.ivk.clone() - } - }).collect(); - let orchard_vks: Vec<_> = db.get_seeds()?.iter().map(|a| { - let mnemonic = Mnemonic::from_phrase(&a.seed, Language::English).unwrap(); - let sk = SpendingKey::from_zip32_seed( - mnemonic.entropy(), - network.coin_type(), - a.id_account, - ).unwrap(); - let fvk = FullViewingKey::from(&sk); - let vk = - OrchardViewKey { - account: a.id_account, - fvk, - }; - vk - }).collect(); + let sapling_vks = db.get_sapling_fvks()?; + let orchard_vks = db.get_orchard_fvks()?; (height, hash, sapling_vks, orchard_vks) }; let end_height = get_latest_height(&mut client).await?; @@ -138,6 +117,7 @@ pub async fn sync_async<'a>( let last_timestamp = last_block.time; // Sapling + log::info!("Sapling"); { let decrypter = SaplingDecrypter::new(network); let warper = WarpProcessor::new(SaplingHasher::default()); @@ -153,6 +133,7 @@ pub async fn sync_async<'a>( } // Orchard + log::info!("Orchard"); { let decrypter = OrchardDecrypter::new(network); let warper = WarpProcessor::new(OrchardHasher::new()); diff --git a/src/sync.rs b/src/sync.rs index cd0214c..11ff193 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -152,6 +152,7 @@ mod tests { use crate::db::DbAdapterBuilder; use crate::init_coin; use crate::sapling::{DecryptedSaplingNote, SaplingDecrypter, SaplingHasher, SaplingViewKey}; + use crate::scan::Blocks; use crate::sync::CTree; use crate::sync::tree::WarpProcessor; use super::Synchronizer; @@ -179,7 +180,7 @@ mod tests { }; synchronizer.initialize().unwrap(); - synchronizer.process(vec![]).unwrap(); + synchronizer.process(&vec![]).unwrap(); } } diff --git a/src/sync/trial_decrypt.rs b/src/sync/trial_decrypt.rs index ab0d862..e146f99 100644 --- a/src/sync/trial_decrypt.rs +++ b/src/sync/trial_decrypt.rs @@ -3,6 +3,7 @@ use crate::chain::Nf; 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_primitives::consensus::{BlockHeight, Parameters}; @@ -57,6 +58,29 @@ pub struct CompactOutputBytes { pub ciphertext: [u8; 52], } +impl std::fmt::Display for CompactOutputBytes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "nullifier: {}", hex::encode(self.nullifier))?; + writeln!(f, "epk: {}", hex::encode(self.epk))?; + writeln!(f, "cmx: {}", hex::encode(self.cmx))?; + writeln!(f, "ciphertext: {}", hex::encode(self.ciphertext)) + } +} + +impl ShieldedOutput for CompactOutputBytes { + fn ephemeral_key(&self) -> EphemeralKeyBytes { + EphemeralKeyBytes(self.epk) + } + + fn cmstar_bytes(&self) -> [u8; 32] { + self.cmx + } + + fn enc_ciphertext(&self) -> &[u8; COMPACT_NOTE_SIZE] { + &self.ciphertext + } +} + impl From<&CompactSaplingOutput> for CompactOutputBytes { fn from(co: &CompactSaplingOutput) -> Self { CompactOutputBytes { diff --git a/src/unified.rs b/src/unified.rs new file mode 100644 index 0000000..cf713d2 --- /dev/null +++ b/src/unified.rs @@ -0,0 +1,116 @@ +use anyhow::anyhow; +use orchard::Address; +use orchard::keys::{FullViewingKey, Scope}; +use rusqlite::Connection; +use zcash_address::{ToAddress, unified, ZcashAddress}; +use zcash_address::unified::{Container, Encoding, Receiver}; +use zcash_client_backend::address::RecipientAddress; +use zcash_client_backend::encoding::{AddressCodec, decode_payment_address, encode_payment_address}; +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, + pub sapling: bool, + pub orchard: bool, +} + +pub struct DecodedUA { + pub network: Network, + pub transparent: Option, + pub sapling: Option, + pub orchard: Option
, +} + +impl std::fmt::Display for DecodedUA { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + 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.orchard.as_ref().map(|a| { + let ua = unified::Address(vec![Receiver::Orchard(a.to_raw_address_bytes())]); + ua.encode(&network2network(&self.network)) + }) + ) + } +} + +pub fn get_unified_address(network: &Network, db: &DbAdapter, account: u32) -> anyhow::Result { + let tpe = db.get_ua_settings(account)?; + let mut rcvs = vec![]; + if tpe.transparent { + let address = db.get_taddr(account)?; + if let Some(address) = address { + let address = TransparentAddress::decode(network, &address)?; + if let TransparentAddress::PublicKey(pkh) = address { + let rcv = Receiver::P2pkh(pkh); + rcvs.push(rcv); + } + } + } + if tpe.sapling { + 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); + } + if tpe.orchard { + let AccountData { address: zaddr, .. } = db.get_account_info(account)?; + let okey = db.get_orchard(account)?; + if let Some(okey) = okey { + let fvk = FullViewingKey::from_bytes(&okey.fvk).unwrap(); + let address = fvk.address_at(0usize, Scope::External); + let rcv = Receiver::Orchard(address.to_raw_address_bytes()); + rcvs.push(rcv); + } + } + + let addresses = unified::Address(rcvs); + let unified_address = ZcashAddress::from_unified(network2network(network), addresses); + Ok(unified_address.encode()) +} + +pub fn decode_unified_address(network: &Network, ua: &str) -> anyhow::Result { + let mut decoded_ua = DecodedUA { + network: network.clone(), + transparent: None, + sapling: None, + orchard: None + }; + let network = network2network(network); + let (a_network, ua) = unified::Address::decode(ua)?; + if network != a_network { + anyhow::bail!("Invalid network") + } + + for recv in ua.items_as_parsed() { + match recv { + Receiver::Orchard(addr) => { + decoded_ua.orchard = Address::from_raw_address_bytes(addr).into(); + } + Receiver::Sapling(addr) => { + decoded_ua.sapling = PaymentAddress::from_bytes(addr); + } + Receiver::P2pkh(addr) => { + decoded_ua.transparent = Some(TransparentAddress::PublicKey(*addr)); + } + Receiver::P2sh(_) => {} + Receiver::Unknown { .. } => {} + } + } + Ok(decoded_ua) +} + +fn network2network(n: &Network) -> zcash_address::Network { + match n { + Network::MainNetwork => zcash_address::Network::Main, + Network::TestNetwork => zcash_address::Network::Test, + Network::YCashMainNetwork => zcash_address::Network::Main, + Network::YCashTestNetwork => zcash_address::Network::Test, + Network::PirateChainMainNetwork => zcash_address::Network::Main, + } +} + +// u1pncsxa8jt7aq37r8uvhjrgt7sv8a665hdw44rqa28cd9t6qqmktzwktw772nlle6skkkxwmtzxaan3slntqev03g70tzpky3c58hfgvfjkcky255cwqgfuzdjcktfl7pjalt5sl33se75pmga09etn9dplr98eq2g8cgmvgvx6jx2a2xhy39x96c6rumvlyt35whml87r064qdzw30e \ No newline at end of file