Unified address encode/decode & accounts

This commit is contained in:
Hanh 2022-10-30 18:03:38 +08:00
parent f8a692e3e5
commit 544fb25f9a
11 changed files with 304 additions and 47 deletions

View File

@ -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<String> {
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<String> {
let c = CoinConfig::get(coin);
let res = crate::decode_unified_address(c.chain.network(), address)?;
Ok(res.to_string())
}

View File

@ -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<HashMap<u32, AccountViewKey>> {
pub fn get_sapling_fvks(&self) -> anyhow::Result<Vec<SaplingViewKey>> {
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<String> = 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<u32, AccountViewKey> = 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<Vec<OrchardViewKey>> {
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,
};
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<u32> {
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<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()?;
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<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
})
})?;
Ok(tpe)
}
pub fn store_historical_prices(
&mut self,
prices: &[Quote],

View File

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

View File

@ -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<String, Error> {
Ok(address)
}
#[get("/unified_address")]
pub fn get_unified_address() -> Result<String, Error> {
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?<address>")]
pub fn decode_unified_address(address: String) -> Result<String, Error> {
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<Config>) -> Result<Json<Backup>, Error> {
if !config.allow_backup {

View File

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

View File

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

View File

@ -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<OrchardDomain, OrchardViewKey> 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 <N: Parameters> TrialDecrypter<N, OrchardDomain, OrchardViewKey, DecryptedO
vtx.actions.iter().map(|co| co.into()).collect()
}
}
#[test]
pub fn test_decrypt() -> 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(())
}

View File

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

View File

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

View File

@ -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<OrchardDomain, COMPACT_NOTE_SIZE> 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 {

116
src/unified.rs Normal file
View File

@ -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<TransparentAddress>,
pub sapling: Option<PaymentAddress>,
pub orchard: Option<Address>,
}
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<String> {
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<DecodedUA> {
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