From fe1400d10481d393a8b63145943279ca9e3a0174 Mon Sep 17 00:00:00 2001 From: moana Date: Mon, 9 Sep 2024 20:23:28 +0200 Subject: [PATCH 1/4] wallet-core: Add moonlight-transaction support Co-authored-by: Daksh --- rusk-wallet/src/wallet.rs | 375 ++++++++++++++---- wallet-core/src/lib.rs | 3 +- wallet-core/src/moonlight.rs | 142 +++++++ .../src/{transaction.rs => phoenix.rs} | 17 +- 4 files changed, 440 insertions(+), 97 deletions(-) create mode 100644 wallet-core/src/moonlight.rs rename wallet-core/src/{transaction.rs => phoenix.rs} (96%) diff --git a/rusk-wallet/src/wallet.rs b/rusk-wallet/src/wallet.rs index 7ecdeb876b..caf0ca1709 100644 --- a/rusk-wallet/src/wallet.rs +++ b/rusk-wallet/src/wallet.rs @@ -22,22 +22,19 @@ use std::fs; use std::path::{Path, PathBuf}; use wallet_core::{ + moonlight::{moonlight, moonlight_stake, moonlight_unstake}, + phoenix::{phoenix, phoenix_stake, phoenix_stake_reward, phoenix_unstake}, 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, + derive_bls_pk, derive_bls_sk, derive_phoenix_pk, derive_phoenix_sk, + derive_phoenix_vk, }, BalanceInfo, }; use execution_core::{ signatures::bls::{PublicKey as BlsPublicKey, SecretKey as BlsSecretKey}, - transfer::{ - data::{ContractCall, TransactionData}, - Transaction, - }, + transfer::{data::TransactionData, Transaction}, }; use zeroize::Zeroize; @@ -87,6 +84,11 @@ impl Wallet { } /// Returns phoenix key pair for a given address + /// + /// # Errors + /// + /// - If the Address provided is not a Phoenix address + /// - If the address is not owned pub fn phoenix_keys( &self, addr: &Address, @@ -99,8 +101,8 @@ impl Wallet { let index = addr.index()?; // retrieve keys - let sk = derive_phoenix_sk(self.store.get_seed(), index); - let pk = addr.pk(); + let sk = self.phoenix_secret_key(index); + let pk = addr.pk()?; Ok((*pk, sk)) } @@ -124,7 +126,10 @@ impl Wallet { let bytes = seed.as_bytes().try_into().unwrap(); // Generate the default address - let address = Address::new(0, derive_phoenix_pk(&bytes, 0)); + let address = Address::Phoenix { + index: Some(0), + addr: derive_phoenix_pk(&bytes, 0), + }; // return new wallet instance Ok(Wallet { @@ -160,7 +165,10 @@ impl Wallet { // return early if its legacy if let DatFileVersion::Legacy = file_version { - let address = Address::new(0, derive_phoenix_pk(&seed, 0)); + let address = Address::Phoenix { + index: Some(0), + addr: derive_phoenix_pk(&seed, 0), + }; // return the store return Ok(Self { @@ -173,7 +181,10 @@ impl Wallet { } let addresses: Vec<_> = (0..address_count) - .map(|i| Address::new(i, derive_phoenix_pk(&seed, i))) + .map(|i| Address::Phoenix { + index: Some(i), + addr: derive_phoenix_pk(&seed, i), + }) .collect(); // create and return @@ -315,7 +326,7 @@ impl Wallet { let index = addr.index()?; let vk = derive_phoenix_vk(seed, index); - let pk = addr.pk(); + let pk = addr.pk()?; let live_notes = self.state()?.fetch_notes(pk)?; let spent_notes = self.state()?.cache().spent_notes(pk)?; @@ -345,7 +356,7 @@ impl Wallet { } /// Obtain balance information for a given address - pub async fn get_balance( + pub async fn get_phoenix_balance( &self, addr: &Address, ) -> Result { @@ -356,30 +367,35 @@ impl Wallet { } 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 notes = state.fetch_notes(addr.pk()?)?; let seed = self.store.get_seed(); Ok(phoenix_balance( &derive_phoenix_vk(seed, index), - notes.into_iter(), + notes.iter(), )) } + /// Get moonlight account balance + pub fn get_moonlight_balance(&self, addr: &Address) -> Result { + let pk = addr.apk()?; + let state = self.state()?; + let account = state.fetch_account(pk)?; + + Ok(Dusk::from(account.balance)) + } + /// 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 pk = derive_phoenix_pk(seed, len as u8); - let addr = Address::new(len as u8, pk); + let addr = Address::Phoenix { + index: Some(len as u8), + addr: pk, + }; self.addresses.push(addr); self.addresses.last().unwrap() @@ -395,14 +411,90 @@ impl Wallet { &self.addresses } - /// Executes a generic contract call - pub async fn execute( + /// Returns the phoenix secret-key for a given index + pub(crate) fn phoenix_secret_key(&self, index: u8) -> PhoenixSecretKey { + let seed = self.store.get_seed(); + derive_phoenix_sk(seed, index) + } + + /// Returns the phoenix public-key for a given index + pub fn phoenix_public_key(&self, index: u8) -> PhoenixPublicKey { + let seed = self.store.get_seed(); + derive_phoenix_pk(seed, index) + } + + /// Returns the bls secret-key for a given index + pub(crate) fn bls_secret_key(&self, index: u8) -> BlsSecretKey { + let seed = self.store.get_seed(); + derive_bls_sk(seed, index) + } + + /// Returns the bls public-key for a given index + pub fn bls_public_key(&self, index: u8) -> BlsPublicKey { + let seed = self.store.get_seed(); + derive_bls_pk(seed, index) + } + + /// Creates a generic moonlight transaction. + #[allow(clippy::too_many_arguments)] + pub fn moonlight_transaction( + &self, + from_addr: &Address, + to_account: Option, + transfer_value: Dusk, + deposit: Dusk, + gas: Gas, + exec: Option>, + ) -> Result { + // make sure we own the sender address + if !from_addr.is_owned() { + return Err(Error::Unauthorized); + } + + // check gas limits + if !gas.is_enough() { + return Err(Error::NotEnoughGas); + } + + let state = self.state()?; + let deposit = *deposit; + + let from_index = from_addr.index()?; + let mut from_sk = self.bls_secret_key(from_index); + let from_account = self.bls_public_key(from_index); + + let account = state.fetch_account(&from_account)?; + + // technically this check is not necessary, but it's nice to not spam + // the network with transactions that are unspendable. + let nonce = account.nonce + 1; + + let chain_id = state.fetch_chain_id()?; + + let tx = moonlight( + &from_sk, + to_account, + *transfer_value, + deposit, + gas.limit, + gas.price, + nonce, + chain_id, + exec, + )?; + + from_sk.zeroize(); + + state.prove_and_propagate(tx) + } + + /// Executes a generic contract call, paying gas with phoenix notes + pub async fn phoenix_execute( &self, sender: &Address, - reciever: &Address, deposit: Dusk, - data: Option>, gas: Gas, + data: TransactionData, ) -> Result { // make sure we own the sender address if !sender.is_owned() { @@ -414,14 +506,14 @@ 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()?; - let mut sender_sk = derive_phoenix_sk(seed, sender_index); + let mut sender_sk = self.phoenix_secret_key(sender_index); + // in a contract execution, the sender and receiver are the same + let receiver_pk = sender.pk()?; let inputs = state .inputs(sender_index, deposit + gas.limit * gas.price)? @@ -435,8 +527,8 @@ impl Wallet { let tx = phoenix( &mut rng, &sender_sk, - sender.pk(), - reciever.pk(), + sender.pk()?, + receiver_pk, inputs, root, 0, @@ -449,15 +541,13 @@ impl Wallet { &Prover, )?; - let tx = state.prove_and_propagate(tx); - sender_sk.zeroize(); - tx + state.prove_and_propagate(tx) } - /// Transfers funds between addresses - pub async fn transfer( + /// Transfers funds between phoenix-addresses + pub async fn phoenix_transfer( &self, sender: &Address, rcvr: &Address, @@ -483,11 +573,9 @@ impl Wallet { let sender_index = sender.index()?; let amt = *amt; - let seed = self.store.get_seed(); - - let mut sender_sk = derive_phoenix_sk(seed, sender_index); - let change_pk = sender.pk(); - let reciever_pk = rcvr.pk(); + let mut sender_sk = self.phoenix_secret_key(sender_index); + let change_pk = sender.pk()?; + let reciever_pk = rcvr.pk()?; let inputs = state .inputs(sender_index, amt + gas.limit * gas.price)? @@ -515,15 +603,59 @@ impl Wallet { &Prover, )?; - let tx = state.prove_and_propagate(tx); - sender_sk.zeroize(); - tx + state.prove_and_propagate(tx) } - /// Stakes Dusk - pub async fn stake( + /// Transfer through moonlight + pub async fn moonlight_transfer( + &self, + sender: &Address, + rcvr: &Address, + amt: Dusk, + gas: Gas, + ) -> Result { + // make sure we own the sender address + if !sender.is_owned() { + return Err(Error::Unauthorized); + } + // make sure amount is positive + if amt == 0 { + return Err(Error::AmountIsZero); + } + // check gas limits + if !gas.is_enough() { + return Err(Error::NotEnoughGas); + } + + let mut from_sk = self.bls_secret_key(sender.index()?); + let apk = rcvr.apk()?; + let amt = *amt; + + let state = self.state()?; + let account = state.fetch_account(apk)?; + let chain_id = state.fetch_chain_id()?; + + let tx = moonlight( + &from_sk, + Some(*apk), + amt, + 0, + gas.limit, + gas.price, + account.nonce, + chain_id, + None::, + )?; + + from_sk.zeroize(); + + state.prove_and_propagate(tx) + } + + /// Stakes Dusk using phoenix notes + pub async fn phoenix_stake( &self, addr: &Address, amt: Dusk, @@ -543,13 +675,12 @@ impl Wallet { } 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); + let mut sender_sk = self.phoenix_secret_key(sender_index); + let mut stake_sk = self.bls_secret_key(sender_index); let nonce = state .fetch_stake(&AccountPublicKey::from(&stake_sk))? @@ -570,30 +701,68 @@ impl Wallet { gas.price, chain_id, amt, nonce, &Prover, )?; - let tx = state.prove_and_propagate(stake); - sender_sk.zeroize(); stake_sk.zeroize(); - tx + state.prove_and_propagate(stake) + } + + /// Stake via moonlight + pub fn moonlight_stake( + &self, + addr: &Address, + amt: Dusk, + gas: Gas, + ) -> Result { + // make sure we own the staking address + if !addr.is_owned() { + return Err(Error::Unauthorized); + } + // make sure amount is positive + if amt == 0 { + return Err(Error::AmountIsZero); + } + // check if the gas is enough + if !gas.is_enough() { + return Err(Error::NotEnoughGas); + } + + let state = self.state()?; + + let amt = *amt; + let sender_index = addr.index()?; + let mut stake_sk = self.bls_secret_key(sender_index); + let pk = AccountPublicKey::from(&stake_sk); + let account = state.fetch_account(&pk)?; + let chain_id = state.fetch_chain_id()?; + + let nonce = state.fetch_stake(&pk)?.map(|s| s.nonce).unwrap_or(0); + + let stake = moonlight_stake( + &stake_sk, + amt, + chain_id, + nonce, + account.nonce, + gas.limit, + gas.price, + )?; + + stake_sk.zeroize(); + + state.prove_and_propagate(stake) } /// Obtains stake information for a given address pub async fn stake_info( &self, - addr: &Address, + addr_idx: u8, ) -> Result, Error> { - let seed = self.store.get_seed(); - - self.state()? - .fetch_stake(&AccountPublicKey::from(&derive_bls_sk( - seed, - addr.index()?, - ))) + self.state()?.fetch_stake(&self.bls_public_key(addr_idx)) } - /// Unstakes Dusk - pub async fn unstake( + /// Unstakes Dusk into phoenix notes + pub async fn phoenix_unstake( &self, addr: &Address, gas: Gas, @@ -605,12 +774,11 @@ impl Wallet { let mut rng = StdRng::from_entropy(); 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 mut sender_sk = self.phoenix_secret_key(index); + let mut stake_sk = self.bls_secret_key(index); let unstake_value = state .fetch_stake(&AccountPublicKey::from(&stake_sk))? @@ -636,21 +804,18 @@ impl Wallet { &Prover, )?; - let tx = state.prove_and_propagate(unstake); - sender_sk.zeroize(); stake_sk.zeroize(); - tx + state.prove_and_propagate(unstake) } - /// Withdraw accumulated staking reward for a given address - pub async fn withdraw_reward( + /// Unstakes Dusk through moonlight + pub async fn moonlight_unstake( &self, addr: &Address, gas: Gas, ) -> Result { - let state = self.state()?; // make sure we own the staking address if !addr.is_owned() { return Err(Error::Unauthorized); @@ -658,12 +823,54 @@ impl Wallet { let mut rng = StdRng::from_entropy(); let index = addr.index()?; - let seed = self.store.get_seed(); + let state = self.state()?; + let mut stake_sk = self.bls_secret_key(index); - let mut sender_sk = derive_phoenix_sk(seed, index); - let mut stake_sk = derive_bls_sk(seed, index); + let pk = AccountPublicKey::from(&stake_sk); - let inputs = state.inputs(index, gas.limit * gas.price)?; + let chain_id = state.fetch_chain_id()?; + let account = state.fetch_account(&pk)?; + + let unstake_value = state + .fetch_stake(&pk)? + .and_then(|s| s.amount) + .map(|s| s.value) + .unwrap_or(0); + + let unstake = moonlight_unstake( + &mut rng, + &stake_sk, + unstake_value, + chain_id, + account.nonce + 1, + gas.price, + gas.limit, + )?; + + stake_sk.zeroize(); + + state.prove_and_propagate(unstake) + } + + /// Withdraw accumulated staking reward for a given address + pub async fn phoenix_stake_withdraw( + &self, + sender_addr: &Address, + gas: Gas, + ) -> Result { + let state = self.state()?; + // make sure we own the staking address + if !sender_addr.is_owned() { + return Err(Error::Unauthorized); + } + + let mut rng = StdRng::from_entropy(); + let sender_index = sender_addr.index()?; + + let mut sender_sk = self.phoenix_secret_key(sender_index); + let mut stake_sk = self.bls_secret_key(sender_index); + + let inputs = state.inputs(sender_index, gas.limit * gas.price)?; let root = state.fetch_root()?; let chain_id = state.fetch_chain_id()?; @@ -686,12 +893,10 @@ impl Wallet { &Prover, )?; - let tx = state.prove_and_propagate(withdraw); - sender_sk.zeroize(); stake_sk.zeroize(); - tx + state.prove_and_propagate(withdraw) } /// Returns bls key pair for provisioner nodes @@ -705,14 +910,14 @@ impl Wallet { } let index = addr.index()?; - let sk = derive_bls_sk(self.store.get_seed(), index); - let pk = BlsPublicKey::from(&sk); + let sk = self.bls_secret_key(index); + let pk = self.bls_public_key(index); Ok((pk, sk)) } /// Export bls key pair for provisioners in node-compatible format - pub fn export_keys( + pub fn export_provisioner_keys( &self, addr: &Address, dir: &Path, @@ -756,7 +961,7 @@ impl Wallet { pub fn claim_as_address(&self, addr: Address) -> Result<&Address, Error> { self.addresses() .iter() - .find(|a| a.pk == addr.pk) + .find(|&a| a == &addr) .ok_or(Error::AddressNotOwned) } diff --git a/wallet-core/src/lib.rs b/wallet-core/src/lib.rs index cf2bb291e7..953a936930 100644 --- a/wallet-core/src/lib.rs +++ b/wallet-core/src/lib.rs @@ -23,8 +23,9 @@ extern crate alloc; mod ffi; pub mod keys; +pub mod moonlight; pub mod notes; -pub mod transaction; +pub mod phoenix; /// The seed used to generate the entropy for the keys pub type Seed = [u8; 64]; diff --git a/wallet-core/src/moonlight.rs b/wallet-core/src/moonlight.rs new file mode 100644 index 0000000000..f7e2e887b3 --- /dev/null +++ b/wallet-core/src/moonlight.rs @@ -0,0 +1,142 @@ +// 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. + +//! Implementations of basic wallet functionalities to create moonlight +//! transactions. + +#![allow(clippy::module_name_repetitions)] + +use execution_core::{ + signatures::bls::{PublicKey as BlsPublicKey, SecretKey as BlsSecretKey}, + stake::{Stake, Withdraw as StakeWithdraw, STAKE_CONTRACT}, + transfer::{ + data::{ContractCall, TransactionData}, + moonlight::Transaction as MoonlightTransaction, + withdraw::{Withdraw, WithdrawReceiver, WithdrawReplayToken}, + Transaction, + }, + Error, +}; + +use rand::{CryptoRng, RngCore}; + +/// Generate a moonlight transaction +/// +/// # Errors +/// - the transaction-data is incorrect +#[allow(clippy::too_many_arguments)] +pub fn moonlight( + from_sk: &BlsSecretKey, + to_account: Option, + value: u64, + deposit: u64, + gas_limit: u64, + gas_price: u64, + nonce: u64, + chain_id: u8, + data: Option>, +) -> Result { + Ok(MoonlightTransaction::new( + from_sk, + to_account, + value, + deposit, + gas_limit, + gas_price, + nonce + 1, + chain_id, + data, + )? + .into()) +} + +/// Stake through moonlight, the `stake_nonce` is the nonce of the stake +/// which is obtained via stake info query on the chain +/// +/// The `nonce` is the nonce of the moonlight transaction +/// +/// # Errors +/// +/// This function most likey not fail but if the `nonce` is incorrect +/// or the `stake_nonce` the node will error and not accept the transcation +pub fn moonlight_stake( + from_sk: &BlsSecretKey, + stake_value: u64, + chain_id: u8, + stake_nonce: u64, + nonce: u64, + gas_limit: u64, + gas_price: u64, +) -> Result { + let receiver_pk = BlsPublicKey::from(from_sk); + + let transfer_value = 0; + let deposit = stake_value; + + let stake = Stake::new(from_sk, stake_value, stake_nonce + 1, chain_id); + + let contract_call = ContractCall::new(STAKE_CONTRACT, "stake", &stake)?; + + Ok(MoonlightTransaction::new( + from_sk, + Some(receiver_pk), + transfer_value, + deposit, + gas_limit, + gas_price, + nonce + 1, + chain_id, + Some(contract_call), + )? + .into()) +} + +/// Unstake through moonlight +/// +/// # Errors +/// +/// This function most likey not fail but if the `nonce` is incorrect +/// or the `stake_nonce` the node will error and not accept the transcation +pub fn moonlight_unstake( + rng: &mut R, + from_sk: &BlsSecretKey, + unstake_value: u64, + chain_id: u8, + nonce: u64, + gas_limit: u64, + gas_price: u64, +) -> Result { + let receiver_pk = BlsPublicKey::from(from_sk); + + let transfer_value = 0; + let deposit = unstake_value; + + let withdraw = Withdraw::new( + rng, + from_sk, + STAKE_CONTRACT, + unstake_value, + WithdrawReceiver::Moonlight(receiver_pk), + WithdrawReplayToken::Moonlight(nonce), + ); + + let unstake = StakeWithdraw::new(from_sk, withdraw); + + let contract_call = ContractCall::new(STAKE_CONTRACT, "unstake", &unstake)?; + + Ok(MoonlightTransaction::new( + from_sk, + Some(receiver_pk), + transfer_value, + deposit, + gas_limit, + gas_price, + nonce + 1, + chain_id, + Some(contract_call), + )? + .into()) +} diff --git a/wallet-core/src/transaction.rs b/wallet-core/src/phoenix.rs similarity index 96% rename from wallet-core/src/transaction.rs rename to wallet-core/src/phoenix.rs index 51f9a313c0..8c5d437f09 100644 --- a/wallet-core/src/transaction.rs +++ b/wallet-core/src/phoenix.rs @@ -4,7 +4,10 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. -//! Implementations of basic wallet functionalities to create transactions. +//! Implementations of basic wallet functionalities to create phoenix +//! transactions. + +#![allow(clippy::module_name_repetitions)] use alloc::vec::Vec; @@ -29,16 +32,7 @@ use execution_core::{ BlsScalar, ContractId, Error, JubJubScalar, }; -/// An unproven-transaction is nearly identical to a [`PhoenixTransaction`] with -/// the only difference being that it carries a serialized [`TxCircuitVec`] -/// instead of the proof bytes. -/// This way it is possible to delegate the proof generation of the -/// [`TxCircuitVec`] after the unproven transaction was created while at the -/// same time ensuring non-malleability of the transaction, as the transaction's -/// payload-hash is part of the public inputs of the circuit. -/// Once the proof is generated from the [`TxCircuitVec`] bytes, it can -/// replace the serialized circuit in the transaction by calling -/// [`Transaction::replace_proof`]. +/// Generate a phoenix-transaction with a given prover. /// /// # Errors /// The creation of a transaction is not possible and will error if: @@ -48,6 +42,7 @@ use execution_core::{ /// - the `inputs` vector contains duplicate `Note`s /// - the `Prove` trait is implemented incorrectly /// - the Memo provided with `data` is too large +/// - the transaction-data is incorrect #[allow(clippy::too_many_arguments)] pub fn phoenix( rng: &mut R, From 626a482bca4417ae75b0bb8f31f1c0144fe46320 Mon Sep 17 00:00:00 2001 From: moana Date: Mon, 9 Sep 2024 20:23:57 +0200 Subject: [PATCH 2/4] rusk-wallet: Acclimate to moonlight support in wallet-core Extend CLI interface to display moonlight balance Co-authored-by: Daksh --- rusk-wallet/src/bin/command.rs | 143 +++++++++++++++++++-------- rusk-wallet/src/bin/interactive.rs | 112 +++++++++++++++------- rusk-wallet/src/bin/main.rs | 7 +- rusk-wallet/src/cache.rs | 6 ++ rusk-wallet/src/clients.rs | 34 +++++-- rusk-wallet/src/clients/sync.rs | 26 ++--- rusk-wallet/src/error.rs | 6 ++ rusk-wallet/src/lib.rs | 2 +- rusk-wallet/src/rusk.rs | 1 + rusk-wallet/src/wallet/address.rs | 149 +++++++++++++++++++++-------- 10 files changed, 348 insertions(+), 138 deletions(-) diff --git a/rusk-wallet/src/bin/command.rs b/rusk-wallet/src/bin/command.rs index e75126097b..1c8e39d2d2 100644 --- a/rusk-wallet/src/bin/command.rs +++ b/rusk-wallet/src/bin/command.rs @@ -48,8 +48,8 @@ pub(crate) enum Command { file: Option, }, - /// Check your current balance - Balance { + /// Check your current phoenix balance + PhoenixBalance { /// Address #[clap(short, long)] addr: Option
, @@ -59,6 +59,13 @@ pub(crate) enum Command { spendable: bool, }, + /// Check your current moonlight balance + MoonlightBalance { + /// Address + #[clap(short, long)] + addr: Option
, + }, + /// List your existing addresses and generate new ones Addresses { /// Create new address @@ -67,19 +74,19 @@ pub(crate) enum Command { }, /// Show address transaction history - History { + PhoenixHistory { /// Address for which you want to see the history #[clap(short, long)] addr: Option
, }, - /// Send DUSK through the network - Transfer { - /// Address from which to send DUSK [default: first address] + /// Send DUSK privately through the network using Phoenix + PhoenixTransfer { + /// Phoenix Address from which to send DUSK [default: first address] #[clap(short, long)] sndr: Option
, - /// Receiver address + /// Phoenix Receiver address #[clap(short, long)] rcvr: Address, @@ -96,9 +103,32 @@ pub(crate) enum Command { gas_price: Lux, }, - /// Start staking DUSK - Stake { - /// Address from which to stake DUSK [default: first address] + /// Send DUSK through the network using moonlight + MoonlightTransfer { + /// Bls Address from which to send DUSK [default: first address] + #[clap(short, long)] + sndr: Option
, + + /// Bls Receiver address + #[clap(short, long)] + rcvr: Address, + + /// Amount of DUSK to send + #[clap(short, long)] + amt: Dusk, + + /// Max amt of gas for this transaction + #[clap(short = 'l', long, default_value_t= DEFAULT_LIMIT)] + gas_limit: u64, + + /// Price you're going to pay for each gas unit (in LUX) + #[clap(short = 'p', long, default_value_t= DEFAULT_PRICE)] + gas_price: Lux, + }, + + /// Start staking DUSK through phoenix + PhoenixStake { + /// Phoenix Address from which to stake DUSK [default: first address] #[clap(short = 's', long)] addr: Option
, @@ -126,9 +156,10 @@ pub(crate) enum Command { reward: bool, }, - /// Unstake a key's stake - Unstake { - /// Address from which your DUSK was staked [default: first address] + /// Phoeinx Unstake a key's stake + PhoenixUnstake { + /// Phoenix Address from which your DUSK was staked [default: first + /// address] #[clap(short, long)] addr: Option
, @@ -141,9 +172,10 @@ pub(crate) enum Command { gas_price: Lux, }, - /// Withdraw accumulated reward for a stake key - Withdraw { - /// Address from which your DUSK was staked [default: first address] + /// Phoenix Withdraw accumulated reward for a stake key + PhoenixWithdraw { + /// Phoenix Address from which your DUSK was staked [default: first + /// address] #[clap(short, long)] addr: Option
, @@ -184,7 +216,7 @@ impl Command { settings: &Settings, ) -> anyhow::Result { match self { - Command::Balance { addr, spendable } => { + Command::PhoenixBalance { addr, spendable } => { let sync_result = wallet.sync().await; if let Err(e) = sync_result { // Sync error should be reported only if wallet is online @@ -198,8 +230,18 @@ impl Command { None => wallet.default_address(), }; - let balance = wallet.get_balance(addr).await?; - Ok(RunResult::Balance(balance, spendable)) + let balance = wallet.get_phoenix_balance(addr).await?; + Ok(RunResult::PhoenixBalance(balance, spendable)) + } + Command::MoonlightBalance { addr } => { + let addr = match addr { + Some(addr) => wallet.claim_as_address(addr)?, + None => wallet.default_address(), + }; + + Ok(RunResult::MoonlightBalance( + wallet.get_moonlight_balance(addr)?, + )) } Command::Addresses { new } => { if new { @@ -217,7 +259,7 @@ impl Command { Ok(RunResult::Addresses(wallet.addresses().clone())) } } - Command::Transfer { + Command::PhoenixTransfer { sndr, rcvr, amt, @@ -231,23 +273,42 @@ impl Command { }; let gas = Gas::new(gas_limit).with_price(gas_price); - let tx = wallet.transfer(sender, &rcvr, amt, gas).await?; + let tx = + wallet.phoenix_transfer(sender, &rcvr, amt, gas).await?; Ok(RunResult::Tx(tx.hash())) } - Command::Stake { + Command::MoonlightTransfer { + sndr, + rcvr, + amt, + gas_limit, + gas_price, + } => { + let gas = Gas::new(gas_limit).with_price(gas_price); + let sender = match sndr { + Some(addr) => wallet.claim_as_address(addr)?, + None => wallet.default_address(), + }; + + let tx = + wallet.moonlight_transfer(sender, &rcvr, amt, gas).await?; + + Ok(RunResult::Tx(tx.hash())) + } + Command::PhoenixStake { addr, amt, gas_limit, gas_price, } => { wallet.sync().await?; + let gas = Gas::new(gas_limit).with_price(gas_price); let addr = match addr { Some(addr) => wallet.claim_as_address(addr)?, None => wallet.default_address(), }; - let gas = Gas::new(gas_limit).with_price(gas_price); - let tx = wallet.stake(addr, amt, gas).await?; + let tx = wallet.phoenix_stake(addr, amt, gas).await?; Ok(RunResult::Tx(tx.hash())) } Command::StakeInfo { addr, reward } => { @@ -255,12 +316,14 @@ impl Command { Some(addr) => wallet.claim_as_address(addr)?, None => wallet.default_address(), }; - let si = - wallet.stake_info(addr).await?.ok_or(Error::NotStaked)?; + let si = wallet + .stake_info(addr.index()?) + .await? + .ok_or(Error::NotStaked)?; Ok(RunResult::StakeInfo(si, reward)) } - Command::Unstake { + Command::PhoenixUnstake { addr, gas_limit, gas_price, @@ -273,10 +336,10 @@ impl Command { let gas = Gas::new(gas_limit).with_price(gas_price); - let tx = wallet.unstake(addr, gas).await?; + let tx = wallet.phoenix_unstake(addr, gas).await?; Ok(RunResult::Tx(tx.hash())) } - Command::Withdraw { + Command::PhoenixWithdraw { addr, gas_limit, gas_price, @@ -289,7 +352,7 @@ impl Command { let gas = Gas::new(gas_limit).with_price(gas_price); - let tx = wallet.withdraw_reward(addr, gas).await?; + let tx = wallet.phoenix_stake_withdraw(addr, gas).await?; Ok(RunResult::Tx(tx.hash())) } Command::Export { addr, dir, name } => { @@ -305,11 +368,11 @@ impl Command { )?; let (pub_key, key_pair) = - wallet.export_keys(addr, &dir, name, &pwd)?; + wallet.export_provisioner_keys(addr, &dir, name, &pwd)?; Ok(RunResult::ExportedKeys(pub_key, key_pair)) } - Command::History { addr } => { + Command::PhoenixHistory { addr } => { wallet.sync().await?; let addr = match addr { Some(addr) => wallet.claim_as_address(addr)?, @@ -320,7 +383,7 @@ impl Command { let transactions = history::transaction_from_notes(settings, notes).await?; - Ok(RunResult::History(transactions)) + Ok(RunResult::PhoenixHistory(transactions)) } Command::Create { .. } => Ok(RunResult::Create()), Command::Restore { .. } => Ok(RunResult::Restore()), @@ -332,7 +395,8 @@ impl Command { /// Possible results of running a command in interactive mode pub enum RunResult { Tx(BlsScalar), - Balance(BalanceInfo, bool), + PhoenixBalance(BalanceInfo, bool), + MoonlightBalance(Dusk), StakeInfo(StakeData, bool), Address(Box
), Addresses(Vec
), @@ -340,21 +404,24 @@ pub enum RunResult { Create(), Restore(), Settings(), - History(Vec), + PhoenixHistory(Vec), } impl fmt::Display for RunResult { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use RunResult::*; match self { - Balance(balance, _) => { + PhoenixBalance(balance, _) => { write!( f, - "> Total balance is: {} DUSK\n> Maximum spendable per TX is: {} DUSK", + "> Total Phoenix balance is: {} DUSK\n> Maximum spendable per TX is: {} DUSK", Dusk::from(balance.value), Dusk::from(balance.spendable) ) } + MoonlightBalance(balance) => { + write!(f, "> Total Moonlight balance is: {} DUSK", balance) + } Address(addr) => { write!(f, "> {}", addr) } @@ -395,7 +462,7 @@ impl fmt::Display for RunResult { kp.display() ) } - History(transactions) => { + PhoenixHistory(transactions) => { writeln!(f, "{}", TransactionHistory::header())?; for th in transactions { writeln!(f, "{th}")?; diff --git a/rusk-wallet/src/bin/interactive.rs b/rusk-wallet/src/bin/interactive.rs index 07f35cea34..a6befe3ab0 100644 --- a/rusk-wallet/src/bin/interactive.rs +++ b/rusk-wallet/src/bin/interactive.rs @@ -66,17 +66,30 @@ pub(crate) async fn run_loop( AddrSelect::Exit => std::process::exit(0), }; + let index = addr.index()?; + + let moonlight = Address::Bls { + index: Some(index), + addr: wallet.bls_public_key(addr.index()?), + }; + loop { // get balance for this address prompt::hide_cursor()?; - let balance = wallet.get_balance(&addr).await?; - let spendable = balance.spendable.into(); - let total: Dusk = balance.value.into(); + let phoenix_bal = wallet.get_phoenix_balance(&addr).await?; + let moonlight_bal = wallet.get_moonlight_balance(&moonlight)?; + let spendable = phoenix_bal.spendable.into(); + let total: Dusk = phoenix_bal.value.into(); + prompt::hide_cursor()?; // display address information println!(); - println!("Address: {addr}"); + println!("Moonlight Address: {moonlight}"); + println!("Balance:"); + println!(" - Total: {moonlight_bal}"); + println!(); + println!("Phoenix Address: {addr}"); println!("Balance:"); println!(" - Spendable: {spendable}"); println!(" - Total: {total}"); @@ -193,12 +206,13 @@ enum AddrOp { #[derive(PartialEq, Eq, Hash, Clone, Debug)] enum CommandMenuItem { - History, - Transfer, - Stake, + PhoenixHistory, + PhoenixTransfer, + MoonlightTransfer, + PhoenixStake, StakeInfo, - Unstake, - Withdraw, + PhoenixUnstake, + PhoenixWithdraw, Export, Back, } @@ -213,12 +227,13 @@ fn menu_op( use CommandMenuItem as CMI; let cmd_menu = Menu::new() - .add(CMI::History, "Transaction History") - .add(CMI::Transfer, "Transfer Dusk") - .add(CMI::Stake, "Stake Dusk") + .add(CMI::PhoenixHistory, "Phoenix Transaction History") + .add(CMI::PhoenixTransfer, "Phoenix Transfer Dusk") + .add(CMI::MoonlightTransfer, "Moonlight Transfer Dusk") + .add(CMI::PhoenixStake, "Phoenix Stake Dusk") .add(CMI::StakeInfo, "Check existing stake") - .add(CMI::Unstake, "Unstake Dusk") - .add(CMI::Withdraw, "Withdraw staking reward") + .add(CMI::PhoenixUnstake, "Phoenix Unstake Dusk") + .add(CMI::PhoenixWithdraw, "Phoenix Withdraw staking reward") .add(CMI::Export, "Export provisioner key-pair") .separator() .add(CMI::Back, "Back"); @@ -232,17 +247,28 @@ fn menu_op( let cmd = cmd_menu.answer(&answer).to_owned(); let res = match cmd { - CMI::History => { - AddrOp::Run(Box::new(Command::History { addr: Some(addr) })) + CMI::PhoenixHistory => { + AddrOp::Run(Box::new(Command::PhoenixHistory { addr: Some(addr) })) } - CMI::Transfer => AddrOp::Run(Box::new(Command::Transfer { - sndr: Some(addr), - rcvr: prompt::request_rcvr_addr("recipient")?, - amt: prompt::request_token_amt("transfer", balance)?, - gas_limit: prompt::request_gas_limit(gas::DEFAULT_LIMIT)?, - gas_price: prompt::request_gas_price()?, - })), - CMI::Stake => AddrOp::Run(Box::new(Command::Stake { + CMI::PhoenixTransfer => { + AddrOp::Run(Box::new(Command::PhoenixTransfer { + sndr: Some(addr), + rcvr: prompt::request_rcvr_addr("recipient")?, + amt: prompt::request_token_amt("transfer", balance)?, + gas_limit: prompt::request_gas_limit(gas::DEFAULT_LIMIT)?, + gas_price: prompt::request_gas_price()?, + })) + } + CMI::MoonlightTransfer => { + AddrOp::Run(Box::new(Command::MoonlightTransfer { + sndr: Some(addr), + rcvr: prompt::request_rcvr_addr("recipient")?, + amt: prompt::request_token_amt("transfer", balance)?, + gas_limit: prompt::request_gas_limit(gas::DEFAULT_LIMIT)?, + gas_price: prompt::request_gas_price()?, + })) + } + CMI::PhoenixStake => AddrOp::Run(Box::new(Command::PhoenixStake { addr: Some(addr), amt: prompt::request_token_amt("stake", balance)?, gas_limit: prompt::request_gas_limit(DEFAULT_STAKE_GAS_LIMIT)?, @@ -252,16 +278,18 @@ fn menu_op( addr: Some(addr), reward: false, })), - CMI::Unstake => AddrOp::Run(Box::new(Command::Unstake { - addr: Some(addr), - gas_limit: prompt::request_gas_limit(DEFAULT_STAKE_GAS_LIMIT)?, - gas_price: prompt::request_gas_price()?, - })), - CMI::Withdraw => AddrOp::Run(Box::new(Command::Withdraw { + CMI::PhoenixUnstake => AddrOp::Run(Box::new(Command::PhoenixUnstake { addr: Some(addr), gas_limit: prompt::request_gas_limit(DEFAULT_STAKE_GAS_LIMIT)?, gas_price: prompt::request_gas_price()?, })), + CMI::PhoenixWithdraw => { + AddrOp::Run(Box::new(Command::PhoenixWithdraw { + addr: Some(addr), + gas_limit: prompt::request_gas_limit(DEFAULT_STAKE_GAS_LIMIT)?, + gas_price: prompt::request_gas_price()?, + })) + } CMI::Export => AddrOp::Run(Box::new(Command::Export { addr: Some(addr), name: None, @@ -429,7 +457,22 @@ fn menu_wallet(wallet_found: Option) -> anyhow::Result { /// Request user confirmation for a transfer transaction fn confirm(cmd: &Command) -> anyhow::Result { match cmd { - Command::Transfer { + Command::PhoenixTransfer { + sndr, + rcvr, + amt, + gas_limit, + gas_price, + } => { + let sndr = sndr.as_ref().expect("sender to be a valid address"); + let max_fee = gas_limit * gas_price; + println!(" > Send from = {}", sndr.preview()); + println!(" > Recipient = {}", rcvr.preview()); + println!(" > Amount to transfer = {} DUSK", amt); + println!(" > Max fee = {} DUSK", Dusk::from(max_fee)); + prompt::ask_confirm() + } + Command::MoonlightTransfer { sndr, rcvr, amt, @@ -442,9 +485,10 @@ fn confirm(cmd: &Command) -> anyhow::Result { println!(" > Recipient = {}", rcvr.preview()); println!(" > Amount to transfer = {} DUSK", amt); println!(" > Max fee = {} DUSK", Dusk::from(max_fee)); + println!(" > ALERT: THIS IS A PUBLIC TRANSACTION"); prompt::ask_confirm() } - Command::Stake { + Command::PhoenixStake { addr, amt, gas_limit, @@ -457,7 +501,7 @@ fn confirm(cmd: &Command) -> anyhow::Result { println!(" > Max fee = {} DUSK", Dusk::from(max_fee)); prompt::ask_confirm() } - Command::Unstake { + Command::PhoenixUnstake { addr, gas_limit, gas_price, @@ -468,7 +512,7 @@ fn confirm(cmd: &Command) -> anyhow::Result { println!(" > Max fee = {} DUSK", Dusk::from(max_fee)); prompt::ask_confirm() } - Command::Withdraw { + Command::PhoenixWithdraw { addr, gas_limit, gas_price, diff --git a/rusk-wallet/src/bin/main.rs b/rusk-wallet/src/bin/main.rs index 050c4d8742..2862bedc21 100644 --- a/rusk-wallet/src/bin/main.rs +++ b/rusk-wallet/src/bin/main.rs @@ -287,13 +287,16 @@ async fn exec() -> anyhow::Result<()> { // run command match cmd { Some(cmd) => match cmd.run(&mut wallet, &settings).await? { - RunResult::Balance(balance, spendable) => { + RunResult::PhoenixBalance(balance, spendable) => { if spendable { println!("{}", Dusk::from(balance.spendable)); } else { println!("{}", Dusk::from(balance.value)); } } + RunResult::MoonlightBalance(balance) => { + println!("Total: {}", balance); + } RunResult::Address(addr) => { println!("{addr}"); } @@ -325,7 +328,7 @@ async fn exec() -> anyhow::Result<()> { RunResult::ExportedKeys(pub_key, key_pair) => { println!("{},{}", pub_key.display(), key_pair.display()) } - RunResult::History(transactions) => { + RunResult::PhoenixHistory(transactions) => { println!("{}", TransactionHistory::header()); for th in transactions { println!("{th}"); diff --git a/rusk-wallet/src/cache.rs b/rusk-wallet/src/cache.rs index 2dd4936d57..8f400386f7 100644 --- a/rusk-wallet/src/cache.rs +++ b/rusk-wallet/src/cache.rs @@ -239,6 +239,12 @@ impl Ord for NoteData { } } +impl AsRef for NoteData { + fn as_ref(&self) -> &Note { + &self.note + } +} + impl Serializable<{ u64::SIZE + Note::SIZE }> for NoteData { type Error = dusk_bytes::Error; /// Converts a Note into a byte representation diff --git a/rusk-wallet/src/clients.rs b/rusk-wallet/src/clients.rs index fb7a6b4dd3..bc307793b9 100644 --- a/rusk-wallet/src/clients.rs +++ b/rusk-wallet/src/clients.rs @@ -8,7 +8,8 @@ mod sync; use dusk_bytes::Serializable; use execution_core::{ - transfer::{phoenix::Prove, Transaction}, + signatures::bls::PublicKey as AccountPublicKey, + transfer::{moonlight::AccountData, phoenix::Prove, Transaction}, Error as ExecutionCoreError, }; @@ -143,15 +144,15 @@ impl State { /// Skips writing the proof for non phoenix transactions pub fn prove_and_propagate( &self, - utx: Transaction, + tx: Transaction, ) -> Result { let status = self.status; let prover = &self.prover; - let mut utx = utx; + let mut tx = tx; - if let Transaction::Phoenix(tx) = &mut utx { + if let Transaction::Phoenix(utx) = &mut tx { let status = self.status; - let proof = tx.proof(); + let proof = utx.proof(); status("Attempt to prove tx..."); @@ -162,12 +163,12 @@ impl State { ExecutionCoreError::PhoenixCircuit(e.to_string()) })?; - tx.set_proof(proof); + utx.set_proof(proof); status("Proving sucesss!"); } - let tx_bytes = utx.to_var_bytes(); + let tx_bytes = tx.to_var_bytes(); status("Attempt to preverify tx..."); let preverify_req = RuskRequest::new("preverify", tx_bytes.clone()); @@ -179,7 +180,7 @@ impl State { let _ = self.client.call(2, "Chain", &propagate_req).wait()?; status("Transaction propagated!"); - Ok(utx) + Ok(tx) } /// Find notes for a view key, starting from the given block height. @@ -219,6 +220,23 @@ impl State { inputs } + pub(crate) fn fetch_account( + &self, + pk: &AccountPublicKey, + ) -> Result { + let status = self.status; + status("Fetching account-data..."); + + let account = self + .client + .contract_query::<_, 1024>(TRANSFER_CONTRACT, "account", pk) + .wait()?; + let account = rkyv::from_bytes(&account).map_err(|_| Error::Rkyv)?; + status("account-data received!"); + + Ok(account) + } + pub(crate) fn fetch_notes( &self, pk: &PhoenixPublicKey, diff --git a/rusk-wallet/src/clients/sync.rs b/rusk-wallet/src/clients/sync.rs index c884c91b63..32eb5a3e65 100644 --- a/rusk-wallet/src/clients/sync.rs +++ b/rusk-wallet/src/clients/sync.rs @@ -25,17 +25,17 @@ pub(crate) async fn sync_db( ) -> Result<(), Error> { 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(); + let keys: 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..."); @@ -87,7 +87,7 @@ pub(crate) async fn sync_db( buffer = leaf_chunk.remainder().to_vec(); } - for (sk, vk, pk) in addresses.iter() { + for (sk, vk, pk) in keys.iter() { for (block_height, note) in note_data.iter() { if vk.owns(note.stealth_address()) { let nullifier = note.gen_nullifier(sk); @@ -108,7 +108,7 @@ pub(crate) async fn sync_db( // Remove spent nullifiers from live notes // zerorize all the secret keys - for (mut sk, _, pk) in addresses { + for (mut sk, _, pk) in keys { let nullifiers: Vec = cache.unspent_notes_id(&pk)?; if !nullifiers.is_empty() { diff --git a/rusk-wallet/src/error.rs b/rusk-wallet/src/error.rs index a5ee8670c7..260d0a69e2 100644 --- a/rusk-wallet/src/error.rs +++ b/rusk-wallet/src/error.rs @@ -119,6 +119,12 @@ pub enum Error { /// Memo provided is too large #[error("Memo too large {0}")] MemoTooLarge(usize), + /// Expected Bls Key + #[error("Expected Bls Public Key")] + ExpectedBlsPublicKey, + /// Expected Phoenix public key + #[error("Expected Phoenix public Key")] + ExpectedPhoenixPublicKey, } impl From for Error { diff --git a/rusk-wallet/src/lib.rs b/rusk-wallet/src/lib.rs index 5d0371c63b..d89b1902c1 100644 --- a/rusk-wallet/src/lib.rs +++ b/rusk-wallet/src/lib.rs @@ -58,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 = 1; +const DEFAULT_MAX_ADDRESSES: usize = 2; 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 b1eb580f5b..2ab9312241 100644 --- a/rusk-wallet/src/rusk.rs +++ b/rusk-wallet/src/rusk.rs @@ -125,4 +125,5 @@ impl RuskHttpClient { Ok(response) } } + } diff --git a/rusk-wallet/src/wallet/address.rs b/rusk-wallet/src/wallet/address.rs index dd3e19f094..ddd3ba3731 100644 --- a/rusk-wallet/src/wallet/address.rs +++ b/rusk-wallet/src/wallet/address.rs @@ -4,109 +4,174 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. -use std::fmt; use std::hash::Hasher; -use std::str::FromStr; +use std::{fmt, str::FromStr}; use super::*; use crate::Error; use dusk_bytes::{DeserializableSlice, Error as BytesError, Serializable}; +/// Address for which to perform transactions with +/// it may be owned by the user or not, if the address is a reciever +/// then the index field will be none #[derive(Clone, Eq)] -/// A public address within the Dusk Network -pub struct Address { - pub(crate) index: Option, - pub(crate) pk: PhoenixPublicKey, +#[allow(missing_docs)] +pub enum Address { + /// A Phoenix address + Phoenix { + index: Option, + addr: PhoenixPublicKey, + }, + /// A BLS address for moonlight account + Bls { + index: Option, + addr: AccountPublicKey, + }, } +/// A public address within the Dusk Network impl Address { - pub(crate) fn new(index: u8, pk: PhoenixPublicKey) -> Self { - Self { - index: Some(index), - pk, + /// Returns true if the current user owns this address + pub fn is_owned(&self) -> bool { + self.index().is_ok() + } + + pub(crate) fn pk(&self) -> Result<&PhoenixPublicKey, Error> { + if let Self::Phoenix { addr, .. } = self { + Ok(addr) + } else { + Err(Error::ExpectedPhoenixPublicKey) } } - /// Returns true if the current user owns this address - pub fn is_owned(&self) -> bool { - self.index.is_some() + pub(crate) fn apk(&self) -> Result<&AccountPublicKey, Error> { + if let Self::Bls { addr, .. } = self { + Ok(addr) + } else { + Err(Error::ExpectedBlsPublicKey) + } } - pub(crate) fn pk(&self) -> &PhoenixPublicKey { - &self.pk + /// find index of the address + pub fn index(&self) -> Result { + match self { + Self::Phoenix { index, .. } => index, + Self::Bls { index, .. } => index, + } + .ok_or(Error::AddressNotOwned) } - pub(crate) fn index(&self) -> Result { - self.index.ok_or(Error::AddressNotOwned) + pub(crate) fn to_bytes(&self) -> Vec { + match self { + Self::Phoenix { addr, .. } => addr.to_bytes().to_vec(), + Self::Bls { addr, .. } => addr.to_bytes().to_vec(), + } } /// A trimmed version of the address to display as preview pub fn preview(&self) -> String { - let addr = bs58::encode(self.pk.to_bytes()).into_string(); + let bytes = self.to_bytes(); + let addr = bs58::encode(bytes).into_string(); format!("{}...{}", &addr[..7], &addr[addr.len() - 7..]) } -} -impl FromStr for Address { - type Err = Error; - - fn from_str(s: &str) -> Result { + /// try to create phoenix address from string + pub fn try_from_str_phoenix(s: &str) -> Result { let bytes = bs58::decode(s).into_vec()?; let pk = PhoenixPublicKey::from_reader(&mut &bytes[..]) .map_err(|_| Error::BadAddress)?; - let addr = Address { index: None, pk }; + let addr = Self::Phoenix { + index: None, + addr: pk, + }; Ok(addr) } -} -impl TryFrom for Address { - type Error = Error; + /// try to create moonlight address from string + pub fn try_from_str_bls(s: &str) -> Result { + let bytes = bs58::decode(s).into_vec()?; - fn try_from(s: String) -> Result { - Address::from_str(s.as_str()) - } -} + let apk = AccountPublicKey::from_reader(&mut &bytes[..]) + .map_err(|_| Error::BadAddress)?; -impl TryFrom<&[u8; PhoenixPublicKey::SIZE]> for Address { - type Error = Error; + let addr = Self::Bls { + index: None, + addr: apk, + }; + + Ok(addr) + } - fn try_from( + /// try to create phoenix public key from bytes + pub fn try_from_bytes_phoenix( bytes: &[u8; PhoenixPublicKey::SIZE], - ) -> Result { - let addr = Address { + ) -> Result { + let addr = Self::Phoenix { + index: None, + addr: PhoenixPublicKey::from_bytes(bytes)?, + }; + + Ok(addr) + } + + /// Create an address instance from `BlsPublicKey` bytes. + pub fn try_from_bytes_bls( + bytes: &[u8; AccountPublicKey::SIZE], + ) -> Result { + let addr = Self::Bls { index: None, - pk: PhoenixPublicKey::from_bytes(bytes)?, + addr: AccountPublicKey::from_bytes(bytes)?, }; + Ok(addr) } } +impl FromStr for Address { + type Err = Error; + + fn from_str(s: &str) -> Result { + let try_phoenix = Self::try_from_str_phoenix(s); + let try_bls = Self::try_from_str_bls(s); + + if let Ok(addr) = try_phoenix { + Ok(addr) + } else { + try_bls + } + } +} + impl PartialEq for Address { fn eq(&self, other: &Self) -> bool { - self.index == other.index && self.pk == other.pk + match (self.index(), other.index()) { + (Ok(x), Ok(y)) => x == y && self.preview() == other.preview(), + (_, _) => self.preview() == other.preview(), + } } } impl std::hash::Hash for Address { fn hash(&self, state: &mut H) { - self.index.hash(state); - self.pk.to_bytes().hash(state); + // we dont care about addresses we dont own + let _ = self.index().map(|f| f.hash(state)); + self.preview().hash(state); } } impl fmt::Display for Address { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", bs58::encode(self.pk.to_bytes()).into_string()) + write!(f, "{}", bs58::encode(self.to_bytes()).into_string()) } } impl fmt::Debug for Address { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", bs58::encode(self.pk.to_bytes()).into_string()) + write!(f, "{}", bs58::encode(self.to_bytes()).into_string()) } } From 4cd78608cb14721db70b10b4a5b72f5240b17e5c Mon Sep 17 00:00:00 2001 From: moana Date: Mon, 9 Sep 2024 20:24:45 +0200 Subject: [PATCH 3/4] test-wallet: Adapt to new wallet-core Co-authored-by: Daksh --- rusk-wallet/src/wallet.rs | 6 +- wallet-core/src/lib.rs | 3 +- wallet-core/src/moonlight.rs | 142 ------------------ .../src/{phoenix.rs => transaction.rs} | 2 - 4 files changed, 5 insertions(+), 148 deletions(-) delete mode 100644 wallet-core/src/moonlight.rs rename wallet-core/src/{phoenix.rs => transaction.rs} (99%) diff --git a/rusk-wallet/src/wallet.rs b/rusk-wallet/src/wallet.rs index caf0ca1709..775866851a 100644 --- a/rusk-wallet/src/wallet.rs +++ b/rusk-wallet/src/wallet.rs @@ -22,13 +22,15 @@ use std::fs; use std::path::{Path, PathBuf}; use wallet_core::{ - moonlight::{moonlight, moonlight_stake, moonlight_unstake}, - phoenix::{phoenix, phoenix_stake, phoenix_stake_reward, phoenix_unstake}, phoenix_balance, prelude::keys::{ derive_bls_pk, derive_bls_sk, derive_phoenix_pk, derive_phoenix_sk, derive_phoenix_vk, }, + transaction::{ + moonlight, moonlight_stake, moonlight_unstake, phoenix, phoenix_stake, + phoenix_stake_reward, phoenix_unstake, + }, BalanceInfo, }; diff --git a/wallet-core/src/lib.rs b/wallet-core/src/lib.rs index 953a936930..cf2bb291e7 100644 --- a/wallet-core/src/lib.rs +++ b/wallet-core/src/lib.rs @@ -23,9 +23,8 @@ extern crate alloc; mod ffi; pub mod keys; -pub mod moonlight; pub mod notes; -pub mod phoenix; +pub mod transaction; /// The seed used to generate the entropy for the keys pub type Seed = [u8; 64]; diff --git a/wallet-core/src/moonlight.rs b/wallet-core/src/moonlight.rs deleted file mode 100644 index f7e2e887b3..0000000000 --- a/wallet-core/src/moonlight.rs +++ /dev/null @@ -1,142 +0,0 @@ -// 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. - -//! Implementations of basic wallet functionalities to create moonlight -//! transactions. - -#![allow(clippy::module_name_repetitions)] - -use execution_core::{ - signatures::bls::{PublicKey as BlsPublicKey, SecretKey as BlsSecretKey}, - stake::{Stake, Withdraw as StakeWithdraw, STAKE_CONTRACT}, - transfer::{ - data::{ContractCall, TransactionData}, - moonlight::Transaction as MoonlightTransaction, - withdraw::{Withdraw, WithdrawReceiver, WithdrawReplayToken}, - Transaction, - }, - Error, -}; - -use rand::{CryptoRng, RngCore}; - -/// Generate a moonlight transaction -/// -/// # Errors -/// - the transaction-data is incorrect -#[allow(clippy::too_many_arguments)] -pub fn moonlight( - from_sk: &BlsSecretKey, - to_account: Option, - value: u64, - deposit: u64, - gas_limit: u64, - gas_price: u64, - nonce: u64, - chain_id: u8, - data: Option>, -) -> Result { - Ok(MoonlightTransaction::new( - from_sk, - to_account, - value, - deposit, - gas_limit, - gas_price, - nonce + 1, - chain_id, - data, - )? - .into()) -} - -/// Stake through moonlight, the `stake_nonce` is the nonce of the stake -/// which is obtained via stake info query on the chain -/// -/// The `nonce` is the nonce of the moonlight transaction -/// -/// # Errors -/// -/// This function most likey not fail but if the `nonce` is incorrect -/// or the `stake_nonce` the node will error and not accept the transcation -pub fn moonlight_stake( - from_sk: &BlsSecretKey, - stake_value: u64, - chain_id: u8, - stake_nonce: u64, - nonce: u64, - gas_limit: u64, - gas_price: u64, -) -> Result { - let receiver_pk = BlsPublicKey::from(from_sk); - - let transfer_value = 0; - let deposit = stake_value; - - let stake = Stake::new(from_sk, stake_value, stake_nonce + 1, chain_id); - - let contract_call = ContractCall::new(STAKE_CONTRACT, "stake", &stake)?; - - Ok(MoonlightTransaction::new( - from_sk, - Some(receiver_pk), - transfer_value, - deposit, - gas_limit, - gas_price, - nonce + 1, - chain_id, - Some(contract_call), - )? - .into()) -} - -/// Unstake through moonlight -/// -/// # Errors -/// -/// This function most likey not fail but if the `nonce` is incorrect -/// or the `stake_nonce` the node will error and not accept the transcation -pub fn moonlight_unstake( - rng: &mut R, - from_sk: &BlsSecretKey, - unstake_value: u64, - chain_id: u8, - nonce: u64, - gas_limit: u64, - gas_price: u64, -) -> Result { - let receiver_pk = BlsPublicKey::from(from_sk); - - let transfer_value = 0; - let deposit = unstake_value; - - let withdraw = Withdraw::new( - rng, - from_sk, - STAKE_CONTRACT, - unstake_value, - WithdrawReceiver::Moonlight(receiver_pk), - WithdrawReplayToken::Moonlight(nonce), - ); - - let unstake = StakeWithdraw::new(from_sk, withdraw); - - let contract_call = ContractCall::new(STAKE_CONTRACT, "unstake", &unstake)?; - - Ok(MoonlightTransaction::new( - from_sk, - Some(receiver_pk), - transfer_value, - deposit, - gas_limit, - gas_price, - nonce + 1, - chain_id, - Some(contract_call), - )? - .into()) -} diff --git a/wallet-core/src/phoenix.rs b/wallet-core/src/transaction.rs similarity index 99% rename from wallet-core/src/phoenix.rs rename to wallet-core/src/transaction.rs index 8c5d437f09..de9378f5a3 100644 --- a/wallet-core/src/phoenix.rs +++ b/wallet-core/src/transaction.rs @@ -7,8 +7,6 @@ //! Implementations of basic wallet functionalities to create phoenix //! transactions. -#![allow(clippy::module_name_repetitions)] - use alloc::vec::Vec; use rand::{CryptoRng, RngCore}; From e11c701f9badd8a9e399686373bff306030e7254 Mon Sep 17 00:00:00 2001 From: Daksh <41485688+Daksh14@users.noreply.github.com> Date: Tue, 10 Sep 2024 12:53:00 -0400 Subject: [PATCH 4/4] test-wallet: Adapt to new wallet-core Co-authored-by: Daksh --- rusk-wallet/src/lib.rs | 5 ++--- rusk-wallet/src/wallet.rs | 17 +++++++++-------- wallet-core/src/transaction.rs | 15 +++++++++++---- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/rusk-wallet/src/lib.rs b/rusk-wallet/src/lib.rs index d89b1902c1..62e43610ef 100644 --- a/rusk-wallet/src/lib.rs +++ b/rusk-wallet/src/lib.rs @@ -40,9 +40,8 @@ use execution_core::{ signatures::bls::PublicKey as AccountPublicKey, stake::StakeData, transfer::phoenix::{ - ArchivedNoteLeaf, Note, NoteLeaf, NoteOpening, - PublicKey as PhoenixPublicKey, SecretKey as PhoenixSecretKey, - ViewKey as PhoenixViewKey, + ArchivedNoteLeaf, Note, NoteOpening, PublicKey as PhoenixPublicKey, + SecretKey as PhoenixSecretKey, ViewKey as PhoenixViewKey, }, BlsScalar, }; diff --git a/rusk-wallet/src/wallet.rs b/rusk-wallet/src/wallet.rs index 775866851a..f41dfa62fe 100644 --- a/rusk-wallet/src/wallet.rs +++ b/rusk-wallet/src/wallet.rs @@ -36,7 +36,7 @@ use wallet_core::{ use execution_core::{ signatures::bls::{PublicKey as BlsPublicKey, SecretKey as BlsSecretKey}, - transfer::{data::TransactionData, Transaction}, + transfer::{data::ContractCall, data::TransactionData, Transaction}, }; use zeroize::Zeroize; @@ -539,7 +539,7 @@ impl Wallet { gas.limit, gas.price, chain_id, - data, + Some(data), &Prover, )?; @@ -730,7 +730,6 @@ impl Wallet { } let state = self.state()?; - let amt = *amt; let sender_index = addr.index()?; let mut stake_sk = self.bls_secret_key(sender_index); @@ -741,13 +740,14 @@ impl Wallet { let nonce = state.fetch_stake(&pk)?.map(|s| s.nonce).unwrap_or(0); let stake = moonlight_stake( + &stake_sk, &stake_sk, amt, - chain_id, - nonce, - account.nonce, gas.limit, gas.price, + account.nonce, + nonce, + chain_id, )?; stake_sk.zeroize(); @@ -842,11 +842,12 @@ impl Wallet { let unstake = moonlight_unstake( &mut rng, &stake_sk, + &stake_sk, unstake_value, - chain_id, - account.nonce + 1, gas.price, gas.limit, + account.nonce + 1, + chain_id, )?; stake_sk.zeroize(); diff --git a/wallet-core/src/transaction.rs b/wallet-core/src/transaction.rs index de9378f5a3..51f9a313c0 100644 --- a/wallet-core/src/transaction.rs +++ b/wallet-core/src/transaction.rs @@ -4,8 +4,7 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. -//! Implementations of basic wallet functionalities to create phoenix -//! transactions. +//! Implementations of basic wallet functionalities to create transactions. use alloc::vec::Vec; @@ -30,7 +29,16 @@ use execution_core::{ BlsScalar, ContractId, Error, JubJubScalar, }; -/// Generate a phoenix-transaction with a given prover. +/// An unproven-transaction is nearly identical to a [`PhoenixTransaction`] with +/// the only difference being that it carries a serialized [`TxCircuitVec`] +/// instead of the proof bytes. +/// This way it is possible to delegate the proof generation of the +/// [`TxCircuitVec`] after the unproven transaction was created while at the +/// same time ensuring non-malleability of the transaction, as the transaction's +/// payload-hash is part of the public inputs of the circuit. +/// Once the proof is generated from the [`TxCircuitVec`] bytes, it can +/// replace the serialized circuit in the transaction by calling +/// [`Transaction::replace_proof`]. /// /// # Errors /// The creation of a transaction is not possible and will error if: @@ -40,7 +48,6 @@ use execution_core::{ /// - the `inputs` vector contains duplicate `Note`s /// - the `Prove` trait is implemented incorrectly /// - the Memo provided with `data` is too large -/// - the transaction-data is incorrect #[allow(clippy::too_many_arguments)] pub fn phoenix( rng: &mut R,