From a1e2e9d26d744372e708d87d5d4c316370d9fe18 Mon Sep 17 00:00:00 2001 From: Daksh <41485688+Daksh14@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:10:12 -0400 Subject: [PATCH 1/2] rusk-wallet: Upgrade to new `wallet-core` - Change crate name to - Acclimate to the new naming scheme of keys and types - Zerorize secret keys - Acclimate to new phoenix_balance method --- rusk-wallet/Cargo.toml | 26 +- rusk-wallet/src/bin/command.rs | 40 +- rusk-wallet/src/bin/command/history.rs | 19 +- rusk-wallet/src/bin/interactive.rs | 12 +- rusk-wallet/src/bin/io/gql.rs | 17 +- rusk-wallet/src/bin/io/prompt.rs | 12 +- rusk-wallet/src/bin/main.rs | 14 +- rusk-wallet/src/bin/settings.rs | 2 +- rusk-wallet/src/cache.rs | 63 ++-- rusk-wallet/src/clients.rs | 480 +++++++++++------------- rusk-wallet/src/clients/sync.rs | 96 ++--- rusk-wallet/src/currency.rs | 28 +- rusk-wallet/src/dat.rs | 26 +- rusk-wallet/src/error.rs | 44 +-- rusk-wallet/src/lib.rs | 22 +- rusk-wallet/src/rusk.rs | 1 + rusk-wallet/src/store.rs | 35 +- rusk-wallet/src/wallet.rs | 486 ++++++++++++++----------- rusk-wallet/src/wallet/address.rs | 38 +- 19 files changed, 739 insertions(+), 722 deletions(-) diff --git a/rusk-wallet/Cargo.toml b/rusk-wallet/Cargo.toml index 0bb2c4133..41a9226df 100644 --- a/rusk-wallet/Cargo.toml +++ b/rusk-wallet/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "dusk-wallet" -version = "0.22.1" +name = "rusk-wallet" +version = "0.1.0" edition = "2021" autobins = false description = "A library providing functionalities to create wallets compatible with Dusk Network" @@ -32,7 +32,6 @@ requestty = "0.5.0" futures = "0.3" base64 = "0.13" crypto = "0.3" -whoami = "1.2" blake3 = "1.3" sha2 = "0.10.7" toml = "0.5" @@ -44,24 +43,17 @@ aes = "0.7" rocksdb = "0.22" flume = "0.10.14" reqwest = { version = "0.11", features = ["stream"] } - -dusk-wallet-core = "0.24.0-plonk.0.16-rc.2" dusk-bytes = "0.1" -dusk-pki = "0.13" -rusk-abi = { version = "0.12.0-rc", default-features = false } -phoenix-core = { version = "0.21", features = ["alloc"] } -dusk-schnorr = { version = "0.14", default-features = false } -dusk-poseidon = "0.31" -dusk-plonk = "0.16" -dusk-bls12_381-sign = { version = "0.5", default-features = false } -ff = { version = "0.13", default-features = false } -poseidon-merkle = "0.3" + +zeroize = { version = "1", default-features = false, features = ["derive"] } +wallet-core = { path = "../wallet-core" } +execution-core = { path = "../execution-core" } tracing = "0.1" tracing-subscriber = { version = "0.3.0", features = [ - "fmt", - "env-filter", - "json", + "fmt", + "env-filter", + "json", ] } rkyv = { version = "=0.7.39", default-features = false } diff --git a/rusk-wallet/src/bin/command.rs b/rusk-wallet/src/bin/command.rs index a39066936..e75126097 100644 --- a/rusk-wallet/src/bin/command.rs +++ b/rusk-wallet/src/bin/command.rs @@ -7,17 +7,19 @@ mod history; use clap::Subcommand; -use dusk_plonk::prelude::BlsScalar; -use rusk_abi::hash::Hasher; use std::{fmt, path::PathBuf}; use crate::io::prompt; use crate::settings::Settings; use crate::{WalletFile, WalletPath}; -use dusk_wallet::gas::{Gas, DEFAULT_LIMIT, DEFAULT_PRICE}; -use dusk_wallet::{Address, Dusk, Lux, Wallet, EPOCH, MAX_ADDRESSES}; -use dusk_wallet_core::{BalanceInfo, StakeInfo}; +use execution_core::{stake::StakeData, BlsScalar}; +use rusk_wallet::{ + currency::{Dusk, Lux}, + gas::{Gas, DEFAULT_LIMIT, DEFAULT_PRICE}, + Address, Error, Wallet, EPOCH, MAX_ADDRESSES, +}; +use wallet_core::BalanceInfo; pub use history::TransactionHistory; @@ -230,7 +232,7 @@ impl Command { let gas = Gas::new(gas_limit).with_price(gas_price); let tx = wallet.transfer(sender, &rcvr, amt, gas).await?; - Ok(RunResult::Tx(Hasher::digest(tx.to_hash_input_bytes()))) + Ok(RunResult::Tx(tx.hash())) } Command::Stake { addr, @@ -246,14 +248,16 @@ impl Command { let gas = Gas::new(gas_limit).with_price(gas_price); let tx = wallet.stake(addr, amt, gas).await?; - Ok(RunResult::Tx(Hasher::digest(tx.to_hash_input_bytes()))) + Ok(RunResult::Tx(tx.hash())) } Command::StakeInfo { addr, reward } => { let addr = match addr { Some(addr) => wallet.claim_as_address(addr)?, None => wallet.default_address(), }; - let si = wallet.stake_info(addr).await?; + let si = + wallet.stake_info(addr).await?.ok_or(Error::NotStaked)?; + Ok(RunResult::StakeInfo(si, reward)) } Command::Unstake { @@ -270,7 +274,7 @@ impl Command { let gas = Gas::new(gas_limit).with_price(gas_price); let tx = wallet.unstake(addr, gas).await?; - Ok(RunResult::Tx(Hasher::digest(tx.to_hash_input_bytes()))) + Ok(RunResult::Tx(tx.hash())) } Command::Withdraw { addr, @@ -286,7 +290,7 @@ impl Command { let gas = Gas::new(gas_limit).with_price(gas_price); let tx = wallet.withdraw_reward(addr, gas).await?; - Ok(RunResult::Tx(Hasher::digest(tx.to_hash_input_bytes()))) + Ok(RunResult::Tx(tx.hash())) } Command::Export { addr, dir, name } => { let addr = match addr { @@ -329,7 +333,7 @@ impl Command { pub enum RunResult { Tx(BlsScalar), Balance(BalanceInfo, bool), - StakeInfo(StakeInfo, bool), + StakeInfo(StakeData, bool), Address(Box
), Addresses(Vec
), ExportedKeys(PathBuf, PathBuf), @@ -366,13 +370,13 @@ impl fmt::Display for RunResult { let hash = hex::encode(hash.to_bytes()); write!(f, "> Transaction sent: {hash}",) } - StakeInfo(si, _) => { - let stake_str = match si.amount { - Some((value, eligibility)) => format!( + StakeInfo(data, _) => { + let stake_str = match data.amount { + Some(amt) => format!( "Current stake amount is: {} DUSK\n> Stake eligibility from block #{} (Epoch {})", - Dusk::from(value), - eligibility, - eligibility / EPOCH + Dusk::from(amt.value), + amt.eligibility, + amt.eligibility / EPOCH ), None => "No active stake found for this key".to_string(), }; @@ -380,7 +384,7 @@ impl fmt::Display for RunResult { f, "> {}\n> Accumulated reward is: {} DUSK", stake_str, - Dusk::from(si.reward) + Dusk::from(data.reward) ) } ExportedKeys(pk, kp) => { diff --git a/rusk-wallet/src/bin/command/history.rs b/rusk-wallet/src/bin/command/history.rs index 2e8e573da..3ccc437c0 100644 --- a/rusk-wallet/src/bin/command/history.rs +++ b/rusk-wallet/src/bin/command/history.rs @@ -8,9 +8,9 @@ use std::collections::hash_map::Entry; use std::collections::HashMap; use std::fmt::{self, Display}; -use dusk_wallet::DecodedNote; -use dusk_wallet_core::Transaction; -use rusk_abi::dusk; +use rusk_wallet::DecodedNote; + +use execution_core::{dusk, from_dusk, transfer::Transaction}; use crate::io::{self, GraphQL}; use crate::settings::Settings; @@ -35,17 +35,17 @@ impl TransactionHistory { impl Display for TransactionHistory { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let dusk = self.amount / dusk::dusk(1.0) as f64; + let dusk = self.amount / dusk(1.0) as f64; let contract = match self.tx.call() { None => "transfer", - Some((_, method, _)) => method, + Some(call) => &call.fn_name, }; let fee = match self.direction { TransactionDirection::In => "".into(), TransactionDirection::Out => { let fee = self.fee; - let fee = dusk::from_dusk(fee); + let fee = from_dusk(fee); format!("{: >12.9}", fee) } }; @@ -95,8 +95,8 @@ pub(crate) async fn transaction_from_notes( let note_hash = decoded_note.note.hash(); // Looking for the transaction which created the note let note_creator = txs.iter().find(|(t, _, _)| { - t.outputs().iter().any(|&n| n.hash().eq(¬e_hash)) - || t.nullifiers + t.outputs().iter().any(|n| n.hash().eq(¬e_hash)) + || t.nullifiers() .iter() .any(|tx_null| nullifiers.iter().any(|(n, _)| n == tx_null)) }); @@ -114,13 +114,14 @@ pub(crate) async fn transaction_from_notes( true => TransactionDirection::Out, false => TransactionDirection::In, }; + match ret.iter_mut().find(|th| &th.id == tx_id) { Some(tx) => tx.amount += note_amount, None => ret.push(TransactionHistory { direction, height: decoded_note.block_height, amount: note_amount - inputs_amount, - fee: gas_spent * t.fee().gas_price, + fee: gas_spent * t.gas_price(), tx: t.clone(), id: tx_id.clone(), }), diff --git a/rusk-wallet/src/bin/interactive.rs b/rusk-wallet/src/bin/interactive.rs index f6494ae3e..07f35cea3 100644 --- a/rusk-wallet/src/bin/interactive.rs +++ b/rusk-wallet/src/bin/interactive.rs @@ -5,10 +5,12 @@ // Copyright (c) DUSK NETWORK. All rights reserved. use bip39::{Language, Mnemonic, MnemonicType}; -use dusk_wallet::dat::{DatFileVersion, LATEST_VERSION}; -use dusk_wallet::gas; -use dusk_wallet::{Address, Dusk, Error, Wallet, WalletPath, MAX_ADDRESSES}; use requestty::Question; +use rusk_wallet::{ + currency::Dusk, + dat::{DatFileVersion, LATEST_VERSION}, + gas, Address, Error, Wallet, WalletPath, MAX_ADDRESSES, +}; use crate::command::DEFAULT_STAKE_GAS_LIMIT; use crate::io; @@ -68,7 +70,7 @@ pub(crate) async fn run_loop( // get balance for this address prompt::hide_cursor()?; let balance = wallet.get_balance(&addr).await?; - let spendable: Dusk = balance.spendable.into(); + let spendable = balance.spendable.into(); let total: Dusk = balance.value.into(); prompt::hide_cursor()?; @@ -159,7 +161,7 @@ fn menu_addr(wallet: &Wallet) -> anyhow::Result { )); } - if let Some(rx) = &wallet.sync_rx { + if let Some(rx) = &wallet.state()?.sync_rx { if let Ok(status) = rx.try_recv() { action_menu = action_menu .separator() diff --git a/rusk-wallet/src/bin/io/gql.rs b/rusk-wallet/src/bin/io/gql.rs index d1fafc664..4b0c9d6c0 100644 --- a/rusk-wallet/src/bin/io/gql.rs +++ b/rusk-wallet/src/bin/io/gql.rs @@ -4,10 +4,10 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. -use dusk_wallet_core::Transaction; +use execution_core::transfer::Transaction; use tokio::time::{sleep, Duration}; -use dusk_wallet::{Error, RuskHttpClient, RuskRequest}; +use rusk_wallet::{Error, RuskHttpClient, RuskRequest}; use serde::Deserialize; /// GraphQL is a helper struct that aggregates all queries done @@ -129,7 +129,7 @@ impl GraphQL { pub enum GraphQLError { /// Generic errors #[error("Error fetching data from the node: {0}")] - Generic(dusk_wallet::Error), + Generic(Error), /// Failed to fetch transaction status #[error("Failed to obtain transaction status")] TxStatus, @@ -137,8 +137,8 @@ pub enum GraphQLError { BlockInfo, } -impl From for GraphQLError { - fn from(e: dusk_wallet::Error) -> Self { +impl From for GraphQLError { + fn from(e: Error) -> Self { Self::Generic(e) } } @@ -150,10 +150,7 @@ impl From for GraphQLError { } impl GraphQL { - pub async fn query( - &self, - query: &str, - ) -> Result, dusk_wallet::Error> { + pub async fn query(&self, query: &str) -> Result, Error> { let request = RuskRequest::new("gql", query.as_bytes().to_vec()); self.client.call(2, "Chain", &request).await } @@ -177,7 +174,7 @@ async fn test() -> Result<(), Box> { .await?; let block_txs = gql.txs_for_block(90).await?; block_txs.into_iter().for_each(|(t, chain_txid, _)| { - let hash = rusk_abi::hash::Hasher::digest(t.to_hash_input_bytes()); + let hash = t.hash(); let tx_id = hex::encode(hash.to_bytes()); assert_eq!(chain_txid, tx_id); println!("txid: {tx_id}"); diff --git a/rusk-wallet/src/bin/io/prompt.rs b/rusk-wallet/src/bin/io/prompt.rs index e4200d916..b358af147 100644 --- a/rusk-wallet/src/bin/io/prompt.rs +++ b/rusk-wallet/src/bin/io/prompt.rs @@ -15,13 +15,14 @@ use crossterm::{ use anyhow::Result; use bip39::{ErrorKind, Language, Mnemonic}; -use dusk_wallet::{dat::DatFileVersion, Error}; use requestty::Question; -use dusk_wallet::{Address, Dusk, Lux}; - -use dusk_wallet::gas; -use dusk_wallet::{MAX_CONVERTIBLE, MIN_CONVERTIBLE}; +use rusk_wallet::gas; +use rusk_wallet::{ + currency::{Dusk, Lux}, + dat::DatFileVersion, + Address, Error, MAX_CONVERTIBLE, MIN_CONVERTIBLE, +}; use sha2::{Digest, Sha256}; /// Request the user to authenticate with a password @@ -244,6 +245,7 @@ pub(crate) fn request_token_amt( .build(); let a = requestty::prompt_one(question)?; + Ok(a.as_float().expect("answer to be a float").into()) } diff --git a/rusk-wallet/src/bin/main.rs b/rusk-wallet/src/bin/main.rs index 7ddb9cada..050c4d874 100644 --- a/rusk-wallet/src/bin/main.rs +++ b/rusk-wallet/src/bin/main.rs @@ -12,7 +12,6 @@ mod menu; mod settings; pub(crate) use command::{Command, RunResult}; -use dusk_wallet::dat::LATEST_VERSION; pub(crate) use menu::Menu; use clap::Parser; @@ -25,8 +24,11 @@ use bip39::{Language, Mnemonic, MnemonicType}; use crate::command::TransactionHistory; use crate::settings::{LogFormat, Settings}; -use dusk_wallet::{dat, Error}; -use dusk_wallet::{Dusk, SecureWalletFile, Wallet, WalletPath}; +use rusk_wallet::{currency::Dusk, SecureWalletFile, Wallet, WalletPath}; +use rusk_wallet::{ + dat::{self, LATEST_VERSION}, + Error, +}; use config::Config; use io::{prompt, status}; @@ -83,7 +85,7 @@ where // check for connection errors match con { - Err(Error::RocksDB(e)) => panic!{"Invalid cache {e}"}, + Err(Error::RocksDB(e)) => panic!{"Please reset the cache! {e}"}, Err(e) => warn!("[OFFLINE MODE]: Unable to connect to Rusk, limited functionality available: {e}"), _ => {} } @@ -121,7 +123,7 @@ async fn exec() -> anyhow::Result<()> { // Finally complete the settings by setting the network let settings = settings_builder .network(cfg.network) - .map_err(|_| dusk_wallet::Error::NetworkNotFound)?; + .map_err(|_| rusk_wallet::Error::NetworkNotFound)?; // generate a subscriber with the desired log level // @@ -314,7 +316,7 @@ async fn exec() -> anyhow::Result<()> { println!("{}", Dusk::from(info.reward)); } else { let staked_amount = match info.amount { - Some((staked, ..)) => staked, + Some(info) => info.value, None => 0, }; println!("{}", Dusk::from(staked_amount)); diff --git a/rusk-wallet/src/bin/settings.rs b/rusk-wallet/src/bin/settings.rs index 99517c2e1..3284bb96d 100644 --- a/rusk-wallet/src/bin/settings.rs +++ b/rusk-wallet/src/bin/settings.rs @@ -7,7 +7,7 @@ use crate::config::Network; use crate::io::WalletArgs; -use dusk_wallet::Error; +use rusk_wallet::Error; use std::fmt; use std::path::PathBuf; diff --git a/rusk-wallet/src/cache.rs b/rusk-wallet/src/cache.rs index acc7b3552..2dd4936d5 100644 --- a/rusk-wallet/src/cache.rs +++ b/rusk-wallet/src/cache.rs @@ -4,18 +4,15 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. -use std::cmp::Ordering; -use std::collections::BTreeSet; use std::path::Path; +use std::{cmp::Ordering, collections::BTreeSet}; use dusk_bytes::{DeserializableSlice, Serializable}; -use dusk_pki::PublicSpendKey; -use dusk_plonk::prelude::BlsScalar; -use dusk_wallet_core::Store; -use phoenix_core::Note; use rocksdb::{DBWithThreadMode, MultiThreaded, Options}; -use crate::{error::Error, store::LocalStore, MAX_ADDRESSES}; +use super::*; + +use crate::error::Error; type DB = DBWithThreadMode; @@ -30,21 +27,9 @@ impl Cache { /// Returns a new cache instance. pub(crate) fn new>( path: T, - store: &LocalStore, + cfs: Vec, status: fn(&str), ) -> Result { - let cfs: Vec<_> = (0..MAX_ADDRESSES) - .flat_map(|i| { - let ssk = - store.retrieve_ssk(i as u64).expect("ssk to be available"); - let psk = ssk.public_spend_key(); - - let live = format!("{:?}", psk); - let spent = format!("spent_{:?}", psk); - [live, spent] - }) - .collect(); - status("Opening notes database"); let mut opts = Options::default(); @@ -59,16 +44,16 @@ impl Cache { Ok(Self { db }) } - // We store a column family named by hex representation of the psk. + // We store a column family named by hex representation of the pk. // We store the nullifier of the note as key and the value is the bytes // representation of the tuple (NoteHeight, Note) pub(crate) fn insert( &self, - psk: &PublicSpendKey, + pk: &PhoenixPublicKey, height: u64, note_data: (Note, BlsScalar), ) -> Result<(), Error> { - let cf_name = format!("{:?}", psk); + let cf_name = format!("{:?}", pk); let cf = self .db @@ -85,16 +70,16 @@ impl Cache { Ok(()) } - // We store a column family named by hex representation of the psk. + // We store a column family named by hex representation of the pk. // We store the nullifier of the note as key and the value is the bytes // representation of the tuple (NoteHeight, Note) pub(crate) fn insert_spent( &self, - psk: &PublicSpendKey, + pk: &PhoenixPublicKey, height: u64, note_data: (Note, BlsScalar), ) -> Result<(), Error> { - let cf_name = format!("spent_{:?}", psk); + let cf_name = format!("spent_{:?}", pk); let cf = self .db @@ -113,14 +98,14 @@ impl Cache { pub(crate) fn spend_notes( &self, - psk: &PublicSpendKey, + pk: &PhoenixPublicKey, nullifiers: &[BlsScalar], ) -> Result<(), Error> { if nullifiers.is_empty() { return Ok(()); } - let cf_name = format!("{:?}", psk); - let spent_cf_name = format!("spent_{:?}", psk); + let cf_name = format!("{:?}", pk); + let spent_cf_name = format!("spent_{:?}", pk); let cf = self .db @@ -160,12 +145,12 @@ impl Cache { })) } - /// Returns an iterator over all unspent notes nullifier for the given PSK. + /// Returns an iterator over all unspent notes nullifier for the given pk. pub(crate) fn unspent_notes_id( &self, - psk: &PublicSpendKey, + pk: &PhoenixPublicKey, ) -> Result, Error> { - let cf_name = format!("{:?}", psk); + let cf_name = format!("{:?}", pk); let mut notes = vec![]; if let Some(cf) = self.db.cf_handle(&cf_name) { @@ -183,13 +168,13 @@ impl Cache { Ok(notes) } - /// Returns an iterator over all unspent notes inserted for the given PSK, + /// Returns an iterator over all unspent notes inserted for the given pk, /// in order of note position. pub(crate) fn notes( &self, - psk: &PublicSpendKey, + pk: &PhoenixPublicKey, ) -> Result, Error> { - let cf_name = format!("{:?}", psk); + let cf_name = format!("{:?}", pk); let mut notes = BTreeSet::::new(); if let Some(cf) = self.db.cf_handle(&cf_name) { @@ -208,13 +193,13 @@ impl Cache { Ok(notes) } - /// Returns an iterator over all notes inserted for the given PSK, in order + /// Returns an iterator over all notes inserted for the given pk, in order /// of block height. pub(crate) fn spent_notes( &self, - psk: &PublicSpendKey, + pk: &PhoenixPublicKey, ) -> Result, Error> { - let cf_name = format!("spent_{:?}", psk); + let cf_name = format!("spent_{:?}", pk); let mut notes = vec![]; if let Some(cf) = self.db.cf_handle(&cf_name) { @@ -249,7 +234,7 @@ impl PartialOrd for NoteData { } impl Ord for NoteData { - fn cmp(&self, other: &Self) -> Ordering { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.note.pos().cmp(other.note.pos()) } } diff --git a/rusk-wallet/src/clients.rs b/rusk-wallet/src/clients.rs index 35a5de5af..4c1a7ff9f 100644 --- a/rusk-wallet/src/clients.rs +++ b/rusk-wallet/src/clients.rs @@ -6,43 +6,34 @@ mod sync; -use dusk_bls12_381_sign::PublicKey; -use dusk_bytes::{DeserializableSlice, Serializable, Write}; -use dusk_pki::ViewKey; -use dusk_plonk::prelude::*; -use dusk_plonk::proof_system::Proof; -use dusk_schnorr::Signature; -use dusk_wallet_core::{ - EnrichedNote, ProverClient, StakeInfo, StateClient, Transaction, - UnprovenTransaction, POSEIDON_TREE_DEPTH, +use dusk_bytes::Serializable; +use execution_core::{ + transfer::{phoenix::Prove, Transaction}, + Error as ExecutionCoreError, }; -use flume::Sender; -use phoenix_core::transaction::StakeData; -use phoenix_core::{Crossover, Fee, Note}; -use poseidon_merkle::Opening as PoseidonOpening; +use flume::Receiver; use tokio::time::{sleep, Duration}; +use wallet_core::{ + input::try_input_notes, + keys::{derive_phoenix_pk, derive_phoenix_sk, derive_phoenix_vk}, +}; +use zeroize::Zeroize; -use std::path::Path; -use std::sync::{Arc, Mutex}; +use std::{ + path::Path, + sync::{Arc, Mutex}, +}; use self::sync::sync_db; -use super::block::Block; -use super::cache::Cache; - -use crate::rusk::{RuskHttpClient, RuskRequest}; -use crate::store::LocalStore; -use crate::Error; - -const STCT_INPUT_SIZE: usize = Fee::SIZE - + Crossover::SIZE - + u64::SIZE - + JubJubScalar::SIZE - + BlsScalar::SIZE - + Signature::SIZE; +use super::{block::Block, cache::Cache, *}; -const WFCT_INPUT_SIZE: usize = - JubJubAffine::SIZE + u64::SIZE + JubJubScalar::SIZE; +use crate::{ + cache::NoteData, + rusk::{RuskHttpClient, RuskRequest}, + store::LocalStore, + Error, MAX_ADDRESSES, +}; const TRANSFER_CONTRACT: &str = "0100000000000000000000000000000000000000000000000000000000000000"; @@ -53,191 +44,84 @@ const STAKE_CONTRACT: &str = // Sync every 3 seconds for now const SYNC_INTERVAL_SECONDS: u64 = 3; -/// Implementation of the ProverClient trait from wallet-core -pub struct Prover { - state: RuskHttpClient, - prover: RuskHttpClient, - status: fn(status: &str), -} - -impl Prover { - pub fn new(state: RuskHttpClient, prover: RuskHttpClient) -> Self { - Prover { - state, - prover, - status: |_| {}, - } - } +/// A prover struct that has the `Prove` trait from executio-core implemented. +/// It currently uses a hardcoded prover which delegates the proving to the +/// `prove_execute` +pub struct Prover; - /// Sets the callback method to send status updates - pub fn set_status_callback(&mut self, status: fn(&str)) { - self.status = status; - } - - pub async fn check_connection(&self) -> Result<(), reqwest::Error> { - self.state.check_connection().await?; - self.prover.check_connection().await +impl Prove for Prover { + fn prove( + tx_circuit_vec_bytes: &[u8], + ) -> Result, ExecutionCoreError> { + Ok(tx_circuit_vec_bytes.to_vec()) } } -impl ProverClient for Prover { - /// Error returned by the prover client. - type Error = Error; - - /// Requests that a node prove the given transaction and later propagates it - fn compute_proof_and_propagate( - &self, - utx: &UnprovenTransaction, - ) -> Result { - self.status("Proving tx, please wait..."); - let utx_bytes = utx.to_var_bytes(); - let prove_req = RuskRequest::new("prove_execute", utx_bytes); - let proof_bytes = self.prover.call(2, "rusk", &prove_req).wait()?; - self.status("Proof success!"); - let proof = Proof::from_slice(&proof_bytes).map_err(Error::Bytes)?; - let tx = utx.clone().prove(proof); - let tx_bytes = tx.to_var_bytes(); - - self.status("Attempt to preverify tx..."); - let preverify_req = RuskRequest::new("preverify", tx_bytes.clone()); - let _ = self.state.call(2, "rusk", &preverify_req).wait()?; - self.status("Preverify success!"); - - self.status("Propagating tx..."); - let propagate_req = RuskRequest::new("propagate_tx", tx_bytes); - let _ = self.state.call(2, "Chain", &propagate_req).wait()?; - self.status("Transaction propagated!"); - - Ok(tx) - } - - /// Requests an STCT proof. - fn request_stct_proof( - &self, - fee: &Fee, - crossover: &Crossover, - value: u64, - blinder: JubJubScalar, - address: BlsScalar, - signature: Signature, - ) -> Result { - let mut buf = [0; STCT_INPUT_SIZE]; - let mut writer = &mut buf[..]; - writer.write(&fee.to_bytes())?; - writer.write(&crossover.to_bytes())?; - writer.write(&value.to_bytes())?; - writer.write(&blinder.to_bytes())?; - writer.write(&address.to_bytes())?; - writer.write(&signature.to_bytes())?; - - self.status("Requesting stct proof..."); - - let prove_req = RuskRequest::new("prove_stct", buf.to_vec()); - let res = self.prover.call(2, "rusk", &prove_req).wait()?; - - self.status("Stct proof success!"); - - let mut proof_bytes = [0u8; Proof::SIZE]; - proof_bytes.copy_from_slice(&res); - - let proof = Proof::from_bytes(&proof_bytes)?; - Ok(proof) - } - - /// Request a WFCT proof. - fn request_wfct_proof( - &self, - commitment: JubJubAffine, - value: u64, - blinder: JubJubScalar, - ) -> Result { - let mut buf = [0; WFCT_INPUT_SIZE]; - let mut writer = &mut buf[..]; - writer.write(&commitment.to_bytes())?; - writer.write(&value.to_bytes())?; - writer.write(&blinder.to_bytes())?; - - self.status("Requesting wfct proof..."); - let prove_req = RuskRequest::new("prove_wfct", buf.to_vec()); - let res = self.prover.call(2, "rusk", &prove_req).wait()?; - self.status("Wfct proof success!"); - - let mut proof_bytes = [0u8; Proof::SIZE]; - proof_bytes.copy_from_slice(&res); - - let proof = Proof::from_bytes(&proof_bytes)?; - Ok(proof) - } -} - -impl Prover { - fn status(&self, text: &str) { - (self.status)(text) - } -} - -/// Implementation of the StateClient trait from wallet-core -/// inner is an option because we don't want to open the db twice and lock it -/// We construct StateStore twice -pub struct StateStore { - inner: Mutex, +/// The state struct is responsible for managing the state of the wallet +pub struct State { + cache: Mutex>, status: fn(&str), - pub(crate) store: LocalStore, -} - -struct InnerState { client: RuskHttpClient, - cache: Arc, + prover: RuskHttpClient, + store: LocalStore, + pub sync_rx: Option>, } -impl StateStore { +impl State { /// Creates a new state instance. Should only be called once. pub(crate) fn new( - client: RuskHttpClient, data_dir: &Path, - store: LocalStore, status: fn(&str), + client: RuskHttpClient, + prover: RuskHttpClient, + store: LocalStore, ) -> Result { - let cache = Arc::new(Cache::new(data_dir, &store, status)?); - let inner = Mutex::new(InnerState { client, cache }); + let cfs = (0..MAX_ADDRESSES) + .flat_map(|i| { + let pk: PhoenixPublicKey = + derive_phoenix_pk(store.get_seed(), i as u8); + + [format!("{:?}", pk), format!("spent_{:?}", pk)] + }) + .collect(); + + let cache = Mutex::new(Arc::new(Cache::new(data_dir, cfs, status)?)); Ok(Self { - inner, - status, + cache, + sync_rx: None, store, + prover, + status, + client, }) } - pub async fn check_connection(&self) -> Result<(), reqwest::Error> { - let client = { self.inner.lock().unwrap().client.clone() }; + pub(crate) fn cache(&self) -> Arc { + let state = self.cache.lock().unwrap(); - client.check_connection().await + Arc::clone(&state) } - pub async fn register_sync( - &self, - sync_tx: Sender, - ) -> Result<(), Error> { - let state = self.inner.lock().unwrap(); + pub async fn register_sync(&mut self) -> Result<(), Error> { + let (sync_tx, sync_rx) = flume::unbounded::(); + + self.sync_rx = Some(sync_rx); + + let cache = self.cache(); let status = self.status; + let client = self.client.clone(); let store = self.store.clone(); - let client = state.client.clone(); - let cache = Arc::clone(&state.cache); - let sender = Arc::new(sync_tx); status("Starting Sync.."); tokio::spawn(async move { loop { - let status = |_: &_| {}; - let sender = Arc::clone(&sender); - let _ = sender.send("Syncing..".to_string()); - - let sync_status = - sync_db(&client, &store, &cache, status).await; - let _ = match sync_status { - Ok(_) => sender.send("Syncing Complete".to_string()), - Err(e) => sender.send(format!("Error during sync:.. {e}")), + let _ = sync_tx.send("Syncing..".to_string()); + + let _ = match sync_db(&client, &cache, &store, status).await { + Ok(_) => sync_tx.send("Syncing Complete".to_string()), + Err(e) => sync_tx.send(format!("Error during sync:.. {e}")), }; sleep(Duration::from_secs(SYNC_INTERVAL_SECONDS)).await; @@ -248,133 +132,179 @@ impl StateStore { } pub async fn sync(&self) -> Result<(), Error> { - let store = self.store.clone(); + sync_db(&self.client, &self.cache(), &self.store, self.status).await + } + + /// Requests that a node prove the given transaction and later propagates it + /// Skips writing the proof for non phoenix transactions + pub fn prove_and_propagate( + &self, + utx: Transaction, + ) -> Result { let status = self.status; - let (cache, client) = { - let state = self.inner.lock().unwrap(); + let prover = &self.prover; + let mut utx = utx; - let cache = state.cache.clone(); - let client = state.client.clone(); - (cache, client) - }; + if let Transaction::Phoenix(tx) = &mut utx { + let status = self.status; + let proof = tx.proof(); - sync_db(&client, &store, cache.as_ref(), status).await - } + status("Attempt to prove tx..."); - pub(crate) fn cache(&self) -> Arc { - let state = self.inner.lock().unwrap(); - Arc::clone(&state.cache) - } -} + let prove_req = RuskRequest::new("prove_execute", proof.to_vec()); + + let proof = + prover.call(2, "rusk", &prove_req).wait().map_err(|e| { + ExecutionCoreError::PhoenixCircuit(e.to_string()) + })?; + + tx.set_proof(proof); + + status("Proving sucesss!"); + } -/// Types that are clients of the state API. -impl StateClient for StateStore { - /// Error returned by the node client. - type Error = Error; + let tx_bytes = utx.to_var_bytes(); + + status("Attempt to preverify tx..."); + let preverify_req = RuskRequest::new("preverify", tx_bytes.clone()); + let _ = self.client.call(2, "rusk", &preverify_req).wait()?; + status("Preverify success!"); + + status("Propagating tx..."); + let propagate_req = RuskRequest::new("propagate_tx", tx_bytes); + let _ = self.client.call(2, "Chain", &propagate_req).wait()?; + status("Transaction propagated!"); + + Ok(utx) + } /// Find notes for a view key, starting from the given block height. - fn fetch_notes( + pub(crate) fn inputs( &self, - vk: &ViewKey, - ) -> Result, Self::Error> { - let psk = vk.public_spend_key(); - let state = self.inner.lock().unwrap(); - - Ok(state - .cache - .notes(&psk)? + index: u8, + target: u64, + ) -> Result, Error> { + let vk = derive_phoenix_vk(self.store().get_seed(), index); + let mut sk = derive_phoenix_sk(self.store().get_seed(), index); + let pk = derive_phoenix_pk(self.store().get_seed(), index); + + let inputs: Result, Error> = self + .cache() + .notes(&pk)? .into_iter() - .map(|data| (data.note, data.height)) - .collect()) - } + .map(|data| { + let note = data.note; + let nullifiers = note.gen_nullifier(&sk); + let value = note.value(Some(&vk)).unwrap(); - /// Fetch the current anchor of the state. - fn fetch_anchor(&self) -> Result { - let state = self.inner.lock().unwrap(); + Ok((note, value, nullifiers)) + }) + .collect(); - self.status("Fetching anchor..."); + let inputs = try_input_notes(inputs?, target) + .into_iter() + .map(|(note, scalar)| { + let opening = self.fetch_opening(¬e)?; - let anchor = state - .client - .contract_query::<(), 0>(TRANSFER_CONTRACT, "root", &()) - .wait()?; - self.status("Anchor received!"); - let anchor = rkyv::from_bytes(&anchor).map_err(|_| Error::Rkyv)?; - Ok(anchor) - } + Ok((note, opening, scalar)) + }) + .collect(); - /// Asks the node to return the nullifiers that already exist from the given - /// nullifiers. - fn fetch_existing_nullifiers( - &self, - _nullifiers: &[BlsScalar], - ) -> Result, Self::Error> { - Ok(vec![]) + sk.zeroize(); + + inputs } - /// Queries the node to find the opening for a specific note. - fn fetch_opening( + pub(crate) fn fetch_notes( &self, - note: &Note, - ) -> Result, Self::Error> { - let state = self.inner.lock().unwrap(); + pk: &PhoenixPublicKey, + ) -> Result, Error> { + self.cache() + .notes(pk)? + .into_iter() + .map(|data| { + Ok(NoteData { + note: data.note, + height: data.height, + }) + }) + .collect() + } - self.status("Fetching opening notes..."); + /// Fetch the current root of the state. + pub(crate) fn fetch_root(&self) -> Result { + let status = self.status; + status("Fetching root..."); - let data = state + let root = self .client - .contract_query::<_, 1024>(TRANSFER_CONTRACT, "opening", note.pos()) + .contract_query::<(), 0>(TRANSFER_CONTRACT, "root", &()) .wait()?; - - self.status("Opening notes received!"); - - let branch = rkyv::from_bytes(&data).map_err(|_| Error::Rkyv)?; - Ok(branch) + status("root received!"); + let root = rkyv::from_bytes(&root).map_err(|_| Error::Rkyv)?; + Ok(root) } /// Queries the node for the amount staked by a key. - fn fetch_stake(&self, pk: &PublicKey) -> Result { - let state = self.inner.lock().unwrap(); - - self.status("Fetching stake..."); + pub(crate) fn fetch_stake( + &self, + pk: &AccountPublicKey, + ) -> Result, Error> { + let status = self.status; + status("Fetching stake..."); - let data = state + let data = self .client .contract_query::<_, 1024>(STAKE_CONTRACT, "get_stake", pk) .wait()?; let res: Option = rkyv::from_bytes(&data).map_err(|_| Error::Rkyv)?; - self.status("Stake received!"); + status("Stake received!"); let staking_address = pk.to_bytes().to_vec(); let staking_address = bs58::encode(staking_address).into_string(); println!("Staking address: {}", staking_address); - // FIX_ME: proper solution should to return an Option - // changing the trait implementation. That would reflect the state of - // the stake contract. It would be up to the consumer to decide what to - // do with a None - let stake = res - .map( - |StakeData { - amount, - reward, - counter, - }| StakeInfo { - amount, - reward, - counter, - }, + Ok(res) + } + + pub(crate) fn store(&self) -> &LocalStore { + &self.store + } + + pub(crate) fn fetch_chain_id(&self) -> Result { + let status = self.status; + status("Fetching chain_id..."); + + let data = self + .client + .contract_query::<_, { u8::SIZE }>( + TRANSFER_CONTRACT, + "chain_id", + &(), ) - .unwrap_or_default(); + .wait()?; + + let res: u8 = rkyv::from_bytes(&data).map_err(|_| Error::Rkyv)?; + status("Chain id received!"); - Ok(stake) + Ok(res) } -} -impl StateStore { - fn status(&self, text: &str) { - (self.status)(text) + /// Queries the node to find the opening for a specific note. + fn fetch_opening(&self, note: &Note) -> Result { + let status = self.status; + status("Fetching opening notes..."); + + let data = self + .client + .contract_query::<_, 1024>(TRANSFER_CONTRACT, "opening", note.pos()) + .wait()?; + + status("Opening notes received!"); + + let branch = rkyv::from_bytes(&data).map_err(|_| Error::Rkyv)?; + Ok(branch) } } diff --git a/rusk-wallet/src/clients/sync.rs b/rusk-wallet/src/clients/sync.rs index 40e2e27ee..c884c91b6 100644 --- a/rusk-wallet/src/clients/sync.rs +++ b/rusk-wallet/src/clients/sync.rs @@ -6,33 +6,36 @@ use std::mem::size_of; -use dusk_plonk::prelude::BlsScalar; -use dusk_wallet_core::Store; use futures::StreamExt; -use phoenix_core::transaction::{ArchivedTreeLeaf, TreeLeaf}; use crate::block::Block; use crate::clients::{Cache, TRANSFER_CONTRACT}; use crate::rusk::RuskHttpClient; -use crate::store::LocalStore; -use crate::{Error, RuskRequest, MAX_ADDRESSES}; +use crate::{Error, RuskRequest}; -const RKYV_TREE_LEAF_SIZE: usize = size_of::(); +use super::*; -pub(crate) async fn sync_db( +const TREE_LEAF: usize = size_of::(); + +pub(crate) async fn sync_db( client: &RuskHttpClient, - store: &LocalStore, cache: &Cache, - status: F, + store: &LocalStore, + status: fn(&str), ) -> Result<(), Error> { - let addresses: Vec<_> = (0..MAX_ADDRESSES) - .flat_map(|i| store.retrieve_ssk(i as u64)) - .map(|ssk| { - let vk = ssk.view_key(); - let psk = vk.public_spend_key(); - (ssk, vk, psk) - }) - .collect(); + let seed = store.get_seed(); + + let addresses: Vec<(PhoenixSecretKey, PhoenixViewKey, PhoenixPublicKey)> = + (0..MAX_ADDRESSES) + .map(|i| { + let i = i as u8; + ( + derive_phoenix_sk(seed, i), + derive_phoenix_vk(seed, i), + derive_phoenix_pk(seed, i), + ) + }) + .collect(); status("Getting cached note position..."); @@ -63,51 +66,62 @@ pub(crate) async fn sync_db( // This buffer is needed because `.bytes_stream();` introduce additional // spliting of chunks according to it's own buffer let mut buffer = vec![]; + let mut note_data = Vec::new(); while let Some(http_chunk) = stream.next().await { buffer.extend_from_slice(&http_chunk?); - let mut leaf_chunk = buffer.chunks_exact(RKYV_TREE_LEAF_SIZE); + let mut leaf_chunk = buffer.chunks_exact(TREE_LEAF); for leaf_bytes in leaf_chunk.by_ref() { - let TreeLeaf { block_height, note } = + let NoteLeaf { block_height, note } = rkyv::from_bytes(leaf_bytes).map_err(|_| Error::Rkyv)?; last_pos = std::cmp::max(last_pos, *note.pos()); - for (ssk, vk, psk) in addresses.iter() { - if vk.owns(¬e) { - let nullifier = note.gen_nullifier(ssk); - let spent = - fetch_existing_nullifiers_remote(client, &[nullifier]) - .wait()? - .first() - .is_some(); - let note = (note, nullifier); - match spent { - true => cache.insert_spent(psk, block_height, note), - false => cache.insert(psk, block_height, note), - }?; - - break; - } - } - cache.insert_last_pos(last_pos)?; + note_data.push((block_height, note)); } + cache.insert_last_pos(last_pos)?; + buffer = leaf_chunk.remainder().to_vec(); } + for (sk, vk, pk) in addresses.iter() { + for (block_height, note) in note_data.iter() { + if vk.owns(note.stealth_address()) { + let nullifier = note.gen_nullifier(sk); + let spent = + fetch_existing_nullifiers_remote(client, &[nullifier]) + .wait()? + .first() + .is_some(); + let note = (note.clone(), nullifier); + + match spent { + true => cache.insert_spent(pk, *block_height, note), + false => cache.insert(pk, *block_height, note), + }?; + } + } + } + // Remove spent nullifiers from live notes - for (_, _, psk) in addresses { - let nullifiers = cache.unspent_notes_id(&psk)?; + // zerorize all the secret keys + for (mut sk, _, pk) in addresses { + let nullifiers: Vec = cache.unspent_notes_id(&pk)?; if !nullifiers.is_empty() { let existing = - fetch_existing_nullifiers_remote(client, &nullifiers).wait()?; - cache.spend_notes(&psk, &existing)?; + fetch_existing_nullifiers_remote(client, nullifiers.as_slice()) + .wait()?; + + cache.spend_notes(&pk, existing.as_slice())?; } + + sk.zeroize(); } + Ok(()) } diff --git a/rusk-wallet/src/currency.rs b/rusk-wallet/src/currency.rs index 8222045d1..377ff0be3 100644 --- a/rusk-wallet/src/currency.rs +++ b/rusk-wallet/src/currency.rs @@ -11,7 +11,7 @@ use std::num::ParseFloatError; use std::ops::{Add, Deref, Div, Mul, Sub}; use std::str::FromStr; -use rusk_abi::dusk; +use super::*; /// The underlying unit of Dusk pub type Lux = u64; @@ -24,7 +24,7 @@ impl Dusk { /// The smallest value that can be represented by Dusk currency pub const MIN: Dusk = Dusk(0); /// The largest value that can be represented by Dusk currency - pub const MAX: Dusk = Dusk(dusk::dusk(f64::MAX / dusk::dusk(1.0) as f64)); + pub const MAX: Dusk = Dusk(dusk(f64::MAX / dusk(1.0) as f64)); /// Returns a new Dusk based on the [Lux] given pub const fn new(lux: Lux) -> Dusk { @@ -70,18 +70,18 @@ impl Sub for Dusk { impl Mul for Dusk { type Output = Self; fn mul(self, other: Self) -> Self { - let a = dusk::from_dusk(self.0); - let b = dusk::from_dusk(other.0); - Self(dusk::dusk(a * b)) + let a = from_dusk(self.0); + let b = from_dusk(other.0); + Self(dusk(a * b)) } } impl Mul for Dusk { type Output = Self; fn mul(self, other: Lux) -> Self { - let a = dusk::from_dusk(self.0); - let b = dusk::from_dusk(other); - Self(dusk::dusk(a * b)) + let a = from_dusk(self.0); + let b = from_dusk(other); + Self(dusk(a * b)) } } @@ -89,14 +89,14 @@ impl Mul for Dusk { impl Div for Dusk { type Output = Self; fn div(self, other: Self) -> Self { - Self(dusk::dusk(self.0 as f64 / other.0 as f64)) + Self(dusk(self.0 as f64 / other.0 as f64)) } } impl Div for Dusk { type Output = Self; fn div(self, other: Lux) -> Self { - Self(dusk::dusk(self.0 as f64 / other as f64)) + Self(dusk(self.0 as f64 / other as f64)) } } @@ -118,7 +118,7 @@ impl PartialEq for Dusk { } impl PartialEq for Dusk { fn eq(&self, other: &f64) -> bool { - self.0 == dusk::dusk(*other) + self.0 == dusk(*other) } } @@ -141,7 +141,7 @@ impl PartialOrd for Dusk { } impl PartialOrd for Dusk { fn partial_cmp(&self, other: &f64) -> Option { - self.0.partial_cmp(&dusk::dusk(*other)) + self.0.partial_cmp(&dusk(*other)) } } @@ -154,13 +154,13 @@ impl From for Dusk { if val < 0.0 { panic!("Dusk type does not support negative values"); } - Self(dusk::dusk(val)) + Self(dusk(val)) } } impl From for f64 { fn from(val: Dusk) -> f64 { - dusk::from_dusk(*val) + from_dusk(*val) } } diff --git a/rusk-wallet/src/dat.rs b/rusk-wallet/src/dat.rs index 836f549e2..6ab92adaf 100644 --- a/rusk-wallet/src/dat.rs +++ b/rusk-wallet/src/dat.rs @@ -4,13 +4,12 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. -use dusk_bytes::DeserializableSlice; - use std::fs; use std::io::Read; +use wallet_core::keys::Seed; + use crate::crypto::decrypt; -use crate::store; use crate::Error; use crate::WalletPath; @@ -51,7 +50,7 @@ pub(crate) fn get_seed_and_address( file: DatFileVersion, mut bytes: Vec, pwd: &[u8], -) -> Result<(store::Seed, u8), Error> { +) -> Result<(Seed, u8), Error> { match file { DatFileVersion::Legacy => { if bytes[1] == 0 && bytes[2] == 0 { @@ -61,7 +60,8 @@ pub(crate) fn get_seed_and_address( bytes = decrypt(&bytes, pwd)?; // get our seed - let seed = store::Seed::from_reader(&mut &bytes[..]) + let seed = bytes[..] + .try_into() .map_err(|_| Error::WalletFileCorrupted)?; Ok((seed, 1)) @@ -69,23 +69,24 @@ pub(crate) fn get_seed_and_address( DatFileVersion::OldWalletCli((major, minor, _, _, _)) => { bytes.drain(..5); - let result: Result<(store::Seed, u8), Error> = match (major, minor) - { + let result: Result<(Seed, u8), Error> = match (major, minor) { (1, 0) => { let content = decrypt(&bytes, pwd)?; - let mut buff = &content[..]; + let buff = &content[..]; - let seed = store::Seed::from_reader(&mut buff) + let seed = buff + .try_into() .map_err(|_| Error::WalletFileCorrupted)?; Ok((seed, 1)) } (2, 0) => { let content = decrypt(&bytes, pwd)?; - let mut buff = &content[..]; + let buff = &content[..]; // extract seed - let seed = store::Seed::from_reader(&mut buff) + let seed = buff + .try_into() .map_err(|_| Error::WalletFileCorrupted)?; // extract addresses count @@ -102,7 +103,8 @@ pub(crate) fn get_seed_and_address( let content = decrypt(rest, pwd)?; if let Some(seed_buff) = content.get(0..65) { - let seed = store::Seed::from_reader(&mut &seed_buff[0..64]) + let seed = seed_buff[0..64] + .try_into() .map_err(|_| Error::WalletFileCorrupted)?; let count = &seed_buff[64..65]; diff --git a/rusk-wallet/src/error.rs b/rusk-wallet/src/error.rs index ba1a89252..430a6f20e 100644 --- a/rusk-wallet/src/error.rs +++ b/rusk-wallet/src/error.rs @@ -4,18 +4,10 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. -use crate::clients::StateStore; -use crate::store::LocalStore; -use phoenix_core::Error as PhoenixError; use rand_core::Error as RngError; use std::io; use std::str::Utf8Error; -use super::clients; -/// Wallet core error -pub(crate) type CoreError = - dusk_wallet_core::Error; - /// Errors returned by this library #[derive(Debug, thiserror::Error)] pub enum Error { @@ -52,9 +44,6 @@ pub enum Error { /// Random number generator errors #[error(transparent)] Rng(#[from] RngError), - /// Transaction model errors - #[error("An error occurred in Phoenix: {0:?}")] - Phoenix(PhoenixError), /// Not enough balance to perform transaction #[error("Insufficient balance to perform this operation")] NotEnoughBalance, @@ -124,6 +113,9 @@ pub enum Error { /// The cache database couldn't find column family required #[error("Cache database corrupted")] CacheDatabaseCorrupted, + /// Prover errors from execution-core + #[error("Prover Error")] + ProverError(String), } impl From for Error { @@ -138,21 +130,23 @@ impl From for Error { } } -impl From for Error { - fn from(e: CoreError) -> Self { - use dusk_wallet_core::Error::*; +impl From for Error { + fn from(e: execution_core::Error) -> Self { + use execution_core::Error::*; + match e { - Store(err) | State(err) | Prover(err) => err, - Rkyv => Self::Rkyv, - Rng(err) => Self::Rng(err), - Bytes(err) => Self::Bytes(err), - Phoenix(err) => Self::Phoenix(err), - NotEnoughBalance => Self::NotEnoughBalance, - NoteCombinationProblem => Self::NoteCombinationProblem, - AlreadyStaked { .. } => Self::AlreadyStaked, - NotStaked { .. } => Self::NotStaked, - NoReward { .. } => Self::NoReward, - Utf8(err) => Self::Utf8(err.utf8_error()), + InsufficientBalance => Self::NotEnoughBalance, + Replay => Self::Transaction("Replay".to_string()), + PhoenixOwnership => Self::AddressNotOwned, + PhoenixCircuit(s) | PhoenixProver(s) => Self::ProverError(s), + InvalidData => Self::Bytes(dusk_bytes::Error::InvalidData), + BadLength(found, expected) => { + Self::Bytes(dusk_bytes::Error::BadLength { found, expected }) + } + InvalidChar(ch, index) => { + Self::Bytes(dusk_bytes::Error::InvalidChar { ch, index }) + } + Rkyv(_) => Self::Rkyv, } } } diff --git a/rusk-wallet/src/lib.rs b/rusk-wallet/src/lib.rs index ab967a9fe..5d0371c63 100644 --- a/rusk-wallet/src/lib.rs +++ b/rusk-wallet/src/lib.rs @@ -14,12 +14,13 @@ #![deny(missing_docs)] +/// module to help with currency conversions +pub mod currency; + mod block; mod cache; mod clients; mod crypto; - -mod currency; mod error; mod rusk; mod store; @@ -30,11 +31,24 @@ pub mod dat; pub use rusk::{RuskHttpClient, RuskRequest}; -pub use currency::{Dusk, Lux}; pub use error::Error; pub use wallet::gas; pub use wallet::{Address, DecodedNote, SecureWalletFile, Wallet, WalletPath}; +use execution_core::{ + dusk, from_dusk, + signatures::bls::PublicKey as AccountPublicKey, + stake::StakeData, + transfer::phoenix::{ + ArchivedNoteLeaf, Note, NoteLeaf, NoteOpening, + PublicKey as PhoenixPublicKey, SecretKey as PhoenixSecretKey, + ViewKey as PhoenixViewKey, + }, + BlsScalar, +}; + +use currency::Dusk; + /// The largest amount of Dusk that is possible to convert pub const MAX_CONVERTIBLE: Dusk = Dusk::MAX; /// The smallest amount of Dusk that is possible to convert @@ -44,7 +58,7 @@ pub const EPOCH: u64 = 2160; /// Max addresses the wallet can store pub const MAX_ADDRESSES: usize = get_max_addresses(); -const DEFAULT_MAX_ADDRESSES: usize = 25; +const DEFAULT_MAX_ADDRESSES: usize = 1; const fn get_max_addresses() -> usize { match option_env!("WALLET_MAX_ADDR") { diff --git a/rusk-wallet/src/rusk.rs b/rusk-wallet/src/rusk.rs index fbcd06581..b1eb580f5 100644 --- a/rusk-wallet/src/rusk.rs +++ b/rusk-wallet/src/rusk.rs @@ -38,6 +38,7 @@ impl RuskRequest { Ok(buffer) } } + #[derive(Clone)] /// Rusk HTTP Binary Client pub struct RuskHttpClient { diff --git a/rusk-wallet/src/store.rs b/rusk-wallet/src/store.rs index d8470a317..b5c31aec5 100644 --- a/rusk-wallet/src/store.rs +++ b/rusk-wallet/src/store.rs @@ -4,18 +4,18 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. -use crate::clients::StateStore; -use crate::Error; +use crate::clients::State; use dusk_bytes::{Error as BytesError, Serializable}; -use dusk_wallet_core::Store; + +use wallet_core::keys::{self, RNG_SEED}; #[derive(Clone)] -pub struct Seed([u8; 64]); +pub struct Seed(keys::Seed); impl Default for Seed { fn default() -> Self { - Self([0u8; 64]) + Self([0u8; RNG_SEED]) } } @@ -36,27 +36,22 @@ pub(crate) struct LocalStore { seed: Seed, } -impl Store for LocalStore { - type Error = Error; - +impl LocalStore { /// Retrieves the seed used to derive keys. - fn get_seed(&self) -> Result<[u8; Seed::SIZE], Self::Error> { - Ok(self.seed.to_bytes()) + pub fn get_seed(&self) -> &[u8; Seed::SIZE] { + &self.seed.0 } } -impl Store for StateStore { - type Error = Error; - - /// Retrieves the seed used to derive keys. - fn get_seed(&self) -> Result<[u8; Seed::SIZE], Self::Error> { - Ok(self.store.seed.to_bytes()) +impl From<[u8; Seed::SIZE]> for LocalStore { + fn from(seed: [u8; Seed::SIZE]) -> Self { + LocalStore { seed: Seed(seed) } } } -impl LocalStore { - /// Creates a new store from a known seed - pub(crate) fn new(seed: Seed) -> Self { - LocalStore { seed } +impl State { + /// Retrieves the seed used to derive keys. + pub fn get_seed(&self) -> &[u8; Seed::SIZE] { + self.store().get_seed() } } diff --git a/rusk-wallet/src/wallet.rs b/rusk-wallet/src/wallet.rs index 54f5abab0..4a2c6756a 100644 --- a/rusk-wallet/src/wallet.rs +++ b/rusk-wallet/src/wallet.rs @@ -9,44 +9,55 @@ mod file; pub mod gas; pub use address::Address; -use dusk_plonk::prelude::BlsScalar; pub use file::{SecureWalletFile, WalletPath}; use bip39::{Language, Mnemonic, Seed}; -use dusk_bytes::{DeserializableSlice, Serializable}; -use ff::Field; -use flume::Receiver; -use phoenix_core::transaction::ModuleId; -use phoenix_core::Note; -use rkyv::ser::serializers::AllocSerializer; +use dusk_bytes::Serializable; +use rand::rngs::StdRng; +use rand::SeedableRng; + use serde::Serialize; use std::fmt::Debug; use std::fs; use std::path::{Path, PathBuf}; -use dusk_bls12_381_sign::{PublicKey, SecretKey}; -use dusk_wallet_core::{ - BalanceInfo, StakeInfo, StateClient, Store, Transaction, - Wallet as WalletCore, MAX_CALL_SIZE, +use wallet_core::{ + phoenix_balance, + prelude::keys::{ + derive_bls_sk, derive_phoenix_pk, derive_phoenix_sk, derive_phoenix_vk, + }, + transaction::{ + phoenix, phoenix_stake, phoenix_stake_reward, phoenix_unstake, + }, + BalanceInfo, }; -use rand::prelude::StdRng; -use rand::SeedableRng; -use dusk_pki::{PublicSpendKey, SecretSpendKey}; +use execution_core::{ + signatures::bls::{PublicKey as BlsPublicKey, SecretKey as BlsSecretKey}, + transfer::{ + contract_exec::{ContractCall, ContractExec}, + Transaction, + }, +}; -use crate::cache::NoteData; -use crate::clients::{Prover, StateStore}; -use crate::crypto::encrypt; -use crate::currency::Dusk; -use crate::dat::{ - self, version_bytes, DatFileVersion, FILE_TYPE, LATEST_VERSION, MAGIC, - RESERVED, +use zeroize::Zeroize; + +use super::*; + +use crate::{ + cache::NoteData, + clients::{Prover, State}, + crypto::encrypt, + currency::Dusk, + dat::{ + self, version_bytes, DatFileVersion, FILE_TYPE, LATEST_VERSION, MAGIC, + RESERVED, + }, + store::LocalStore, + Error, RuskHttpClient, }; -use crate::store::LocalStore; -use crate::{Error, RuskHttpClient}; -use gas::Gas; -use crate::store; +use gas::Gas; /// The interface to the Dusk Network /// @@ -62,14 +73,11 @@ use crate::store; /// able to perform common operations such as checking balance, transfernig /// funds, or staking Dusk. pub struct Wallet { - wallet: Option>, addresses: Vec
, + state: Option, store: LocalStore, file: Option, file_version: Option, - status: fn(status: &str), - /// Recieve the status/errors of the sync procss - pub sync_rx: Option>, } impl Wallet { @@ -78,23 +86,23 @@ impl Wallet { &self.file } - /// Returns spending key pair for a given address - pub fn spending_keys( + /// Returns phoenix key pair for a given address + pub fn phoenix_keys( &self, addr: &Address, - ) -> Result<(PublicSpendKey, SecretSpendKey), Error> { + ) -> Result<(PhoenixPublicKey, PhoenixSecretKey), Error> { // make sure we own the address if !addr.is_owned() { return Err(Error::Unauthorized); } - let index = addr.index()? as u64; + let index = addr.index()?; // retrieve keys - let ssk = self.store.retrieve_ssk(index)?; - let psk: PublicSpendKey = ssk.public_spend_key(); + let sk = derive_phoenix_sk(self.store.get_seed(), index); + let pk = addr.pk(); - Ok((psk, ssk)) + Ok((*pk, sk)) } } @@ -113,29 +121,18 @@ impl Wallet { // derive the mnemonic seed let seed = Seed::new(&mnemonic, ""); // Takes the mnemonic seed as bytes - let mut bytes = seed.as_bytes(); - - // Generate a Store Seed type from the mnemonic Seed bytes - let seed = store::Seed::from_reader(&mut bytes)?; - - let store = LocalStore::new(seed); + let bytes = seed.as_bytes().try_into().unwrap(); // Generate the default address - let ssk = store - .retrieve_ssk(0) - .expect("wallet seed should be available"); - - let address = Address::new(0, ssk.public_spend_key()); + let address = Address::new(0, derive_phoenix_pk(&bytes, 0)); // return new wallet instance Ok(Wallet { - wallet: None, addresses: vec![address], - store, + state: None, + store: LocalStore::from(bytes), file: None, file_version: None, - status: |_| {}, - sync_rx: None, }) } else { Err(Error::InvalidMnemonicPhrase) @@ -161,47 +158,31 @@ impl Wallet { let (seed, address_count) = dat::get_seed_and_address(file_version, bytes, pwd)?; - let store = LocalStore::new(seed); - // return early if its legacy if let DatFileVersion::Legacy = file_version { - let ssk = store - .retrieve_ssk(0) - .expect("wallet seed should be available"); - - let address = Address::new(0, ssk.public_spend_key()); + let address = Address::new(0, derive_phoenix_pk(&seed, 0)); // return the store return Ok(Self { - wallet: None, addresses: vec![address], - store, + store: LocalStore::from(seed), + state: None, file: Some(file), file_version: Some(DatFileVersion::Legacy), - status: |_| {}, - sync_rx: None, }); } let addresses: Vec<_> = (0..address_count) - .map(|i| { - let ssk = store - .retrieve_ssk(i as u64) - .expect("wallet seed should be available"); - - Address::new(i, ssk.public_spend_key()) - }) + .map(|i| Address::new(i, derive_phoenix_pk(&seed, i))) .collect(); // create and return Ok(Self { - wallet: None, addresses, - store, + store: LocalStore::from(seed), + state: None, file: Some(file), file_version: Some(file_version), - status: |_| {}, - sync_rx: None, }) } @@ -219,7 +200,7 @@ impl Wallet { header.extend_from_slice(&version_bytes(LATEST_VERSION)); // create file payload - let seed = self.store.get_seed()?; + let seed = self.store.get_seed(); let mut payload = seed.to_vec(); payload.push(self.addresses.len() as u8); @@ -250,6 +231,15 @@ impl Wallet { self.save() } + /// Access the inner state of the wallet + pub fn state(&self) -> Result<&State, Error> { + if let Some(state) = self.state.as_ref() { + Ok(state) + } else { + Err(Error::Offline) + } + } + /// Connect the wallet to the network providing a callback for status /// updates pub async fn connect_with_status( @@ -274,10 +264,6 @@ impl Wallet { _=> {}, } - // create a prover client - let mut prover = Prover::new(http_state.clone(), http_prover.clone()); - prover.set_status_callback(status); - let cache_dir = { if let Some(file) = &self.file { file.path().cache_dir() @@ -287,58 +273,33 @@ impl Wallet { }; // create a state client - let state = StateStore::new( - http_state, + self.state = Some(State::new( &cache_dir, - self.store.clone(), status, - )?; - - // create wallet instance - self.wallet = Some(WalletCore::new(self.store.clone(), state, prover)); - - // set our own status callback - self.status = status; + http_state, + http_prover, + self.store.clone(), + )?); Ok(()) } /// Sync wallet state pub async fn sync(&self) -> Result<(), Error> { - self.connected_wallet().await?.state().sync().await + self.state()?.sync().await } /// Helper function to register for async-sync outside of connect pub async fn register_sync(&mut self) -> Result<(), Error> { - match self.wallet.as_ref() { - Some(w) => { - let (sync_tx, sync_rx) = flume::unbounded::(); - w.state().register_sync(sync_tx).await?; - self.sync_rx = Some(sync_rx); - Ok(()) - } + match self.state.as_mut() { + Some(w) => w.register_sync().await, None => Err(Error::Offline), } } /// Checks if the wallet has an active connection to the network pub async fn is_online(&self) -> bool { - match self.wallet.as_ref() { - Some(w) => w.state().check_connection().await.is_ok(), - None => false, - } - } - - pub(crate) async fn connected_wallet( - &self, - ) -> Result<&WalletCore, Error> { - match self.wallet.as_ref() { - Some(w) => { - w.state().check_connection().await?; - Ok(w) - } - None => Err(Error::Offline), - } + self.state.is_some() } /// Fetches the notes from the state. @@ -350,18 +311,18 @@ impl Wallet { return Err(Error::Unauthorized); } - let wallet = self.connected_wallet().await?; - let ssk_index = addr.index()? as u64; - let ssk = self.store.retrieve_ssk(ssk_index).unwrap(); - let vk = ssk.view_key(); - let psk = vk.public_spend_key(); + let seed = self.store.get_seed(); + + let index = addr.index()?; + let vk = derive_phoenix_vk(seed, index); + let pk = addr.pk(); - let live_notes = wallet.state().fetch_notes(&vk).unwrap(); - let spent_notes = wallet.state().cache().spent_notes(&psk)?; + let live_notes = self.state()?.fetch_notes(pk)?; + let spent_notes = self.state()?.cache().spent_notes(pk)?; let live_notes = live_notes .into_iter() - .map(|(note, height)| (None, note, height)); + .map(|data| (None, data.note, data.height)); let spent_notes = spent_notes.into_iter().map( |(nullifier, NoteData { note, height })| { (Some(nullifier), note, height) @@ -388,29 +349,37 @@ impl Wallet { &self, addr: &Address, ) -> Result { + let state = self.state()?; // make sure we own this address if !addr.is_owned() { return Err(Error::Unauthorized); } - // get balance - if let Some(wallet) = &self.wallet { - let index = addr.index()? as u64; - Ok(wallet.get_balance(index)?) - } else { - Err(Error::Offline) - } + let index = addr.index()?; + let notes: Vec = state + .fetch_notes(addr.pk())? + .into_iter() + .map(|data| NoteLeaf { + note: data.note, + block_height: data.height, + }) + .collect(); + + let seed = self.store.get_seed(); + + Ok(phoenix_balance( + &derive_phoenix_vk(seed, index), + notes.into_iter(), + )) } /// Creates a new public address. /// The addresses generated are deterministic across sessions. pub fn new_address(&mut self) -> &Address { + let seed = self.store.get_seed(); let len = self.addresses.len(); - let ssk = self - .store - .retrieve_ssk(len as u64) - .expect("wallet seed should be available"); - let addr = Address::new(len as u8, ssk.public_spend_key()); + let pk = derive_phoenix_pk(seed, len as u8); + let addr = Address::new(len as u8, pk); self.addresses.push(addr); self.addresses.last().unwrap() @@ -427,18 +396,14 @@ impl Wallet { } /// Executes a generic contract call - pub async fn execute( + pub async fn execute( &self, sender: &Address, - contract_id: ModuleId, - call_name: String, - call_data: C, + reciever: &Address, + deposit: Dusk, + exec: Option>, gas: Gas, - ) -> Result - where - C: rkyv::Serialize>, - { - let wallet = self.connected_wallet().await?; + ) -> Result { // make sure we own the sender address if !sender.is_owned() { return Err(Error::Unauthorized); @@ -449,22 +414,45 @@ impl Wallet { return Err(Error::NotEnoughGas); } + let seed = self.store.get_seed(); + + let state = self.state()?; + let deposit = *deposit; + let mut rng = StdRng::from_entropy(); - let sender_index = - sender.index().expect("owned address should have an index"); + let sender_index = sender.index()?; + let mut sender_sk = derive_phoenix_sk(seed, sender_index); - // transfer - let tx = wallet.execute( + let inputs = state + .inputs(sender_index, deposit + gas.limit * gas.price)? + .into_iter() + .map(|(a, b, _)| (a, b)) + .collect(); + + let root = state.fetch_root()?; + let chain_id = state.fetch_chain_id()?; + + let tx = phoenix::<_, Prover>( &mut rng, - contract_id.into(), - call_name, - call_data, - sender_index as u64, - sender.psk(), + &sender_sk, + sender.pk(), + reciever.pk(), + inputs, + root, + 0, + true, + deposit, gas.limit, gas.price, + chain_id, + exec, )?; - Ok(tx) + + let tx = state.prove_and_propagate(tx); + + sender_sk.zeroize(); + + tx } /// Transfers funds between addresses @@ -475,7 +463,6 @@ impl Wallet { amt: Dusk, gas: Gas, ) -> Result { - let wallet = self.connected_wallet().await?; // make sure we own the sender address if !sender.is_owned() { return Err(Error::Unauthorized); @@ -489,23 +476,48 @@ impl Wallet { return Err(Error::NotEnoughGas); } + let state = self.state()?; + let mut rng = StdRng::from_entropy(); - let ref_id = BlsScalar::random(&mut rng); - let sender_index = - sender.index().expect("owned address should have an index"); + let sender_index = sender.index()?; + let amt = *amt; + + let seed = self.store.get_seed(); - // transfer - let tx = wallet.transfer( + let mut sender_sk = derive_phoenix_sk(seed, sender_index); + let change_pk = sender.pk(); + let reciever_pk = rcvr.pk(); + + let inputs = state + .inputs(sender_index, amt + gas.limit * gas.price)? + .into_iter() + .map(|(a, b, _)| (a, b)) + .collect(); + + let root = state.fetch_root()?; + let chain_id = state.fetch_chain_id()?; + + let tx = phoenix::<_, Prover>( &mut rng, - sender_index as u64, - sender.psk(), - rcvr.psk(), - *amt, + &sender_sk, + change_pk, + reciever_pk, + inputs, + root, + amt, + true, + 0, gas.limit, gas.price, - ref_id, + chain_id, + None::, )?; - Ok(tx) + + let tx = state.prove_and_propagate(tx); + + sender_sk.zeroize(); + + tx } /// Stakes Dusk @@ -515,7 +527,6 @@ impl Wallet { amt: Dusk, gas: Gas, ) -> Result { - let wallet = self.connected_wallet().await?; // make sure we own the staking address if !addr.is_owned() { return Err(Error::Unauthorized); @@ -529,31 +540,54 @@ impl Wallet { return Err(Error::NotEnoughGas); } + let state = self.state()?; + let seed = self.store.get_seed(); + let mut rng = StdRng::from_entropy(); + let amt = *amt; let sender_index = addr.index()?; + let mut sender_sk = derive_phoenix_sk(seed, sender_index); + let mut stake_sk = derive_bls_sk(seed, sender_index); - // stake - let tx = wallet.stake( - &mut rng, - sender_index as u64, - sender_index as u64, - addr.psk(), - *amt, - gas.limit, - gas.price, + let nonce = state + .fetch_stake(&AccountPublicKey::from(&stake_sk))? + .map(|s| s.nonce) + .unwrap_or(0); + + let inputs = state + .inputs(sender_index, amt + gas.limit * gas.price)? + .into_iter() + .map(|(a, b, _)| (a, b)) + .collect(); + + let root = state.fetch_root()?; + let chain_id = state.fetch_chain_id()?; + + let stake = phoenix_stake::<_, Prover>( + &mut rng, &sender_sk, &stake_sk, inputs, root, gas.limit, + gas.price, chain_id, amt, nonce, )?; - Ok(tx) + + let tx = state.prove_and_propagate(stake); + + sender_sk.zeroize(); + stake_sk.zeroize(); + + tx } /// Obtains stake information for a given address - pub async fn stake_info(&self, addr: &Address) -> Result { - let wallet = self.connected_wallet().await?; - // make sure we own the staking address - if !addr.is_owned() { - return Err(Error::Unauthorized); - } - let index = addr.index()? as u64; - wallet.get_stake(index).map_err(Error::from) + pub async fn stake_info( + &self, + addr: &Address, + ) -> Result, Error> { + let seed = self.store.get_seed(); + + self.state()? + .fetch_stake(&AccountPublicKey::from(&derive_bls_sk( + seed, + addr.index()?, + ))) } /// Unstakes Dusk @@ -562,24 +596,49 @@ impl Wallet { addr: &Address, gas: Gas, ) -> Result { - let wallet = self.connected_wallet().await?; // make sure we own the staking address if !addr.is_owned() { return Err(Error::Unauthorized); } let mut rng = StdRng::from_entropy(); - let index = addr.index()? as u64; + let index = addr.index()?; + let seed = self.store.get_seed(); + + let state = self.state()?; + + let mut sender_sk = derive_phoenix_sk(seed, index); + let mut stake_sk = derive_bls_sk(seed, index); + + let unstake_value = state + .fetch_stake(&AccountPublicKey::from(&stake_sk))? + .and_then(|s| s.amount) + .map(|s| s.value) + .unwrap_or(0); + + let inputs = state.inputs(index, gas.limit * gas.price)?; + + let root = state.fetch_root()?; + let chain_id = state.fetch_chain_id()?; - let tx = wallet.unstake( + let unstake = phoenix_unstake::<_, Prover>( &mut rng, - index, - index, - addr.psk(), + &sender_sk, + &stake_sk, + inputs, + root, + unstake_value, gas.limit, gas.price, + chain_id, )?; - Ok(tx) + + let tx = state.prove_and_propagate(unstake); + + sender_sk.zeroize(); + stake_sk.zeroize(); + + tx } /// Withdraw accumulated staking reward for a given address @@ -588,41 +647,62 @@ impl Wallet { addr: &Address, gas: Gas, ) -> Result { - let wallet = self.connected_wallet().await?; + let state = self.state()?; // make sure we own the staking address if !addr.is_owned() { return Err(Error::Unauthorized); } let mut rng = StdRng::from_entropy(); - let index = addr.index()? as u64; + let index = addr.index()?; + let seed = self.store.get_seed(); + + let mut sender_sk = derive_phoenix_sk(seed, index); + let mut stake_sk = derive_bls_sk(seed, index); + + let inputs = state.inputs(index, gas.limit * gas.price)?; + + let root = state.fetch_root()?; + let chain_id = state.fetch_chain_id()?; - let tx = wallet.withdraw( + let reward_amount = state + .fetch_stake(&AccountPublicKey::from(&stake_sk))? + .map(|s| s.reward) + .unwrap_or(0); + + let withdraw = phoenix_stake_reward::<_, Prover>( &mut rng, - index, - index, - addr.psk(), + &sender_sk, + &stake_sk, + inputs, + root, + reward_amount, gas.limit, gas.price, + chain_id, )?; - Ok(tx) + + let tx = state.prove_and_propagate(withdraw); + + sender_sk.zeroize(); + stake_sk.zeroize(); + + tx } /// Returns bls key pair for provisioner nodes pub fn provisioner_keys( &self, addr: &Address, - ) -> Result<(PublicKey, SecretKey), Error> { + ) -> Result<(BlsPublicKey, BlsSecretKey), Error> { // make sure we own the staking address if !addr.is_owned() { return Err(Error::Unauthorized); } - let index = addr.index()? as u64; - - // retrieve keys - let sk = self.store.retrieve_sk(index)?; - let pk: PublicKey = From::from(&sk); + let index = addr.index()?; + let sk = derive_bls_sk(self.store.get_seed(), index); + let pk = BlsPublicKey::from(&sk); Ok((pk, sk)) } @@ -672,7 +752,7 @@ impl Wallet { pub fn claim_as_address(&self, addr: Address) -> Result<&Address, Error> { self.addresses() .iter() - .find(|a| a.psk == addr.psk) + .find(|a| a.pk == addr.pk) .ok_or(Error::AddressNotOwned) } diff --git a/rusk-wallet/src/wallet/address.rs b/rusk-wallet/src/wallet/address.rs index 8c4c46d5c..dd3e19f09 100644 --- a/rusk-wallet/src/wallet/address.rs +++ b/rusk-wallet/src/wallet/address.rs @@ -4,25 +4,27 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. -use crate::Error; -use dusk_bytes::{DeserializableSlice, Error as BytesError, Serializable}; -use dusk_pki::PublicSpendKey; use std::fmt; use std::hash::Hasher; use std::str::FromStr; +use super::*; +use crate::Error; + +use dusk_bytes::{DeserializableSlice, Error as BytesError, Serializable}; + #[derive(Clone, Eq)] /// A public address within the Dusk Network pub struct Address { pub(crate) index: Option, - pub(crate) psk: PublicSpendKey, + pub(crate) pk: PhoenixPublicKey, } impl Address { - pub(crate) fn new(index: u8, psk: PublicSpendKey) -> Self { + pub(crate) fn new(index: u8, pk: PhoenixPublicKey) -> Self { Self { index: Some(index), - psk, + pk, } } @@ -31,8 +33,8 @@ impl Address { self.index.is_some() } - pub(crate) fn psk(&self) -> &PublicSpendKey { - &self.psk + pub(crate) fn pk(&self) -> &PhoenixPublicKey { + &self.pk } pub(crate) fn index(&self) -> Result { @@ -41,7 +43,7 @@ impl Address { /// A trimmed version of the address to display as preview pub fn preview(&self) -> String { - let addr = bs58::encode(self.psk.to_bytes()).into_string(); + let addr = bs58::encode(self.pk.to_bytes()).into_string(); format!("{}...{}", &addr[..7], &addr[addr.len() - 7..]) } } @@ -52,10 +54,10 @@ impl FromStr for Address { fn from_str(s: &str) -> Result { let bytes = bs58::decode(s).into_vec()?; - let psk = PublicSpendKey::from_reader(&mut &bytes[..]) + let pk = PhoenixPublicKey::from_reader(&mut &bytes[..]) .map_err(|_| Error::BadAddress)?; - let addr = Address { index: None, psk }; + let addr = Address { index: None, pk }; Ok(addr) } @@ -69,15 +71,15 @@ impl TryFrom for Address { } } -impl TryFrom<&[u8; PublicSpendKey::SIZE]> for Address { +impl TryFrom<&[u8; PhoenixPublicKey::SIZE]> for Address { type Error = Error; fn try_from( - bytes: &[u8; PublicSpendKey::SIZE], + bytes: &[u8; PhoenixPublicKey::SIZE], ) -> Result { let addr = Address { index: None, - psk: dusk_pki::PublicSpendKey::from_bytes(bytes)?, + pk: PhoenixPublicKey::from_bytes(bytes)?, }; Ok(addr) } @@ -85,26 +87,26 @@ impl TryFrom<&[u8; PublicSpendKey::SIZE]> for Address { impl PartialEq for Address { fn eq(&self, other: &Self) -> bool { - self.index == other.index && self.psk == other.psk + self.index == other.index && self.pk == other.pk } } impl std::hash::Hash for Address { fn hash(&self, state: &mut H) { self.index.hash(state); - self.psk.to_bytes().hash(state); + self.pk.to_bytes().hash(state); } } impl fmt::Display for Address { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", bs58::encode(self.psk.to_bytes()).into_string()) + write!(f, "{}", bs58::encode(self.pk.to_bytes()).into_string()) } } impl fmt::Debug for Address { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", bs58::encode(self.psk.to_bytes()).into_string()) + write!(f, "{}", bs58::encode(self.pk.to_bytes()).into_string()) } } From 645ea519e77c04d18f6286f80fd24c86d07f0994 Mon Sep 17 00:00:00 2001 From: Daksh <41485688+Daksh14@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:11:05 -0400 Subject: [PATCH 2/2] wallet-core: Add input module - Add algorithm for picking notes - Re-export some commonly used types --- wallet-core/src/ffi.rs | 3 +- wallet-core/src/input.rs | 128 +++++++++++++++++++++++++++++++++ wallet-core/src/keys.rs | 6 +- wallet-core/src/lib.rs | 21 +++--- wallet-core/tests/notes.rs | 144 ++++++++++++++++++++++++++----------- 5 files changed, 248 insertions(+), 54 deletions(-) create mode 100644 wallet-core/src/input.rs diff --git a/wallet-core/src/ffi.rs b/wallet-core/src/ffi.rs index 28d25eb63..f6e4a2414 100644 --- a/wallet-core/src/ffi.rs +++ b/wallet-core/src/ffi.rs @@ -4,8 +4,7 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. -use crate::keys::{derive_bls_pk, derive_phoenix_pk}; -use crate::RNG_SEED; +use crate::keys::{derive_bls_pk, derive_phoenix_pk, RNG_SEED}; use core::ptr; use dusk_bytes::Serializable; use execution_core::{ diff --git a/wallet-core/src/input.rs b/wallet-core/src/input.rs new file mode 100644 index 000000000..00fb51ed1 --- /dev/null +++ b/wallet-core/src/input.rs @@ -0,0 +1,128 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Helper functions for working with notes. + +use alloc::vec::Vec; + +use super::{alloc, Note}; + +use execution_core::BlsScalar; + +/// The maximum amount of input notes that can be spend in one +/// phoenix-transaction +pub const MAX_INPUT_NOTES: usize = 4; + +/// Pick the notes to be used in a transaction from a vector of notes. +/// +/// The resulting array is only 4 notes long, the argument of this function can +/// be arbitary amount of notes. +/// +/// # Errors +/// +/// If the target sum is greater than the sum of the notes then an error is +/// returned. If the notes vector is empty then an error is returned. +/// +/// See `InputNotesError` type for possible errors +/// this function can yield. +#[must_use] +pub fn try_input_notes( + nodes: Vec<(Note, u64, BlsScalar)>, + target_sum: u64, +) -> Vec<(Note, BlsScalar)> { + if nodes.is_empty() { + return Vec::new(); + } + + let mut i = 0; + let mut sum = 0; + while sum < target_sum && i < nodes.len() { + sum = sum.saturating_add(nodes[i].1); + i += 1; + } + + if sum < target_sum { + return Vec::new(); + } + + pick_notes(target_sum, nodes) +} + +/// Pick the notes to be used in a transaction from a vector of notes. +/// +/// The notes are picked in a way to maximize the number of notes used, +/// while minimizing the value employed. To do this we sort the notes in +/// ascending value order, and go through each combination in a +/// lexicographic order until we find the first combination whose sum is +/// larger or equal to the given value. If such a slice is not found, an +/// empty vector is returned. +/// +/// Note: it is presupposed that the input notes contain enough balance to +/// cover the given `value`. +fn pick_notes( + value: u64, + notes_and_values: Vec<(Note, u64, BlsScalar)>, +) -> Vec<(Note, BlsScalar)> { + let mut notes_and_values = notes_and_values; + let len = notes_and_values.len(); + + if len <= MAX_INPUT_NOTES { + return notes_and_values + .into_iter() + .map(|(note, _, b)| (note, b)) + .collect(); + } + + notes_and_values.sort_by(|(_, aval, _), (_, bval, _)| aval.cmp(bval)); + pick_lexicographic(notes_and_values.len(), |indices| { + indices + .iter() + .map(|index| notes_and_values[*index].1) + .sum::() + >= value + }) + .map(|index| notes_and_values[index].clone()) + .map(|(n, _, b)| (n, b)) + .to_vec() +} + +fn pick_lexicographic bool>( + max_len: usize, + is_valid: F, +) -> [usize; MAX_INPUT_NOTES] { + let mut indices = [0; MAX_INPUT_NOTES]; + indices + .iter_mut() + .enumerate() + .for_each(|(i, index)| *index = i); + + loop { + if is_valid(&indices) { + return indices; + } + + let mut i = MAX_INPUT_NOTES - 1; + + while indices[i] == i + max_len - MAX_INPUT_NOTES { + if i > 0 { + i -= 1; + } else { + break; + } + } + + indices[i] += 1; + for j in i + 1..MAX_INPUT_NOTES { + indices[j] = indices[j - 1] + 1; + } + + if indices[MAX_INPUT_NOTES - 1] == max_len { + break; + } + } + + [0; MAX_INPUT_NOTES] +} diff --git a/wallet-core/src/keys.rs b/wallet-core/src/keys.rs index 54d329d60..1ce471495 100644 --- a/wallet-core/src/keys.rs +++ b/wallet-core/src/keys.rs @@ -21,7 +21,11 @@ use execution_core::{ }, }; -use crate::RNG_SEED; +/// The seed bytes buffer which is used at multiple places +pub type Seed = [u8; RNG_SEED]; + +/// Length of the seed of the generated rng. +pub const RNG_SEED: usize = 64; /// Generates a [`BlsSecretKey`] from a seed and index. /// diff --git a/wallet-core/src/lib.rs b/wallet-core/src/lib.rs index 5dcd25a4f..610e6db1d 100644 --- a/wallet-core/src/lib.rs +++ b/wallet-core/src/lib.rs @@ -16,18 +16,18 @@ static ALLOC: dlmalloc::GlobalDlmalloc = dlmalloc::GlobalDlmalloc; extern crate alloc; -pub mod keys; -pub mod transaction; - #[cfg(target_family = "wasm")] mod ffi; -/// Length of the seed of the generated rng. -pub const RNG_SEED: usize = 64; +pub mod input; +pub mod keys; +pub mod transaction; -// The maximum amount of input notes that can be spend in one -// phoenix-transaction -const MAX_INPUT_NOTES: usize = 4; +pub mod prelude { + //! Re-export of the most commonly used types and traits. + pub use crate::keys; + pub use crate::{input::MAX_INPUT_NOTES, keys::RNG_SEED}; +} use alloc::collections::btree_map::BTreeMap; use alloc::vec::Vec; @@ -77,8 +77,9 @@ where values.sort_by(|a, b| b.cmp(a)); - let spendable = values.iter().take(MAX_INPUT_NOTES).sum(); - let value = spendable + values.iter().skip(MAX_INPUT_NOTES).sum::(); + let spendable = values.iter().take(input::MAX_INPUT_NOTES).sum(); + let value = + spendable + values.iter().skip(input::MAX_INPUT_NOTES).sum::(); BalanceInfo { value, spendable } } diff --git a/wallet-core/tests/notes.rs b/wallet-core/tests/notes.rs index 43a714948..c7cff81fa 100644 --- a/wallet-core/tests/notes.rs +++ b/wallet-core/tests/notes.rs @@ -5,21 +5,49 @@ // Copyright (c) DUSK NETWORK. All rights reserved. use ff::Field; -use rand::rngs::StdRng; -use rand::{Rng, SeedableRng}; +use rand::{rngs::StdRng, CryptoRng, RngCore, SeedableRng}; use execution_core::{ transfer::phoenix::{ Note, NoteLeaf, PublicKey as PhoenixPublicKey, SecretKey as PhoenixSecretKey, }, - JubJubScalar, + BlsScalar, JubJubScalar, }; use wallet_core::{ - keys::derive_multiple_phoenix_sk, map_owned, phoenix_balance, BalanceInfo, + input::try_input_notes, keys::derive_multiple_phoenix_sk, map_owned, + phoenix_balance, BalanceInfo, }; +/// Generate a note, useful for testing purposes +pub fn gen_note( + rng: &mut T, + obfuscated_note: bool, + owner_pk: &PhoenixPublicKey, + value: u64, +) -> Note { + let sender_pk = PhoenixPublicKey::from(&PhoenixSecretKey::random(rng)); + + let value_blinder = JubJubScalar::random(&mut *rng); + let sender_blinder = [ + JubJubScalar::random(&mut *rng), + JubJubScalar::random(&mut *rng), + ]; + if obfuscated_note { + Note::obfuscated( + rng, + &sender_pk, + owner_pk, + value, + value_blinder, + sender_blinder, + ) + } else { + Note::transparent(rng, &sender_pk, owner_pk, value, sender_blinder) + } +} + #[test] fn test_map_owned() { // Assuming this set of notes where the number used as suffix is the @@ -45,7 +73,7 @@ fn test_map_owned() { PhoenixPublicKey::from(&PhoenixSecretKey::random(&mut rng)); let value = 42; - let note_leaves = vec![ + let note_leaves: Vec = vec![ gen_note(&mut rng, true, &owner_1_pks[0], value), // owner 1 gen_note(&mut rng, true, &owner_1_pks[1], value), // owner 1 gen_note(&mut rng, true, &owner_2_pks[0], value), // owner 2 @@ -54,6 +82,14 @@ fn test_map_owned() { gen_note(&mut rng, true, &owner_3_pk, value), // owner 3 ]; + let note_leaves: Vec = note_leaves + .into_iter() + .map(|note| NoteLeaf { + note, + block_height: 0, + }) + .collect(); + // notes with idx 0, 1 and 4 are owned by owner_1 let notes_by_1 = map_owned(&owner_1_sks, ¬e_leaves); assert_eq!(notes_by_1.len(), 3); @@ -115,47 +151,73 @@ fn test_balance() { spendable: 34, }; + let notes: Vec = notes + .into_iter() + .map(|note| NoteLeaf { + note, + block_height: 0, + }) + .collect(); + assert_eq!( phoenix_balance(&(&owner_sk).into(), notes.iter()), expected_balance ); } -fn gen_note( - rng: &mut StdRng, - obfuscated_note: bool, - owner_pk: &PhoenixPublicKey, - value: u64, -) -> NoteLeaf { - let sender_pk = PhoenixPublicKey::from(&PhoenixSecretKey::random(rng)); +#[test] +fn knapsack_works() { + use rand::SeedableRng; - let value_blinder = JubJubScalar::random(&mut *rng); - let sender_blinder = [ - JubJubScalar::random(&mut *rng), - JubJubScalar::random(&mut *rng), - ]; - if obfuscated_note { - NoteLeaf { - note: Note::obfuscated( - rng, - &sender_pk, - &owner_pk, - value, - value_blinder, - sender_blinder, - ), - block_height: rng.gen(), - } - } else { - NoteLeaf { - note: Note::transparent( - rng, - &sender_pk, - &owner_pk, - value, - sender_blinder, - ), - block_height: rng.gen(), - } - } + let mut rng = rand_chacha::ChaCha12Rng::seed_from_u64(0xbeef); + + // sanity check + assert_eq!(try_input_notes(vec![], 70), Vec::new(),); + + let sk = PhoenixSecretKey::random(&mut rng); + let pk = PhoenixPublicKey::from(&sk); + + // basic check + let note = gen_note(&mut rng, true, &pk, 100); + let n = note.gen_nullifier(&sk); + let available = vec![(note, 100, n)]; + let inputs_notes: Vec<(Note, BlsScalar)> = available + .clone() + .into_iter() + .map(|(a, _, b)| (a, b)) + .collect(); + let input = try_input_notes(available, 70); + assert_eq!(input, inputs_notes); + + // out of balance basic check + let note = gen_note(&mut rng, true, &pk, 100); + let available = vec![(note, 100, n)]; + assert_eq!(try_input_notes(available, 101), Vec::new()); + + // multiple inputs check + // note: this test is checking a naive, simple order-based output + + let note1 = gen_note(&mut rng, true, &pk, 100); + let note2 = gen_note(&mut rng, true, &pk, 500); + let note3 = gen_note(&mut rng, true, &pk, 300); + + let available = vec![(note1, 100, n), (note2, 500, n), (note3, 300, n)]; + + let result: Vec<(Note, BlsScalar)> = available + .clone() + .into_iter() + .map(|(a, _, b)| (a, b)) + .collect(); + + assert_eq!(try_input_notes(available.clone(), 600), result); + + let note1 = gen_note(&mut rng, true, &pk, 100); + let note2 = gen_note(&mut rng, true, &pk, 500); + let note3 = gen_note(&mut rng, true, &pk, 300); + + let n = note1.gen_nullifier(&sk); + + let available = vec![(note1, 100, n), (note2, 500, n), (note3, 300, n)]; + + assert_eq!(try_input_notes(available, 901), Vec::new()); }