From 6867390248e1ae4397d15bf2a8c671695c68039b Mon Sep 17 00:00:00 2001 From: Herr Seppia Date: Mon, 20 Jan 2025 15:26:23 +0100 Subject: [PATCH 1/3] rusk: embed `test-wallet` crate in test folder --- rusk/Cargo.toml | 3 +- rusk/benches/block_ingestion.rs | 1 + rusk/tests/common/wallet.rs | 4 + rusk/tests/common/wallet/test_wallet.rs | 140 +++ rusk/tests/common/wallet/test_wallet/imp.rs | 1167 +++++++++++++++++++ rusk/tests/rusk-state.rs | 4 +- rusk/tests/services/contract_deployment.rs | 5 +- rusk/tests/services/contract_stake.rs | 5 +- rusk/tests/services/conversion.rs | 5 +- rusk/tests/services/gas_behavior.rs | 5 +- rusk/tests/services/moonlight_stake.rs | 5 +- rusk/tests/services/multi_transfer.rs | 5 +- rusk/tests/services/owner_calls.rs | 5 +- rusk/tests/services/phoenix_stake.rs | 5 +- rusk/tests/services/transfer.rs | 5 +- rusk/tests/services/unspendable.rs | 5 +- rusk/tests/tests.rs | 2 + 17 files changed, 1348 insertions(+), 23 deletions(-) create mode 100644 rusk/tests/common/wallet/test_wallet.rs create mode 100644 rusk/tests/common/wallet/test_wallet/imp.rs diff --git a/rusk/Cargo.toml b/rusk/Cargo.toml index cf9a03d267..8f27a84c1f 100644 --- a/rusk/Cargo.toml +++ b/rusk/Cargo.toml @@ -82,7 +82,8 @@ rusk-recovery = { workspace = true, optional = true } futures = { workspace = true, optional = true } [dev-dependencies] -test-wallet = { version = "0.1.0", path = "../test-wallet" } +wallet-core = { workspace = true } +zeroize = { workspace = true, features = ["derive"] } rusk-recovery = { workspace = true, features = ["state"] } ff = { workspace = true } rusk-prover = { workspace = true, features = ["no_random", "debug"] } diff --git a/rusk/benches/block_ingestion.rs b/rusk/benches/block_ingestion.rs index 97e282bea9..0afabed639 100644 --- a/rusk/benches/block_ingestion.rs +++ b/rusk/benches/block_ingestion.rs @@ -5,6 +5,7 @@ // Copyright (c) DUSK NETWORK. All rights reserved. #![feature(lazy_cell)] +extern crate alloc; #[path = "../tests/common/mod.rs"] mod common; diff --git a/rusk/tests/common/wallet.rs b/rusk/tests/common/wallet.rs index 4cb908e651..07d0510f4c 100644 --- a/rusk/tests/common/wallet.rs +++ b/rusk/tests/common/wallet.rs @@ -4,6 +4,8 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. +pub mod test_wallet; + use std::collections::HashMap; use std::fmt::Debug; use std::sync::{Arc, RwLock}; @@ -25,6 +27,8 @@ use rusk::{Error, Result, Rusk}; use test_wallet::{self as wallet, Store}; use tracing::info; +pub use test_wallet::Wallet; + #[derive(Debug, Clone)] pub struct TestStore; diff --git a/rusk/tests/common/wallet/test_wallet.rs b/rusk/tests/common/wallet/test_wallet.rs new file mode 100644 index 0000000000..a4f56f2f64 --- /dev/null +++ b/rusk/tests/common/wallet/test_wallet.rs @@ -0,0 +1,140 @@ +// 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. + +//! The wallet specification. + +#![deny(missing_docs)] +#![deny(clippy::all)] +#![allow(clippy::result_large_err)] + +extern crate alloc; + +mod imp; + +use alloc::vec::Vec; + +use dusk_core::signatures::bls::{ + PublicKey as BlsPublicKey, SecretKey as BlsSecretKey, +}; +use dusk_core::stake::StakeData; +use dusk_core::transfer::moonlight::AccountData; +use dusk_core::transfer::phoenix::{ + Note, NoteOpening, PublicKey as PhoenixPublicKey, + SecretKey as PhoenixSecretKey, ViewKey as PhoenixViewKey, +}; +use dusk_core::BlsScalar; +use zeroize::Zeroize; + +pub use wallet_core::keys::{ + derive_bls_sk, derive_phoenix_pk, derive_phoenix_sk, +}; + +pub use imp::Wallet; + +/// Stores the cryptographic material necessary to derive cryptographic keys. +pub trait Store { + /// The error type returned from the store. + type Error; + + /// Retrieves the seed used to derive keys. + fn get_seed(&self) -> Result<[u8; 64], Self::Error>; + + /// Retrieve the secret key with the given index. + fn phoenix_secret_key( + &self, + index: u8, + ) -> Result { + let mut seed = self.get_seed()?; + + let sk = derive_phoenix_sk(&seed, index); + + seed.zeroize(); + + Ok(sk) + } + + /// Retrieve the public key with the given index. + fn phoenix_public_key( + &self, + index: u8, + ) -> Result { + let mut seed = self.get_seed()?; + + let pk = derive_phoenix_pk(&seed, index); + + seed.zeroize(); + + Ok(pk) + } + + /// Retrieve the account secret key with the given index. + fn account_secret_key( + &self, + index: u8, + ) -> Result { + let mut seed = self.get_seed()?; + + let sk = derive_bls_sk(&seed, index); + + seed.zeroize(); + + Ok(sk) + } + + /// Retrieve the account public key with the given index. + fn account_public_key( + &self, + index: u8, + ) -> Result { + let mut seed = self.get_seed()?; + + let mut sk = derive_bls_sk(&seed, index); + let pk = BlsPublicKey::from(&sk); + + seed.zeroize(); + sk.zeroize(); + + Ok(pk) + } +} + +/// Types that are clients of the state API. +pub trait StateClient { + /// Error returned by the node client. + type Error; + + /// Find notes for a view key. + fn fetch_notes( + &self, + vk: &PhoenixViewKey, + ) -> Result, Self::Error>; + + /// Fetch the current root of the state. + fn fetch_root(&self) -> Result; + + /// Asks the node to return the nullifiers that already exist from the given + /// nullifiers. + fn fetch_existing_nullifiers( + &self, + nullifiers: &[BlsScalar], + ) -> Result, Self::Error>; + + /// Queries the node to find the opening for a specific note. + fn fetch_opening(&self, note: &Note) -> Result; + + /// Queries the node for the stake of a key. If the key has no stake, a + /// `Default` stake info should be returned. + fn fetch_stake(&self, pk: &BlsPublicKey) -> Result; + + /// Queries the account data for a given key. + fn fetch_account( + &self, + pk: &BlsPublicKey, + ) -> Result; + + /// Queries for the chain ID. + fn fetch_chain_id(&self) -> Result; +} diff --git a/rusk/tests/common/wallet/test_wallet/imp.rs b/rusk/tests/common/wallet/test_wallet/imp.rs new file mode 100644 index 0000000000..c6a924899f --- /dev/null +++ b/rusk/tests/common/wallet/test_wallet/imp.rs @@ -0,0 +1,1167 @@ +// 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. + +use core::convert::Infallible; +use std::string::FromUtf8Error; + +use dusk_bytes::Error as BytesError; +use dusk_core::signatures::bls::{ + PublicKey as BlsPublicKey, SecretKey as BlsSecretKey, +}; +use dusk_core::stake::StakeData; +use dusk_core::transfer::data::TransactionData; +use dusk_core::transfer::moonlight::{ + AccountData, Transaction as MoonlightTransaction, +}; +use dusk_core::transfer::phoenix::{ + Note, NoteLeaf, NoteOpening, PublicKey as PhoenixPublicKey, + SecretKey as PhoenixSecretKey, ViewKey as PhoenixViewKey, +}; +use dusk_core::transfer::Transaction; +use dusk_core::{BlsScalar, Error as ExecutionError}; +use rand::{CryptoRng, Error as RngError, RngCore}; +use rkyv::ser::serializers::{ + AllocScratchError, CompositeSerializerError, SharedSerializeMapError, +}; +use rkyv::validation::validators::CheckDeserializeError; +use rusk_prover::LocalProver; +use wallet_core::keys::{derive_bls_sk, derive_phoenix_sk}; +// use wallet_core::prelude::*; +use wallet_core::transaction::{ + moonlight, moonlight_deployment, moonlight_stake, moonlight_stake_reward, + moonlight_to_phoenix, moonlight_unstake, phoenix as phoenix_transaction, + phoenix_deployment, phoenix_stake, phoenix_stake_reward, + phoenix_to_moonlight, phoenix_unstake, +}; +use wallet_core::{phoenix_balance, BalanceInfo}; +use zeroize::Zeroize; + +use super::{StateClient, Store}; + +const MAX_INPUT_NOTES: usize = 4; + +type SerializerError = CompositeSerializerError< + Infallible, + AllocScratchError, + SharedSerializeMapError, +>; + +/// The error type returned by this crate. +#[derive(Debug)] +#[allow(clippy::large_enum_variant)] +pub enum Error { + /// Underlying store error. + Store(S::Error), + /// Error originating from the state client. + State(SC::Error), + /// Rkyv serialization. + Rkyv, + /// Random number generator error. + Rng(RngError), + /// Serialization and deserialization of Dusk types. + Bytes(BytesError), + /// Bytes were meant to be utf8 but aren't. + Utf8(FromUtf8Error), + /// Originating from the dusk-core error. + Execution(ExecutionError), + /// Note combination for the given value is impossible given the maximum + /// amount if inputs in a phoenix transaction. + NoteCombinationProblem, + /// The key is already staked. This happens when there already is an amount + /// staked for a key and the user tries to make a stake transaction. + AlreadyStaked { + /// The key that already has a stake. + key: BlsPublicKey, + /// Information about the key's stake. + stake: StakeData, + }, + /// The key is not staked. This happens when a key doesn't have an amount + /// staked and the user tries to make an unstake transaction. + NotStaked { + /// The key that is not staked. + key: BlsPublicKey, + /// Information about the key's stake. + stake: StakeData, + }, + /// The key has no reward. This happens when a key has no reward in the + /// stake contract and the user tries to make a stake withdraw transaction. + NoReward { + /// The key that has no reward. + key: BlsPublicKey, + /// Information about the key's stake. + stake: StakeData, + }, +} + +impl Error { + /// Returns an error from the underlying store error. + pub fn from_store_err(se: S::Error) -> Self { + Self::Store(se) + } + /// Returns an error from the underlying state client. + pub fn from_state_err(se: SC::Error) -> Self { + Self::State(se) + } +} + +impl From for Error { + fn from(_: SerializerError) -> Self { + Self::Rkyv + } +} + +impl From> + for Error +{ + fn from(_: CheckDeserializeError) -> Self { + Self::Rkyv + } +} + +impl From for Error { + fn from(re: RngError) -> Self { + Self::Rng(re) + } +} + +impl From for Error { + fn from(be: BytesError) -> Self { + Self::Bytes(be) + } +} + +impl From for Error { + fn from(err: FromUtf8Error) -> Self { + Self::Utf8(err) + } +} + +impl From for Error { + fn from(ee: ExecutionError) -> Self { + Self::Execution(ee) + } +} + +/// A wallet implementation. +/// +/// This is responsible for holding the keys, and performing operations like +/// creating transactions. +pub struct Wallet { + store: S, + state: SC, +} + +impl Wallet { + /// Create a new wallet given the underlying store and node client. + pub const fn new(store: S, state: SC) -> Self { + Self { store, state } + } + + /// Return the inner Store reference + pub const fn store(&self) -> &S { + &self.store + } + + /// Return the inner State reference + pub const fn state(&self) -> &SC { + &self.state + } +} + +impl Wallet +where + S: Store, + SC: StateClient, +{ + /// Retrieve the secret key with the given index. + pub fn phoenix_secret_key( + &self, + index: u8, + ) -> Result> { + self.store + .phoenix_secret_key(index) + .map_err(Error::from_store_err) + } + + /// Retrieve the public key with the given index. + pub fn phoenix_public_key( + &self, + index: u8, + ) -> Result> { + self.store + .phoenix_public_key(index) + .map_err(Error::from_store_err) + } + + /// Retrieve the account secret key with the given index. + pub fn account_secret_key( + &self, + index: u8, + ) -> Result> { + self.store + .account_secret_key(index) + .map_err(Error::from_store_err) + } + + /// Retrieve the account public key with the given index. + pub fn account_public_key( + &self, + index: u8, + ) -> Result> { + self.store + .account_public_key(index) + .map_err(Error::from_store_err) + } + + /// Fetches the notes and nullifiers in the state and returns the notes that + /// are still available for spending. + fn unspent_notes_and_nullifiers( + &self, + sk: &PhoenixSecretKey, + ) -> Result, Error> { + let vk = PhoenixViewKey::from(sk); + + let note_leaves = + self.state.fetch_notes(&vk).map_err(Error::from_state_err)?; + + let nullifiers: Vec<_> = note_leaves + .iter() + .map(|(note, _bh)| note.gen_nullifier(sk)) + .collect(); + + let existing_nullifiers = self + .state + .fetch_existing_nullifiers(&nullifiers) + .map_err(Error::from_state_err)?; + + let unspent_notes_and_nullifiers = note_leaves + .into_iter() + .zip(nullifiers.into_iter()) + .filter(|(_note, nullifier)| { + !existing_nullifiers.contains(nullifier) + }) + .map(|((note, block_height), nullifier)| { + (NoteLeaf { note, block_height }, nullifier) + }) + .collect(); + + Ok(unspent_notes_and_nullifiers) + } + + /// Here we fetch the notes and their nullifiers to cover the + /// transaction-costs. + #[allow(clippy::type_complexity)] + fn input_notes_nullifiers( + &self, + sender_sk: &PhoenixSecretKey, + transaction_cost: u64, + ) -> Result, Error> { + let sender_vk = PhoenixViewKey::from(sender_sk); + + // decrypt the value of all unspent note + let unspent_notes_nullifiers = + self.unspent_notes_and_nullifiers(sender_sk)?; + let mut notes_values_nullifiers = + Vec::with_capacity(unspent_notes_nullifiers.len()); + + let mut accumulated_value = 0; + for (note_leaf, nullifier) in unspent_notes_nullifiers { + let val = note_leaf + .note + .value(Some(&sender_vk)) + .map_err(|_| ExecutionError::PhoenixOwnership)?; + accumulated_value += val; + notes_values_nullifiers.push((note_leaf.note, val, nullifier)); + } + + if accumulated_value < transaction_cost { + return Err(ExecutionError::InsufficientBalance.into()); + } + + // pick the four smallest notes that cover the costs + let inputs = pick_notes(transaction_cost, notes_values_nullifiers); + + if inputs.is_empty() { + return Err(Error::NoteCombinationProblem); + } + + Ok(inputs) + } + + /// Here we fetch the notes, their openings and nullifiers to cover the + /// transfer-costs. + #[allow(clippy::type_complexity)] + fn input_notes_openings_nullifiers( + &self, + sender_sk: &PhoenixSecretKey, + transaction_cost: u64, + ) -> Result, Error> { + let notes_and_nullifiers = + self.input_notes_nullifiers(sender_sk, transaction_cost)?; + + let mut notes_openings_nullifiers = + Vec::with_capacity(notes_and_nullifiers.len()); + for (note, nullifier) in notes_and_nullifiers.into_iter() { + let opening = self + .state + .fetch_opening(¬e) + .map_err(Error::from_state_err)?; + notes_openings_nullifiers.push((note, opening, nullifier)); + } + + Ok(notes_openings_nullifiers) + } + + /// Here we fetch the notes and their openings to cover the + /// transfer-costs. + #[allow(clippy::type_complexity)] + fn input_notes_openings( + &self, + sender_sk: &PhoenixSecretKey, + transaction_cost: u64, + ) -> Result, Error> { + let notes_and_nullifiers = + self.input_notes_nullifiers(sender_sk, transaction_cost)?; + + let mut notes_openings = Vec::with_capacity(notes_and_nullifiers.len()); + for (note, _nullifier) in notes_and_nullifiers.into_iter() { + let opening = self + .state + .fetch_opening(¬e) + .map_err(Error::from_state_err)?; + notes_openings.push((note, opening)); + } + + Ok(notes_openings) + } + + /// Executes a generic contract call, paying gas from a public account. + #[allow(clippy::too_many_arguments)] + pub fn moonlight_execute( + &self, + sender_idx: u8, + transfer_value: u64, + deposit: u64, + gas_limit: u64, + gas_price: u64, + exec: Option>, + ) -> Result> { + let state = self.state(); + + let mut sender_sk = self.account_secret_key(sender_idx)?; + let sender = self.account_public_key(sender_idx)?; + + let account = state + .fetch_account(&sender) + .map_err(Error::from_state_err)?; + + // 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().map_err(Error::from_state_err)?; + + let tx = moonlight( + &sender_sk, + None, + transfer_value, + deposit, + gas_limit, + gas_price, + nonce, + chain_id, + exec, + )?; + + sender_sk.zeroize(); + Ok(tx) + } + + /// Execute a generic contract call or deployment, using Phoenix notes to + /// pay for gas. + #[allow(clippy::too_many_arguments)] + pub fn phoenix_execute( + &self, + rng: &mut Rng, + sender_index: u8, + gas_limit: u64, + gas_price: u64, + deposit: u64, + data: impl Into, + ) -> Result> { + let mut sender_sk = self.phoenix_secret_key(sender_index)?; + let receiver_pk = self.phoenix_public_key(sender_index)?; + let refund_pk = receiver_pk; + + let input_notes_openings = self.input_notes_openings( + &sender_sk, + gas_limit * gas_price + deposit, + )?; + + let root = self.state.fetch_root().map_err(Error::from_state_err)?; + + let transfer_value = 0; + let obfuscated_transaction = false; + + let chain_id = + self.state.fetch_chain_id().map_err(Error::from_state_err)?; + + let tx = phoenix_transaction( + rng, + &sender_sk, + &refund_pk, + &receiver_pk, + input_notes_openings, + root, + transfer_value, + obfuscated_transaction, + deposit, + gas_limit, + gas_price, + chain_id, + Some(data), + &LocalProver, + )?; + + sender_sk.zeroize(); + + Ok(tx) + } + + /// Transfer Dusk in the form of Phoenix notes from one key to another. + #[allow(clippy::too_many_arguments)] + pub fn phoenix_transfer( + &self, + rng: &mut Rng, + sender_index: u8, + receiver_pk: &PhoenixPublicKey, + transfer_value: u64, + gas_limit: u64, + gas_price: u64, + ) -> Result> { + let mut sender_sk = self.phoenix_secret_key(sender_index)?; + let refund_pk = self.phoenix_public_key(sender_index)?; + + let input_notes_openings = self.input_notes_openings( + &sender_sk, + transfer_value + gas_limit * gas_price, + )?; + + let root = self.state.fetch_root().map_err(Error::from_state_err)?; + + let obfuscated_transaction = true; + let deposit = 0; + + let data: Option = None; + + let chain_id = + self.state.fetch_chain_id().map_err(Error::from_state_err)?; + + let tx = phoenix_transaction( + rng, + &sender_sk, + &refund_pk, + &receiver_pk, + input_notes_openings, + root, + transfer_value, + obfuscated_transaction, + deposit, + gas_limit, + gas_price, + chain_id, + data, + &LocalProver, + )?; + + sender_sk.zeroize(); + + Ok(tx) + } + + /// Stakes an amount of Dusk using Phoenix notes. + #[allow(clippy::too_many_arguments)] + pub fn phoenix_stake( + &self, + rng: &mut Rng, + sender_index: u8, + staker_index: u8, + stake_value: u64, + gas_limit: u64, + gas_price: u64, + ) -> Result> { + let mut phoenix_sender_sk = self.phoenix_secret_key(sender_index)?; + let mut stake_sk = self.account_secret_key(staker_index)?; + + let inputs = self.input_notes_openings( + &phoenix_sender_sk, + gas_limit * gas_price + stake_value, + )?; + + let root = self.state.fetch_root().map_err(Error::from_state_err)?; + + let chain_id = + self.state.fetch_chain_id().map_err(Error::from_state_err)?; + + let tx = phoenix_stake( + rng, + &phoenix_sender_sk, + &stake_sk, + &stake_sk, + inputs, + root, + gas_limit, + gas_price, + chain_id, + stake_value, + &LocalProver, + )?; + + stake_sk.zeroize(); + phoenix_sender_sk.zeroize(); + + Ok(tx) + } + + /// Unstakes a key from the stake contract, using Phoenix notes. + pub fn phoenix_unstake( + &self, + rng: &mut Rng, + sender_index: u8, + staker_index: u8, + gas_limit: u64, + gas_price: u64, + ) -> Result> { + let mut phoenix_sender_sk = self.phoenix_secret_key(sender_index)?; + let mut stake_sk = self.account_secret_key(staker_index)?; + + let stake_pk = BlsPublicKey::from(&stake_sk); + + let inputs = self.input_notes_openings_nullifiers( + &phoenix_sender_sk, + gas_limit * gas_price, + )?; + + let root = self.state.fetch_root().map_err(Error::from_state_err)?; + + let stake = self + .state + .fetch_stake(&stake_pk) + .map_err(Error::from_state_err)?; + + let staked_amount = stake + .amount + .ok_or(Error::NotStaked { + key: stake_pk, + stake, + })? + .value; + + let chain_id = + self.state.fetch_chain_id().map_err(Error::from_state_err)?; + + let tx = phoenix_unstake( + rng, + &phoenix_sender_sk, + &stake_sk, + &stake_sk, + inputs, + root, + staked_amount, + gas_limit, + gas_price, + chain_id, + &LocalProver, + )?; + + stake_sk.zeroize(); + phoenix_sender_sk.zeroize(); + + Ok(tx) + } + + /// Withdraw the accumulated staking reward for a key, into Phoenix notes. + /// Rewards are accumulated by participating in the consensus. + pub fn phoenix_stake_withdraw( + &self, + rng: &mut Rng, + sender_index: u8, + staker_index: u8, + gas_limit: u64, + gas_price: u64, + ) -> Result> { + let mut phoenix_sender_sk = self.phoenix_secret_key(sender_index)?; + let mut stake_sk = self.account_secret_key(staker_index)?; + + let stake_pk = BlsPublicKey::from(&stake_sk); + + let inputs = self.input_notes_openings_nullifiers( + &phoenix_sender_sk, + gas_limit * gas_price, + )?; + + let root = self.state.fetch_root().map_err(Error::from_state_err)?; + + let stake_reward = self + .state + .fetch_stake(&stake_pk) + .map_err(Error::from_state_err)? + .reward; + + let chain_id = + self.state.fetch_chain_id().map_err(Error::from_state_err)?; + + let tx = phoenix_stake_reward( + rng, + &phoenix_sender_sk, + &stake_sk, + &stake_sk, + inputs, + root, + stake_reward, + gas_limit, + gas_price, + chain_id, + &LocalProver, + )?; + + stake_sk.zeroize(); + phoenix_sender_sk.zeroize(); + + Ok(tx) + } + + /// Convert some Phoenix Dusk into Moonlight Dusk. + pub fn phoenix_to_moonlight( + &self, + rng: &mut Rng, + phoenix_sender_index: u8, + moonlight_receiver_index: u8, + convert_value: u64, + gas_limit: u64, + gas_price: u64, + ) -> Result> { + let mut phoenix_sender_sk = + self.phoenix_secret_key(phoenix_sender_index)?; + let mut moonlight_receiver_sk = + self.account_secret_key(moonlight_receiver_index)?; + + let inputs = self.input_notes_openings_nullifiers( + &phoenix_sender_sk, + gas_limit * gas_price, + )?; + + let root = self.state.fetch_root().map_err(Error::from_state_err)?; + + let chain_id = + self.state.fetch_chain_id().map_err(Error::from_state_err)?; + + let tx = phoenix_to_moonlight( + rng, + &phoenix_sender_sk, + &moonlight_receiver_sk, + inputs, + root, + convert_value, + gas_limit, + gas_price, + chain_id, + &LocalProver, + )?; + + phoenix_sender_sk.zeroize(); + moonlight_receiver_sk.zeroize(); + + Ok(tx) + } + + /// Deploy a contract using Phoenix to pay for gas. + pub fn phoenix_deployment( + &self, + rng: &mut Rng, + sender_index: u8, + bytecode: impl Into>, + owner: &BlsPublicKey, + init_args: Vec, + nonce: u64, + gas_limit: u64, + gas_price: u64, + ) -> Result> { + let mut phoenix_sender_sk = self.phoenix_secret_key(sender_index)?; + + let inputs = self.input_notes_openings_nullifiers( + &phoenix_sender_sk, + gas_limit * gas_price, + )?; + + let root = self.state.fetch_root().map_err(Error::from_state_err)?; + + let chain_id = + self.state.fetch_chain_id().map_err(Error::from_state_err)?; + + let tx = phoenix_deployment( + rng, + &phoenix_sender_sk, + inputs, + root, + bytecode, + owner, + init_args, + nonce, + gas_limit, + gas_price, + chain_id, + &LocalProver, + )?; + + phoenix_sender_sk.zeroize(); + + Ok(tx) + } + + /// Transfer Dusk from one account to another using moonlight. + pub fn moonlight_transfer( + &self, + sender_index: u8, + receiver_account: BlsPublicKey, + value: u64, + gas_limit: u64, + gas_price: u64, + ) -> Result> { + let deposit = 0; + let data: Option = None; + + self.moonlight_transaction( + sender_index, + Some(receiver_account), + value, + deposit, + gas_limit, + gas_price, + data, + ) + } + + /// Creates a generic moonlight transaction. + #[allow(clippy::too_many_arguments)] + pub fn moonlight_transaction( + &self, + sender_index: u8, + receiver_account: Option, + value: u64, + deposit: u64, + gas_limit: u64, + gas_price: u64, + data: Option>, + ) -> Result> { + let mut seed = self.store.get_seed().map_err(Error::from_store_err)?; + let mut sender_sk = derive_bls_sk(&seed, sender_index); + let sender_account = BlsPublicKey::from(&sender_sk); + + let account = self + .state + .fetch_account(&sender_account) + .map_err(Error::from_state_err)?; + + // technically this check is not necessary, but it's nice to not spam + // the network with transactions that are unspendable. + let max_value = value + deposit + gas_limit * gas_price; + if max_value > account.balance { + return Err(ExecutionError::InsufficientBalance.into()); + } + let nonce = account.nonce + 1; + + let chain_id = + self.state.fetch_chain_id().map_err(Error::from_state_err)?; + + let tx = MoonlightTransaction::new( + &sender_sk, + receiver_account, + value, + deposit, + gas_limit, + gas_price, + nonce, + chain_id, + data, + )?; + + seed.zeroize(); + sender_sk.zeroize(); + + Ok(tx.into()) + } + + /// Stakes an amount of Dusk using a Moonlight account. + #[allow(clippy::too_many_arguments)] + pub fn moonlight_stake( + &self, + sender_index: u8, + staker_index: u8, + stake_value: u64, + gas_limit: u64, + gas_price: u64, + ) -> Result> { + let mut sender_sk = self.account_secret_key(sender_index)?; + let sender_pk = self.account_public_key(sender_index)?; + + let mut staker_sk = self.account_secret_key(staker_index)?; + + let sender_account = self + .state + .fetch_account(&sender_pk) + .map_err(Error::from_state_err)?; + + let chain_id = + self.state.fetch_chain_id().map_err(Error::from_state_err)?; + + let tx = moonlight_stake( + &sender_sk, + &staker_sk, + &staker_sk, + stake_value, + gas_limit, + gas_price, + sender_account.nonce + 1, + chain_id, + )?; + + sender_sk.zeroize(); + staker_sk.zeroize(); + + Ok(tx) + } + + /// Unstakes a key from the stake contract, using a Moonlight account. + pub fn moonlight_unstake( + &self, + rng: &mut Rng, + sender_index: u8, + staker_index: u8, + gas_limit: u64, + gas_price: u64, + ) -> Result> { + let mut sender_sk = self.account_secret_key(sender_index)?; + let sender_pk = self.account_public_key(sender_index)?; + + let mut staker_sk = self.account_secret_key(staker_index)?; + let staker_pk = self.account_public_key(staker_index)?; + + let sender_account = self + .state + .fetch_account(&sender_pk) + .map_err(Error::from_state_err)?; + let staker_data = self + .state + .fetch_stake(&staker_pk) + .map_err(Error::from_state_err)?; + + let unstake_value = staker_data + .amount + .ok_or(Error::NotStaked { + key: staker_pk, + stake: staker_data, + })? + .value; + let chain_id = + self.state.fetch_chain_id().map_err(Error::from_state_err)?; + + let tx = moonlight_unstake( + rng, + &sender_sk, + &staker_sk, + &staker_sk, + unstake_value, + gas_limit, + gas_price, + sender_account.nonce + 1, + chain_id, + )?; + + sender_sk.zeroize(); + staker_sk.zeroize(); + + Ok(tx) + } + + /// Withdraw the accumulated staking reward for a key, into a Moonlight + /// notes. Rewards are accumulated by participating in the consensus. + pub fn moonlight_stake_withdraw( + &self, + rng: &mut Rng, + sender_index: u8, + staker_index: u8, + gas_limit: u64, + gas_price: u64, + ) -> Result> { + let mut sender_sk = self.account_secret_key(sender_index)?; + let sender_pk = self.account_public_key(sender_index)?; + + let mut staker_sk = self.account_secret_key(staker_index)?; + let staker_pk = self.account_public_key(staker_index)?; + + let sender_account = self + .state + .fetch_account(&sender_pk) + .map_err(Error::from_state_err)?; + let staker_data = self + .state + .fetch_stake(&staker_pk) + .map_err(Error::from_state_err)?; + + let chain_id = + self.state.fetch_chain_id().map_err(Error::from_state_err)?; + + let tx = moonlight_stake_reward( + rng, + &sender_sk, + &staker_sk, + &staker_sk, + staker_data.reward, + gas_limit, + gas_price, + sender_account.nonce + 1, + chain_id, + )?; + + sender_sk.zeroize(); + staker_sk.zeroize(); + + Ok(tx) + } + + /// Convert some Moonlight Dusk into Phoenix Dusk. + pub fn moonlight_to_phoenix( + &self, + rng: &mut Rng, + moonlight_sender_index: u8, + phoenix_receiver_index: u8, + convert_value: u64, + gas_limit: u64, + gas_price: u64, + ) -> Result> { + let mut moonlight_sender_sk = + self.account_secret_key(moonlight_sender_index)?; + let moonlight_sender_pk = + self.account_public_key(moonlight_sender_index)?; + let mut phoenix_receiver_sk = + self.phoenix_secret_key(phoenix_receiver_index)?; + + let moonlight_sender_account = self + .state + .fetch_account(&moonlight_sender_pk) + .map_err(Error::from_state_err)?; + + let nonce = moonlight_sender_account.nonce + 1; + + let chain_id = + self.state.fetch_chain_id().map_err(Error::from_state_err)?; + + let tx = moonlight_to_phoenix( + rng, + &moonlight_sender_sk, + &phoenix_receiver_sk, + convert_value, + gas_limit, + gas_price, + nonce, + chain_id, + )?; + + moonlight_sender_sk.zeroize(); + phoenix_receiver_sk.zeroize(); + + Ok(tx) + } + + /// Deploy a contract using Moonlight to pay for gas. + pub fn moonlight_deployment( + &self, + sender_index: u8, + bytecode: impl Into>, + owner: &BlsPublicKey, + init_args: Vec, + gas_limit: u64, + gas_price: u64, + deploy_nonce: u64, + ) -> Result> { + let mut sender_sk = self.account_secret_key(sender_index)?; + let sender_pk = self.account_public_key(sender_index)?; + + let chain_id = + self.state.fetch_chain_id().map_err(Error::from_state_err)?; + + let moonlight_current_nonce = self + .state + .fetch_account(&sender_pk) + .map_err(Error::from_state_err)? + .nonce; + + let moonlight_nonce = moonlight_current_nonce + 1; + + let tx = moonlight_deployment( + &sender_sk, + bytecode, + owner, + init_args, + gas_limit, + gas_price, + moonlight_nonce, + deploy_nonce, + chain_id, + )?; + + sender_sk.zeroize(); + + Ok(tx) + } + + /// Gets the balance of a key. + pub fn get_balance( + &self, + sk_index: u8, + ) -> Result> { + let mut seed = self.store.get_seed().map_err(Error::from_store_err)?; + let mut phoenix_sk = derive_phoenix_sk(&seed, sk_index); + let phoenix_vk = PhoenixViewKey::from(&phoenix_sk); + + let unspent_notes: Vec = self + .unspent_notes_and_nullifiers(&phoenix_sk)? + .into_iter() + .map(|(note_leaf, _nul)| note_leaf) + .collect(); + let balance = phoenix_balance(&phoenix_vk, unspent_notes.iter()); + + seed.zeroize(); + phoenix_sk.zeroize(); + + Ok(balance) + } + + /// Gets the stake and the expiration of said stake for a key. + pub fn get_stake(&self, sk_index: u8) -> Result> { + let mut seed = self.store.get_seed().map_err(Error::from_store_err)?; + let mut account_sk = derive_bls_sk(&seed, sk_index); + + let account_pk = BlsPublicKey::from(&account_sk); + + let stake = self + .state + .fetch_stake(&account_pk) + .map_err(Error::from_state_err)?; + + seed.zeroize(); + account_sk.zeroize(); + + Ok(stake) + } + + /// Gets the account data for a key. + pub fn get_account( + &self, + sk_index: u8, + ) -> Result> { + let mut seed = self.store.get_seed().map_err(Error::from_store_err)?; + let mut account_sk = derive_bls_sk(&seed, sk_index); + + let account_pk = BlsPublicKey::from(&account_sk); + + let account = self + .state + .fetch_account(&account_pk) + .map_err(Error::from_state_err)?; + + seed.zeroize(); + account_sk.zeroize(); + + Ok(account) + } +} + +/// Pick the notes to be used in a phoenix 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_values_nullifiers: Vec<(Note, u64, BlsScalar)>, +) -> Vec<(Note, BlsScalar)> { + let mut notes_values_nullifiers = notes_values_nullifiers; + let len = notes_values_nullifiers.len(); + + if len <= MAX_INPUT_NOTES { + return notes_values_nullifiers + .into_iter() + .map(|(note, _value, nullifier)| (note, nullifier)) + .collect(); + } + + notes_values_nullifiers + .sort_by(|(_, aval, _), (_, bval, _)| aval.cmp(bval)); + + pick_lexicographic(notes_values_nullifiers.len(), |indices| { + indices + .iter() + .map(|index| ¬es_values_nullifiers[*index].1) + .sum::() + >= value + }) + .map(|indices| { + indices + .into_iter() + .map(|index| { + let (note, _value, nullifier) = + notes_values_nullifiers[index].clone(); + (note, nullifier) + }) + .collect() + }) + .unwrap_or_default() +} + +fn pick_lexicographic bool>( + max_len: usize, + is_valid: F, +) -> Option<[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 Some(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; + } + } + + None +} diff --git a/rusk/tests/rusk-state.rs b/rusk/tests/rusk-state.rs index d9d687d296..93b72e9406 100644 --- a/rusk/tests/rusk-state.rs +++ b/rusk/tests/rusk-state.rs @@ -191,7 +191,7 @@ async fn generate_phoenix_txs() -> Result<(), Box> { Arc::new(std::sync::RwLock::new(std::collections::HashMap::new())); let wallet = - test_wallet::Wallet::new(TestStore, TestStateClient { rusk, cache }); + crate::wallet::Wallet::new(TestStore, TestStateClient { rusk, cache }); const N_ADDRESSES: usize = 100; @@ -253,7 +253,7 @@ async fn generate_moonlight_txs() -> Result<(), Box> { Arc::new(std::sync::RwLock::new(std::collections::HashMap::new())); let wallet = - test_wallet::Wallet::new(TestStore, TestStateClient { rusk, cache }); + crate::wallet::Wallet::new(TestStore, TestStateClient { rusk, cache }); const N_ADDRESSES: usize = 100; diff --git a/rusk/tests/services/contract_deployment.rs b/rusk/tests/services/contract_deployment.rs index 3a346a8f2e..2a4a510e75 100644 --- a/rusk/tests/services/contract_deployment.rs +++ b/rusk/tests/services/contract_deployment.rs @@ -18,7 +18,6 @@ use rand::rngs::StdRng; use rusk::{Result, Rusk, DUSK_CONSENSUS_KEY}; use rusk_recovery_tools::state; use tempfile::tempdir; -use test_wallet::{self as wallet, Wallet}; use tokio::sync::broadcast; use tracing::info; @@ -29,7 +28,9 @@ use crate::common::state::{generator_procedure, ExecuteResult}; use crate::common::state::{ DEFAULT_GAS_PER_DEPLOY_BYTE, DEFAULT_MIN_GAS_LIMIT, }; -use crate::common::wallet::{TestStateClient, TestStore}; +use crate::common::wallet::{ + test_wallet as wallet, TestStateClient, TestStore, Wallet, +}; const BLOCK_HEIGHT: u64 = 1; const BLOCK_GAS_LIMIT: u64 = 1_000_000_000_000; diff --git a/rusk/tests/services/contract_stake.rs b/rusk/tests/services/contract_stake.rs index 90bee3616e..2f137fdf56 100644 --- a/rusk/tests/services/contract_stake.rs +++ b/rusk/tests/services/contract_stake.rs @@ -20,11 +20,12 @@ use rand::rngs::StdRng; use rusk::{Result, Rusk}; use std::collections::HashMap; use tempfile::tempdir; -use test_wallet::{self as wallet}; use tracing::info; use crate::common::state::{generator_procedure2, new_state}; -use crate::common::wallet::{TestStateClient, TestStore}; +use crate::common::wallet::{ + test_wallet as wallet, TestStateClient, TestStore, +}; use crate::common::*; const BLOCK_HEIGHT: u64 = 1; diff --git a/rusk/tests/services/conversion.rs b/rusk/tests/services/conversion.rs index 41ea664552..b038cc9400 100644 --- a/rusk/tests/services/conversion.rs +++ b/rusk/tests/services/conversion.rs @@ -13,11 +13,12 @@ use rand::prelude::*; use rand::rngs::StdRng; use rusk::{Result, Rusk}; use tempfile::tempdir; -use test_wallet::{self as wallet}; use crate::common::logger; use crate::common::state::{generator_procedure, new_state}; -use crate::common::wallet::{TestStateClient, TestStore}; +use crate::common::wallet::{ + test_wallet as wallet, TestStateClient, TestStore, +}; const BLOCK_GAS_LIMIT: u64 = 100_000_000_000; diff --git a/rusk/tests/services/gas_behavior.rs b/rusk/tests/services/gas_behavior.rs index 478b004eb9..e552f3d983 100644 --- a/rusk/tests/services/gas_behavior.rs +++ b/rusk/tests/services/gas_behavior.rs @@ -16,12 +16,13 @@ use rand::prelude::*; use rand::rngs::StdRng; use rusk::{Result, Rusk}; use tempfile::tempdir; -use test_wallet::{self as wallet}; use tracing::info; use crate::common::logger; use crate::common::state::{generator_procedure, new_state}; -use crate::common::wallet::{TestStateClient, TestStore}; +use crate::common::wallet::{ + test_wallet as wallet, TestStateClient, TestStore, +}; const BLOCK_HEIGHT: u64 = 1; const BLOCK_GAS_LIMIT: u64 = 1_000_000_000_000; diff --git a/rusk/tests/services/moonlight_stake.rs b/rusk/tests/services/moonlight_stake.rs index f28cc26c86..a4fee5b358 100644 --- a/rusk/tests/services/moonlight_stake.rs +++ b/rusk/tests/services/moonlight_stake.rs @@ -14,11 +14,12 @@ use rand::rngs::StdRng; use rusk::{Result, Rusk}; use std::collections::HashMap; use tempfile::tempdir; -use test_wallet::{self as wallet}; use tracing::info; use crate::common::state::{generator_procedure, new_state}; -use crate::common::wallet::{TestStateClient, TestStore}; +use crate::common::wallet::{ + test_wallet as wallet, TestStateClient, TestStore, +}; use crate::common::*; const BLOCK_HEIGHT: u64 = 1; diff --git a/rusk/tests/services/multi_transfer.rs b/rusk/tests/services/multi_transfer.rs index 4761f2b70c..689a290a7f 100644 --- a/rusk/tests/services/multi_transfer.rs +++ b/rusk/tests/services/multi_transfer.rs @@ -12,12 +12,13 @@ use rand::prelude::*; use rand::rngs::StdRng; use rusk::{Result, Rusk}; use tempfile::tempdir; -use test_wallet::{self as wallet}; use tracing::info; use crate::common::logger; use crate::common::state::{generator_procedure, new_state, ExecuteResult}; -use crate::common::wallet::{TestStateClient, TestStore}; +use crate::common::wallet::{ + test_wallet as wallet, TestStateClient, TestStore, +}; const BLOCK_HEIGHT: u64 = 1; // This is purposefully chosen to be low to trigger the discarding of a diff --git a/rusk/tests/services/owner_calls.rs b/rusk/tests/services/owner_calls.rs index b8f895db18..adaac088bb 100644 --- a/rusk/tests/services/owner_calls.rs +++ b/rusk/tests/services/owner_calls.rs @@ -23,7 +23,6 @@ use dusk_vm::{gen_contract_id, CallReceipt, ContractData, Session, VM}; use rusk::{Error, Result, Rusk}; use rusk_recovery_tools::state; use tempfile::tempdir; -use test_wallet::{self as wallet, Wallet}; use tokio::sync::broadcast; use tracing::info; @@ -33,7 +32,9 @@ use crate::common::state::DEFAULT_MIN_DEPLOY_POINTS; use crate::common::state::{ DEFAULT_GAS_PER_DEPLOY_BYTE, DEFAULT_MIN_GAS_LIMIT, }; -use crate::common::wallet::{TestStateClient, TestStore}; +use crate::common::wallet::{ + test_wallet as wallet, test_wallet::Wallet, TestStateClient, TestStore, +}; const BLOCK_GAS_LIMIT: u64 = 1_000_000_000_000; const POINT_LIMIT: u64 = 0x10000000; diff --git a/rusk/tests/services/phoenix_stake.rs b/rusk/tests/services/phoenix_stake.rs index aa83b51345..f0c3318e4a 100644 --- a/rusk/tests/services/phoenix_stake.rs +++ b/rusk/tests/services/phoenix_stake.rs @@ -20,11 +20,12 @@ use rand::rngs::StdRng; use rusk::{Result, Rusk}; use std::collections::HashMap; use tempfile::tempdir; -use test_wallet::{self as wallet}; use tracing::info; use crate::common::state::{generator_procedure, new_state}; -use crate::common::wallet::{TestStateClient, TestStore}; +use crate::common::wallet::{ + test_wallet as wallet, TestStateClient, TestStore, +}; use crate::common::*; const BLOCK_HEIGHT: u64 = 1; diff --git a/rusk/tests/services/transfer.rs b/rusk/tests/services/transfer.rs index c618119e9d..8655407301 100644 --- a/rusk/tests/services/transfer.rs +++ b/rusk/tests/services/transfer.rs @@ -13,12 +13,13 @@ use rand::prelude::*; use rand::rngs::StdRng; use rusk::{Result, Rusk}; use tempfile::tempdir; -use test_wallet::{self as wallet}; use tracing::info; use crate::common::logger; use crate::common::state::{generator_procedure, new_state}; -use crate::common::wallet::{TestStateClient, TestStore}; +use crate::common::wallet::{ + test_wallet as wallet, TestStateClient, TestStore, +}; const BLOCK_GAS_LIMIT: u64 = 100_000_000_000; const INITIAL_BALANCE: u64 = 10_000_000_000; diff --git a/rusk/tests/services/unspendable.rs b/rusk/tests/services/unspendable.rs index daa8932f7e..0d425c2697 100644 --- a/rusk/tests/services/unspendable.rs +++ b/rusk/tests/services/unspendable.rs @@ -16,12 +16,13 @@ use rand::prelude::*; use rand::rngs::StdRng; use rusk::{Result, Rusk}; use tempfile::tempdir; -use test_wallet::{self as wallet}; use tracing::info; use crate::common::logger; use crate::common::state::{generator_procedure, new_state, ExecuteResult}; -use crate::common::wallet::{TestStateClient, TestStore}; +use crate::common::wallet::{ + test_wallet as wallet, TestStateClient, TestStore, +}; const BLOCK_HEIGHT: u64 = 1; const BLOCK_GAS_LIMIT: u64 = 1_000_000_000_000; diff --git a/rusk/tests/tests.rs b/rusk/tests/tests.rs index a4dd97f5a5..3b24fd87b2 100644 --- a/rusk/tests/tests.rs +++ b/rusk/tests/tests.rs @@ -7,3 +7,5 @@ #![feature(lazy_cell)] mod common; mod services; + +extern crate alloc; From 0662a65f2cb6df544ca432599f208a8d19649b50 Mon Sep 17 00:00:00 2001 From: Herr Seppia Date: Mon, 20 Jan 2025 15:26:46 +0100 Subject: [PATCH 2/3] workspace: remove `test-wallet` --- Cargo.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5df8715091..d943bdeb7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,8 +25,6 @@ members = [ "node", "rusk-wallet", - # Test utils - "test-wallet", ] resolver = "2" From 9cf1af1344515aa67534584a91dd3a365a6906cd Mon Sep 17 00:00:00 2001 From: Herr Seppia Date: Mon, 20 Jan 2025 15:26:56 +0100 Subject: [PATCH 3/3] test-wallet: remove crate --- test-wallet/Cargo.toml | 22 - test-wallet/src/imp.rs | 1167 ---------------------------------------- test-wallet/src/lib.rs | 140 ----- 3 files changed, 1329 deletions(-) delete mode 100644 test-wallet/Cargo.toml delete mode 100644 test-wallet/src/imp.rs delete mode 100644 test-wallet/src/lib.rs diff --git a/test-wallet/Cargo.toml b/test-wallet/Cargo.toml deleted file mode 100644 index 139faad839..0000000000 --- a/test-wallet/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "test-wallet" -version = "0.1.0" -edition = "2021" -description = "Test wallet used for Rusk" -license = "MPL-2.0" - -[dependencies] -rand = { workspace = true } -dusk-bytes = { workspace = true } -rkyv = { workspace = true } -ff = { workspace = true } -zeroize = { workspace = true, features = ["derive"] } - -dusk-core = { workspace = true } -wallet-core = { workspace = true } -rusk-prover = { workspace = true } - -[dev-dependencies] - -[lib] -crate-type = ["cdylib", "rlib"] diff --git a/test-wallet/src/imp.rs b/test-wallet/src/imp.rs deleted file mode 100644 index 98be4ff899..0000000000 --- a/test-wallet/src/imp.rs +++ /dev/null @@ -1,1167 +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. - -use alloc::string::FromUtf8Error; -use alloc::vec::Vec; -use core::convert::Infallible; - -use dusk_bytes::Error as BytesError; -use dusk_core::signatures::bls::{ - PublicKey as BlsPublicKey, SecretKey as BlsSecretKey, -}; -use dusk_core::stake::StakeData; -use dusk_core::transfer::data::TransactionData; -use dusk_core::transfer::moonlight::{ - AccountData, Transaction as MoonlightTransaction, -}; -use dusk_core::transfer::phoenix::{ - Note, NoteLeaf, NoteOpening, PublicKey as PhoenixPublicKey, - SecretKey as PhoenixSecretKey, ViewKey as PhoenixViewKey, -}; -use dusk_core::transfer::Transaction; -use dusk_core::{BlsScalar, Error as ExecutionError}; -use rand::{CryptoRng, Error as RngError, RngCore}; -use rkyv::ser::serializers::{ - AllocScratchError, CompositeSerializerError, SharedSerializeMapError, -}; -use rkyv::validation::validators::CheckDeserializeError; -use rusk_prover::LocalProver; -use wallet_core::keys::{derive_bls_sk, derive_phoenix_sk}; -use wallet_core::transaction::{ - moonlight, moonlight_deployment, moonlight_stake, moonlight_stake_reward, - moonlight_to_phoenix, moonlight_unstake, phoenix as phoenix_transaction, - phoenix_deployment, phoenix_stake, phoenix_stake_reward, - phoenix_to_moonlight, phoenix_unstake, -}; -use wallet_core::{phoenix_balance, BalanceInfo}; -use zeroize::Zeroize; - -use crate::{StateClient, Store}; - -const MAX_INPUT_NOTES: usize = 4; - -type SerializerError = CompositeSerializerError< - Infallible, - AllocScratchError, - SharedSerializeMapError, ->; - -/// The error type returned by this crate. -#[derive(Debug)] -#[allow(clippy::large_enum_variant)] -pub enum Error { - /// Underlying store error. - Store(S::Error), - /// Error originating from the state client. - State(SC::Error), - /// Rkyv serialization. - Rkyv, - /// Random number generator error. - Rng(RngError), - /// Serialization and deserialization of Dusk types. - Bytes(BytesError), - /// Bytes were meant to be utf8 but aren't. - Utf8(FromUtf8Error), - /// Originating from the dusk-core error. - Execution(ExecutionError), - /// Note combination for the given value is impossible given the maximum - /// amount if inputs in a phoenix transaction. - NoteCombinationProblem, - /// The key is already staked. This happens when there already is an amount - /// staked for a key and the user tries to make a stake transaction. - AlreadyStaked { - /// The key that already has a stake. - key: BlsPublicKey, - /// Information about the key's stake. - stake: StakeData, - }, - /// The key is not staked. This happens when a key doesn't have an amount - /// staked and the user tries to make an unstake transaction. - NotStaked { - /// The key that is not staked. - key: BlsPublicKey, - /// Information about the key's stake. - stake: StakeData, - }, - /// The key has no reward. This happens when a key has no reward in the - /// stake contract and the user tries to make a stake withdraw transaction. - NoReward { - /// The key that has no reward. - key: BlsPublicKey, - /// Information about the key's stake. - stake: StakeData, - }, -} - -impl Error { - /// Returns an error from the underlying store error. - pub fn from_store_err(se: S::Error) -> Self { - Self::Store(se) - } - /// Returns an error from the underlying state client. - pub fn from_state_err(se: SC::Error) -> Self { - Self::State(se) - } -} - -impl From for Error { - fn from(_: SerializerError) -> Self { - Self::Rkyv - } -} - -impl From> - for Error -{ - fn from(_: CheckDeserializeError) -> Self { - Self::Rkyv - } -} - -impl From for Error { - fn from(re: RngError) -> Self { - Self::Rng(re) - } -} - -impl From for Error { - fn from(be: BytesError) -> Self { - Self::Bytes(be) - } -} - -impl From for Error { - fn from(err: FromUtf8Error) -> Self { - Self::Utf8(err) - } -} - -impl From for Error { - fn from(ee: ExecutionError) -> Self { - Self::Execution(ee) - } -} - -/// A wallet implementation. -/// -/// This is responsible for holding the keys, and performing operations like -/// creating transactions. -pub struct Wallet { - store: S, - state: SC, -} - -impl Wallet { - /// Create a new wallet given the underlying store and node client. - pub const fn new(store: S, state: SC) -> Self { - Self { store, state } - } - - /// Return the inner Store reference - pub const fn store(&self) -> &S { - &self.store - } - - /// Return the inner State reference - pub const fn state(&self) -> &SC { - &self.state - } -} - -impl Wallet -where - S: Store, - SC: StateClient, -{ - /// Retrieve the secret key with the given index. - pub fn phoenix_secret_key( - &self, - index: u8, - ) -> Result> { - self.store - .phoenix_secret_key(index) - .map_err(Error::from_store_err) - } - - /// Retrieve the public key with the given index. - pub fn phoenix_public_key( - &self, - index: u8, - ) -> Result> { - self.store - .phoenix_public_key(index) - .map_err(Error::from_store_err) - } - - /// Retrieve the account secret key with the given index. - pub fn account_secret_key( - &self, - index: u8, - ) -> Result> { - self.store - .account_secret_key(index) - .map_err(Error::from_store_err) - } - - /// Retrieve the account public key with the given index. - pub fn account_public_key( - &self, - index: u8, - ) -> Result> { - self.store - .account_public_key(index) - .map_err(Error::from_store_err) - } - - /// Fetches the notes and nullifiers in the state and returns the notes that - /// are still available for spending. - fn unspent_notes_and_nullifiers( - &self, - sk: &PhoenixSecretKey, - ) -> Result, Error> { - let vk = PhoenixViewKey::from(sk); - - let note_leaves = - self.state.fetch_notes(&vk).map_err(Error::from_state_err)?; - - let nullifiers: Vec<_> = note_leaves - .iter() - .map(|(note, _bh)| note.gen_nullifier(sk)) - .collect(); - - let existing_nullifiers = self - .state - .fetch_existing_nullifiers(&nullifiers) - .map_err(Error::from_state_err)?; - - let unspent_notes_and_nullifiers = note_leaves - .into_iter() - .zip(nullifiers.into_iter()) - .filter(|(_note, nullifier)| { - !existing_nullifiers.contains(nullifier) - }) - .map(|((note, block_height), nullifier)| { - (NoteLeaf { note, block_height }, nullifier) - }) - .collect(); - - Ok(unspent_notes_and_nullifiers) - } - - /// Here we fetch the notes and their nullifiers to cover the - /// transaction-costs. - #[allow(clippy::type_complexity)] - fn input_notes_nullifiers( - &self, - sender_sk: &PhoenixSecretKey, - transaction_cost: u64, - ) -> Result, Error> { - let sender_vk = PhoenixViewKey::from(sender_sk); - - // decrypt the value of all unspent note - let unspent_notes_nullifiers = - self.unspent_notes_and_nullifiers(sender_sk)?; - let mut notes_values_nullifiers = - Vec::with_capacity(unspent_notes_nullifiers.len()); - - let mut accumulated_value = 0; - for (note_leaf, nullifier) in unspent_notes_nullifiers { - let val = note_leaf - .note - .value(Some(&sender_vk)) - .map_err(|_| ExecutionError::PhoenixOwnership)?; - accumulated_value += val; - notes_values_nullifiers.push((note_leaf.note, val, nullifier)); - } - - if accumulated_value < transaction_cost { - return Err(ExecutionError::InsufficientBalance.into()); - } - - // pick the four smallest notes that cover the costs - let inputs = pick_notes(transaction_cost, notes_values_nullifiers); - - if inputs.is_empty() { - return Err(Error::NoteCombinationProblem); - } - - Ok(inputs) - } - - /// Here we fetch the notes, their openings and nullifiers to cover the - /// transfer-costs. - #[allow(clippy::type_complexity)] - fn input_notes_openings_nullifiers( - &self, - sender_sk: &PhoenixSecretKey, - transaction_cost: u64, - ) -> Result, Error> { - let notes_and_nullifiers = - self.input_notes_nullifiers(sender_sk, transaction_cost)?; - - let mut notes_openings_nullifiers = - Vec::with_capacity(notes_and_nullifiers.len()); - for (note, nullifier) in notes_and_nullifiers.into_iter() { - let opening = self - .state - .fetch_opening(¬e) - .map_err(Error::from_state_err)?; - notes_openings_nullifiers.push((note, opening, nullifier)); - } - - Ok(notes_openings_nullifiers) - } - - /// Here we fetch the notes and their openings to cover the - /// transfer-costs. - #[allow(clippy::type_complexity)] - fn input_notes_openings( - &self, - sender_sk: &PhoenixSecretKey, - transaction_cost: u64, - ) -> Result, Error> { - let notes_and_nullifiers = - self.input_notes_nullifiers(sender_sk, transaction_cost)?; - - let mut notes_openings = Vec::with_capacity(notes_and_nullifiers.len()); - for (note, _nullifier) in notes_and_nullifiers.into_iter() { - let opening = self - .state - .fetch_opening(¬e) - .map_err(Error::from_state_err)?; - notes_openings.push((note, opening)); - } - - Ok(notes_openings) - } - - /// Executes a generic contract call, paying gas from a public account. - #[allow(clippy::too_many_arguments)] - pub fn moonlight_execute( - &self, - sender_idx: u8, - transfer_value: u64, - deposit: u64, - gas_limit: u64, - gas_price: u64, - exec: Option>, - ) -> Result> { - let state = self.state(); - - let mut sender_sk = self.account_secret_key(sender_idx)?; - let sender = self.account_public_key(sender_idx)?; - - let account = state - .fetch_account(&sender) - .map_err(Error::from_state_err)?; - - // 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().map_err(Error::from_state_err)?; - - let tx = moonlight( - &sender_sk, - None, - transfer_value, - deposit, - gas_limit, - gas_price, - nonce, - chain_id, - exec, - )?; - - sender_sk.zeroize(); - Ok(tx) - } - - /// Execute a generic contract call or deployment, using Phoenix notes to - /// pay for gas. - #[allow(clippy::too_many_arguments)] - pub fn phoenix_execute( - &self, - rng: &mut Rng, - sender_index: u8, - gas_limit: u64, - gas_price: u64, - deposit: u64, - data: impl Into, - ) -> Result> { - let mut sender_sk = self.phoenix_secret_key(sender_index)?; - let receiver_pk = self.phoenix_public_key(sender_index)?; - let refund_pk = receiver_pk; - - let input_notes_openings = self.input_notes_openings( - &sender_sk, - gas_limit * gas_price + deposit, - )?; - - let root = self.state.fetch_root().map_err(Error::from_state_err)?; - - let transfer_value = 0; - let obfuscated_transaction = false; - - let chain_id = - self.state.fetch_chain_id().map_err(Error::from_state_err)?; - - let tx = phoenix_transaction( - rng, - &sender_sk, - &refund_pk, - &receiver_pk, - input_notes_openings, - root, - transfer_value, - obfuscated_transaction, - deposit, - gas_limit, - gas_price, - chain_id, - Some(data), - &LocalProver, - )?; - - sender_sk.zeroize(); - - Ok(tx) - } - - /// Transfer Dusk in the form of Phoenix notes from one key to another. - #[allow(clippy::too_many_arguments)] - pub fn phoenix_transfer( - &self, - rng: &mut Rng, - sender_index: u8, - receiver_pk: &PhoenixPublicKey, - transfer_value: u64, - gas_limit: u64, - gas_price: u64, - ) -> Result> { - let mut sender_sk = self.phoenix_secret_key(sender_index)?; - let refund_pk = self.phoenix_public_key(sender_index)?; - - let input_notes_openings = self.input_notes_openings( - &sender_sk, - transfer_value + gas_limit * gas_price, - )?; - - let root = self.state.fetch_root().map_err(Error::from_state_err)?; - - let obfuscated_transaction = true; - let deposit = 0; - - let data: Option = None; - - let chain_id = - self.state.fetch_chain_id().map_err(Error::from_state_err)?; - - let tx = phoenix_transaction( - rng, - &sender_sk, - &refund_pk, - &receiver_pk, - input_notes_openings, - root, - transfer_value, - obfuscated_transaction, - deposit, - gas_limit, - gas_price, - chain_id, - data, - &LocalProver, - )?; - - sender_sk.zeroize(); - - Ok(tx) - } - - /// Stakes an amount of Dusk using Phoenix notes. - #[allow(clippy::too_many_arguments)] - pub fn phoenix_stake( - &self, - rng: &mut Rng, - sender_index: u8, - staker_index: u8, - stake_value: u64, - gas_limit: u64, - gas_price: u64, - ) -> Result> { - let mut phoenix_sender_sk = self.phoenix_secret_key(sender_index)?; - let mut stake_sk = self.account_secret_key(staker_index)?; - - let inputs = self.input_notes_openings( - &phoenix_sender_sk, - gas_limit * gas_price + stake_value, - )?; - - let root = self.state.fetch_root().map_err(Error::from_state_err)?; - - let chain_id = - self.state.fetch_chain_id().map_err(Error::from_state_err)?; - - let tx = phoenix_stake( - rng, - &phoenix_sender_sk, - &stake_sk, - &stake_sk, - inputs, - root, - gas_limit, - gas_price, - chain_id, - stake_value, - &LocalProver, - )?; - - stake_sk.zeroize(); - phoenix_sender_sk.zeroize(); - - Ok(tx) - } - - /// Unstakes a key from the stake contract, using Phoenix notes. - pub fn phoenix_unstake( - &self, - rng: &mut Rng, - sender_index: u8, - staker_index: u8, - gas_limit: u64, - gas_price: u64, - ) -> Result> { - let mut phoenix_sender_sk = self.phoenix_secret_key(sender_index)?; - let mut stake_sk = self.account_secret_key(staker_index)?; - - let stake_pk = BlsPublicKey::from(&stake_sk); - - let inputs = self.input_notes_openings_nullifiers( - &phoenix_sender_sk, - gas_limit * gas_price, - )?; - - let root = self.state.fetch_root().map_err(Error::from_state_err)?; - - let stake = self - .state - .fetch_stake(&stake_pk) - .map_err(Error::from_state_err)?; - - let staked_amount = stake - .amount - .ok_or(Error::NotStaked { - key: stake_pk, - stake, - })? - .value; - - let chain_id = - self.state.fetch_chain_id().map_err(Error::from_state_err)?; - - let tx = phoenix_unstake( - rng, - &phoenix_sender_sk, - &stake_sk, - &stake_sk, - inputs, - root, - staked_amount, - gas_limit, - gas_price, - chain_id, - &LocalProver, - )?; - - stake_sk.zeroize(); - phoenix_sender_sk.zeroize(); - - Ok(tx) - } - - /// Withdraw the accumulated staking reward for a key, into Phoenix notes. - /// Rewards are accumulated by participating in the consensus. - pub fn phoenix_stake_withdraw( - &self, - rng: &mut Rng, - sender_index: u8, - staker_index: u8, - gas_limit: u64, - gas_price: u64, - ) -> Result> { - let mut phoenix_sender_sk = self.phoenix_secret_key(sender_index)?; - let mut stake_sk = self.account_secret_key(staker_index)?; - - let stake_pk = BlsPublicKey::from(&stake_sk); - - let inputs = self.input_notes_openings_nullifiers( - &phoenix_sender_sk, - gas_limit * gas_price, - )?; - - let root = self.state.fetch_root().map_err(Error::from_state_err)?; - - let stake_reward = self - .state - .fetch_stake(&stake_pk) - .map_err(Error::from_state_err)? - .reward; - - let chain_id = - self.state.fetch_chain_id().map_err(Error::from_state_err)?; - - let tx = phoenix_stake_reward( - rng, - &phoenix_sender_sk, - &stake_sk, - &stake_sk, - inputs, - root, - stake_reward, - gas_limit, - gas_price, - chain_id, - &LocalProver, - )?; - - stake_sk.zeroize(); - phoenix_sender_sk.zeroize(); - - Ok(tx) - } - - /// Convert some Phoenix Dusk into Moonlight Dusk. - pub fn phoenix_to_moonlight( - &self, - rng: &mut Rng, - phoenix_sender_index: u8, - moonlight_receiver_index: u8, - convert_value: u64, - gas_limit: u64, - gas_price: u64, - ) -> Result> { - let mut phoenix_sender_sk = - self.phoenix_secret_key(phoenix_sender_index)?; - let mut moonlight_receiver_sk = - self.account_secret_key(moonlight_receiver_index)?; - - let inputs = self.input_notes_openings_nullifiers( - &phoenix_sender_sk, - gas_limit * gas_price, - )?; - - let root = self.state.fetch_root().map_err(Error::from_state_err)?; - - let chain_id = - self.state.fetch_chain_id().map_err(Error::from_state_err)?; - - let tx = phoenix_to_moonlight( - rng, - &phoenix_sender_sk, - &moonlight_receiver_sk, - inputs, - root, - convert_value, - gas_limit, - gas_price, - chain_id, - &LocalProver, - )?; - - phoenix_sender_sk.zeroize(); - moonlight_receiver_sk.zeroize(); - - Ok(tx) - } - - /// Deploy a contract using Phoenix to pay for gas. - pub fn phoenix_deployment( - &self, - rng: &mut Rng, - sender_index: u8, - bytecode: impl Into>, - owner: &BlsPublicKey, - init_args: Vec, - nonce: u64, - gas_limit: u64, - gas_price: u64, - ) -> Result> { - let mut phoenix_sender_sk = self.phoenix_secret_key(sender_index)?; - - let inputs = self.input_notes_openings_nullifiers( - &phoenix_sender_sk, - gas_limit * gas_price, - )?; - - let root = self.state.fetch_root().map_err(Error::from_state_err)?; - - let chain_id = - self.state.fetch_chain_id().map_err(Error::from_state_err)?; - - let tx = phoenix_deployment( - rng, - &phoenix_sender_sk, - inputs, - root, - bytecode, - owner, - init_args, - nonce, - gas_limit, - gas_price, - chain_id, - &LocalProver, - )?; - - phoenix_sender_sk.zeroize(); - - Ok(tx) - } - - /// Transfer Dusk from one account to another using moonlight. - pub fn moonlight_transfer( - &self, - sender_index: u8, - receiver_account: BlsPublicKey, - value: u64, - gas_limit: u64, - gas_price: u64, - ) -> Result> { - let deposit = 0; - let data: Option = None; - - self.moonlight_transaction( - sender_index, - Some(receiver_account), - value, - deposit, - gas_limit, - gas_price, - data, - ) - } - - /// Creates a generic moonlight transaction. - #[allow(clippy::too_many_arguments)] - pub fn moonlight_transaction( - &self, - sender_index: u8, - receiver_account: Option, - value: u64, - deposit: u64, - gas_limit: u64, - gas_price: u64, - data: Option>, - ) -> Result> { - let mut seed = self.store.get_seed().map_err(Error::from_store_err)?; - let mut sender_sk = derive_bls_sk(&seed, sender_index); - let sender_account = BlsPublicKey::from(&sender_sk); - - let account = self - .state - .fetch_account(&sender_account) - .map_err(Error::from_state_err)?; - - // technically this check is not necessary, but it's nice to not spam - // the network with transactions that are unspendable. - let max_value = value + deposit + gas_limit * gas_price; - if max_value > account.balance { - return Err(ExecutionError::InsufficientBalance.into()); - } - let nonce = account.nonce + 1; - - let chain_id = - self.state.fetch_chain_id().map_err(Error::from_state_err)?; - - let tx = MoonlightTransaction::new( - &sender_sk, - receiver_account, - value, - deposit, - gas_limit, - gas_price, - nonce, - chain_id, - data, - )?; - - seed.zeroize(); - sender_sk.zeroize(); - - Ok(tx.into()) - } - - /// Stakes an amount of Dusk using a Moonlight account. - #[allow(clippy::too_many_arguments)] - pub fn moonlight_stake( - &self, - sender_index: u8, - staker_index: u8, - stake_value: u64, - gas_limit: u64, - gas_price: u64, - ) -> Result> { - let mut sender_sk = self.account_secret_key(sender_index)?; - let sender_pk = self.account_public_key(sender_index)?; - - let mut staker_sk = self.account_secret_key(staker_index)?; - - let sender_account = self - .state - .fetch_account(&sender_pk) - .map_err(Error::from_state_err)?; - - let chain_id = - self.state.fetch_chain_id().map_err(Error::from_state_err)?; - - let tx = moonlight_stake( - &sender_sk, - &staker_sk, - &staker_sk, - stake_value, - gas_limit, - gas_price, - sender_account.nonce + 1, - chain_id, - )?; - - sender_sk.zeroize(); - staker_sk.zeroize(); - - Ok(tx) - } - - /// Unstakes a key from the stake contract, using a Moonlight account. - pub fn moonlight_unstake( - &self, - rng: &mut Rng, - sender_index: u8, - staker_index: u8, - gas_limit: u64, - gas_price: u64, - ) -> Result> { - let mut sender_sk = self.account_secret_key(sender_index)?; - let sender_pk = self.account_public_key(sender_index)?; - - let mut staker_sk = self.account_secret_key(staker_index)?; - let staker_pk = self.account_public_key(staker_index)?; - - let sender_account = self - .state - .fetch_account(&sender_pk) - .map_err(Error::from_state_err)?; - let staker_data = self - .state - .fetch_stake(&staker_pk) - .map_err(Error::from_state_err)?; - - let unstake_value = staker_data - .amount - .ok_or(Error::NotStaked { - key: staker_pk, - stake: staker_data, - })? - .value; - let chain_id = - self.state.fetch_chain_id().map_err(Error::from_state_err)?; - - let tx = moonlight_unstake( - rng, - &sender_sk, - &staker_sk, - &staker_sk, - unstake_value, - gas_limit, - gas_price, - sender_account.nonce + 1, - chain_id, - )?; - - sender_sk.zeroize(); - staker_sk.zeroize(); - - Ok(tx) - } - - /// Withdraw the accumulated staking reward for a key, into a Moonlight - /// notes. Rewards are accumulated by participating in the consensus. - pub fn moonlight_stake_withdraw( - &self, - rng: &mut Rng, - sender_index: u8, - staker_index: u8, - gas_limit: u64, - gas_price: u64, - ) -> Result> { - let mut sender_sk = self.account_secret_key(sender_index)?; - let sender_pk = self.account_public_key(sender_index)?; - - let mut staker_sk = self.account_secret_key(staker_index)?; - let staker_pk = self.account_public_key(staker_index)?; - - let sender_account = self - .state - .fetch_account(&sender_pk) - .map_err(Error::from_state_err)?; - let staker_data = self - .state - .fetch_stake(&staker_pk) - .map_err(Error::from_state_err)?; - - let chain_id = - self.state.fetch_chain_id().map_err(Error::from_state_err)?; - - let tx = moonlight_stake_reward( - rng, - &sender_sk, - &staker_sk, - &staker_sk, - staker_data.reward, - gas_limit, - gas_price, - sender_account.nonce + 1, - chain_id, - )?; - - sender_sk.zeroize(); - staker_sk.zeroize(); - - Ok(tx) - } - - /// Convert some Moonlight Dusk into Phoenix Dusk. - pub fn moonlight_to_phoenix( - &self, - rng: &mut Rng, - moonlight_sender_index: u8, - phoenix_receiver_index: u8, - convert_value: u64, - gas_limit: u64, - gas_price: u64, - ) -> Result> { - let mut moonlight_sender_sk = - self.account_secret_key(moonlight_sender_index)?; - let moonlight_sender_pk = - self.account_public_key(moonlight_sender_index)?; - let mut phoenix_receiver_sk = - self.phoenix_secret_key(phoenix_receiver_index)?; - - let moonlight_sender_account = self - .state - .fetch_account(&moonlight_sender_pk) - .map_err(Error::from_state_err)?; - - let nonce = moonlight_sender_account.nonce + 1; - - let chain_id = - self.state.fetch_chain_id().map_err(Error::from_state_err)?; - - let tx = moonlight_to_phoenix( - rng, - &moonlight_sender_sk, - &phoenix_receiver_sk, - convert_value, - gas_limit, - gas_price, - nonce, - chain_id, - )?; - - moonlight_sender_sk.zeroize(); - phoenix_receiver_sk.zeroize(); - - Ok(tx) - } - - /// Deploy a contract using Moonlight to pay for gas. - pub fn moonlight_deployment( - &self, - sender_index: u8, - bytecode: impl Into>, - owner: &BlsPublicKey, - init_args: Vec, - gas_limit: u64, - gas_price: u64, - deploy_nonce: u64, - ) -> Result> { - let mut sender_sk = self.account_secret_key(sender_index)?; - let sender_pk = self.account_public_key(sender_index)?; - - let chain_id = - self.state.fetch_chain_id().map_err(Error::from_state_err)?; - - let moonlight_current_nonce = self - .state - .fetch_account(&sender_pk) - .map_err(Error::from_state_err)? - .nonce; - - let moonlight_nonce = moonlight_current_nonce + 1; - - let tx = moonlight_deployment( - &sender_sk, - bytecode, - owner, - init_args, - gas_limit, - gas_price, - moonlight_nonce, - deploy_nonce, - chain_id, - )?; - - sender_sk.zeroize(); - - Ok(tx) - } - - /// Gets the balance of a key. - pub fn get_balance( - &self, - sk_index: u8, - ) -> Result> { - let mut seed = self.store.get_seed().map_err(Error::from_store_err)?; - let mut phoenix_sk = derive_phoenix_sk(&seed, sk_index); - let phoenix_vk = PhoenixViewKey::from(&phoenix_sk); - - let unspent_notes: Vec = self - .unspent_notes_and_nullifiers(&phoenix_sk)? - .into_iter() - .map(|(note_leaf, _nul)| note_leaf) - .collect(); - let balance = phoenix_balance(&phoenix_vk, unspent_notes.iter()); - - seed.zeroize(); - phoenix_sk.zeroize(); - - Ok(balance) - } - - /// Gets the stake and the expiration of said stake for a key. - pub fn get_stake(&self, sk_index: u8) -> Result> { - let mut seed = self.store.get_seed().map_err(Error::from_store_err)?; - let mut account_sk = derive_bls_sk(&seed, sk_index); - - let account_pk = BlsPublicKey::from(&account_sk); - - let stake = self - .state - .fetch_stake(&account_pk) - .map_err(Error::from_state_err)?; - - seed.zeroize(); - account_sk.zeroize(); - - Ok(stake) - } - - /// Gets the account data for a key. - pub fn get_account( - &self, - sk_index: u8, - ) -> Result> { - let mut seed = self.store.get_seed().map_err(Error::from_store_err)?; - let mut account_sk = derive_bls_sk(&seed, sk_index); - - let account_pk = BlsPublicKey::from(&account_sk); - - let account = self - .state - .fetch_account(&account_pk) - .map_err(Error::from_state_err)?; - - seed.zeroize(); - account_sk.zeroize(); - - Ok(account) - } -} - -/// Pick the notes to be used in a phoenix 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_values_nullifiers: Vec<(Note, u64, BlsScalar)>, -) -> Vec<(Note, BlsScalar)> { - let mut notes_values_nullifiers = notes_values_nullifiers; - let len = notes_values_nullifiers.len(); - - if len <= MAX_INPUT_NOTES { - return notes_values_nullifiers - .into_iter() - .map(|(note, _value, nullifier)| (note, nullifier)) - .collect(); - } - - notes_values_nullifiers - .sort_by(|(_, aval, _), (_, bval, _)| aval.cmp(bval)); - - pick_lexicographic(notes_values_nullifiers.len(), |indices| { - indices - .iter() - .map(|index| ¬es_values_nullifiers[*index].1) - .sum::() - >= value - }) - .map(|indices| { - indices - .into_iter() - .map(|index| { - let (note, _value, nullifier) = - notes_values_nullifiers[index].clone(); - (note, nullifier) - }) - .collect() - }) - .unwrap_or_default() -} - -fn pick_lexicographic bool>( - max_len: usize, - is_valid: F, -) -> Option<[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 Some(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; - } - } - - None -} diff --git a/test-wallet/src/lib.rs b/test-wallet/src/lib.rs deleted file mode 100644 index ce399123d1..0000000000 --- a/test-wallet/src/lib.rs +++ /dev/null @@ -1,140 +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. - -//! The wallet specification. - -#![deny(missing_docs)] -#![deny(clippy::all)] -#![allow(clippy::result_large_err)] - -extern crate alloc; - -mod imp; - -use alloc::vec::Vec; - -use dusk_core::signatures::bls::{ - PublicKey as BlsPublicKey, SecretKey as BlsSecretKey, -}; -use dusk_core::stake::StakeData; -use dusk_core::transfer::moonlight::AccountData; -use dusk_core::transfer::phoenix::{ - Note, NoteOpening, PublicKey as PhoenixPublicKey, - SecretKey as PhoenixSecretKey, ViewKey as PhoenixViewKey, -}; -use dusk_core::BlsScalar; -use zeroize::Zeroize; - -pub use wallet_core::keys::{ - derive_bls_sk, derive_phoenix_pk, derive_phoenix_sk, -}; - -pub use imp::*; - -/// Stores the cryptographic material necessary to derive cryptographic keys. -pub trait Store { - /// The error type returned from the store. - type Error; - - /// Retrieves the seed used to derive keys. - fn get_seed(&self) -> Result<[u8; 64], Self::Error>; - - /// Retrieve the secret key with the given index. - fn phoenix_secret_key( - &self, - index: u8, - ) -> Result { - let mut seed = self.get_seed()?; - - let sk = derive_phoenix_sk(&seed, index); - - seed.zeroize(); - - Ok(sk) - } - - /// Retrieve the public key with the given index. - fn phoenix_public_key( - &self, - index: u8, - ) -> Result { - let mut seed = self.get_seed()?; - - let pk = derive_phoenix_pk(&seed, index); - - seed.zeroize(); - - Ok(pk) - } - - /// Retrieve the account secret key with the given index. - fn account_secret_key( - &self, - index: u8, - ) -> Result { - let mut seed = self.get_seed()?; - - let sk = derive_bls_sk(&seed, index); - - seed.zeroize(); - - Ok(sk) - } - - /// Retrieve the account public key with the given index. - fn account_public_key( - &self, - index: u8, - ) -> Result { - let mut seed = self.get_seed()?; - - let mut sk = derive_bls_sk(&seed, index); - let pk = BlsPublicKey::from(&sk); - - seed.zeroize(); - sk.zeroize(); - - Ok(pk) - } -} - -/// Types that are clients of the state API. -pub trait StateClient { - /// Error returned by the node client. - type Error; - - /// Find notes for a view key. - fn fetch_notes( - &self, - vk: &PhoenixViewKey, - ) -> Result, Self::Error>; - - /// Fetch the current root of the state. - fn fetch_root(&self) -> Result; - - /// Asks the node to return the nullifiers that already exist from the given - /// nullifiers. - fn fetch_existing_nullifiers( - &self, - nullifiers: &[BlsScalar], - ) -> Result, Self::Error>; - - /// Queries the node to find the opening for a specific note. - fn fetch_opening(&self, note: &Note) -> Result; - - /// Queries the node for the stake of a key. If the key has no stake, a - /// `Default` stake info should be returned. - fn fetch_stake(&self, pk: &BlsPublicKey) -> Result; - - /// Queries the account data for a given key. - fn fetch_account( - &self, - pk: &BlsPublicKey, - ) -> Result; - - /// Queries for the chain ID. - fn fetch_chain_id(&self) -> Result; -}