diff --git a/rusk-wallet/src/bin/command.rs b/rusk-wallet/src/bin/command.rs index 3e0233326..75fbce712 100644 --- a/rusk-wallet/src/bin/command.rs +++ b/rusk-wallet/src/bin/command.rs @@ -159,6 +159,10 @@ pub(crate) enum Command { #[arg(long)] address: Option
, + /// Owner of the stake [default: same Public address of the stake] + #[arg(long)] + owner: Option
, + /// Amount of DUSK to stake #[arg(short, long)] amt: Dusk, @@ -393,21 +397,28 @@ impl Command { } Command::Stake { address, + owner, amt, gas_limit, gas_price, } => { let address = address.unwrap_or(wallet.default_address()); let addr_idx = wallet.find_index(&address)?; + let owner_idx = + owner.map(|owner| wallet.find_index(&owner)).transpose()?; let gas = Gas::new(gas_limit).with_price(gas_price); let tx = match address { Address::Shielded(_) => { wallet.sync().await?; - wallet.phoenix_stake(addr_idx, amt, gas).await + wallet + .phoenix_stake(addr_idx, owner_idx, amt, gas) + .await } Address::Public(_) => { - wallet.moonlight_stake(addr_idx, amt, gas).await + wallet + .moonlight_stake(addr_idx, owner_idx, amt, gas) + .await } }?; diff --git a/rusk-wallet/src/bin/interactive.rs b/rusk-wallet/src/bin/interactive.rs index 129594d64..d3330d5c1 100644 --- a/rusk-wallet/src/bin/interactive.rs +++ b/rusk-wallet/src/bin/interactive.rs @@ -373,6 +373,7 @@ fn confirm(cmd: &Command, wallet: &Wallet) -> anyhow::Result { } Command::Stake { address, + owner, amt, gas_limit, gas_price, @@ -380,8 +381,10 @@ fn confirm(cmd: &Command, wallet: &Wallet) -> anyhow::Result { let sender = address.as_ref().ok_or(Error::BadAddress)?; let max_fee = gas_limit * gas_price; let stake_to = wallet.public_address(wallet.find_index(sender)?)?; + let owner = owner.as_ref().unwrap_or(&stake_to); println!(" > Pay with {}", sender.preview()); println!(" > Stake to {}", stake_to.preview()); + println!(" > Stake owner {}", owner.preview()); println!(" > Amount to stake = {} DUSK", amt); println!(" > Max fee = {} DUSK", Dusk::from(max_fee)); if let Address::Public(_) = sender { diff --git a/rusk-wallet/src/bin/interactive/command_menu.rs b/rusk-wallet/src/bin/interactive/command_menu.rs index f995787c7..3003027e4 100644 --- a/rusk-wallet/src/bin/interactive/command_menu.rs +++ b/rusk-wallet/src/bin/interactive/command_menu.rs @@ -14,7 +14,7 @@ use rusk_wallet::gas::{ DEFAULT_PRICE, GAS_PER_DEPLOY_BYTE, MIN_PRICE_DEPLOYMENT, }; use rusk_wallet::{ - Address, Wallet, MAX_CONTRACT_INIT_ARG_SIZE, MAX_FUNCTION_NAME_SIZE, + Address, Error, Wallet, MAX_CONTRACT_INIT_ARG_SIZE, MAX_FUNCTION_NAME_SIZE, }; use super::ProfileOp; @@ -162,8 +162,29 @@ pub(crate) async fn online( let mempool_gas_prices = wallet.get_mempool_gas_prices().await?; + let stake_idx = wallet + .find_index(&addr) + .expect("index to exists in interactive mode"); + let stake_pk = wallet + .public_key(stake_idx) + .expect("public key to exists in interactive mode"); + + let owner = match wallet.find_stake_owner_account(stake_pk).await { + Ok(account) => account, + Err(Error::NotStaked) => { + let choices = wallet + .profiles() + .iter() + .map(|p| Address::Public(p.public_addr)) + .collect(); + prompt::request_address(stake_idx, choices)? + } + e => e?, + }; + ProfileOp::Run(Box::new(Command::Stake { address: Some(addr), + owner: Some(owner), amt: prompt::request_stake_token_amt(balance)?, gas_limit: prompt::request_gas_limit(gas::DEFAULT_LIMIT_CALL)?, gas_price: prompt::request_gas_price( diff --git a/rusk-wallet/src/bin/io/prompt.rs b/rusk-wallet/src/bin/io/prompt.rs index ecae173c9..7498d2c19 100644 --- a/rusk-wallet/src/bin/io/prompt.rs +++ b/rusk-wallet/src/bin/io/prompt.rs @@ -354,6 +354,19 @@ pub(crate) fn request_transaction_model() -> anyhow::Result { ) } +/// Request transaction model to use +pub(crate) fn request_address( + current_idx: u8, + choices: Vec
, +) -> anyhow::Result
{ + Ok(Select::new( + "Please select the moonlight address to use as stake owner", + choices, + ) + .with_starting_cursor(current_idx as usize) + .prompt()?) +} + /// Request contract WASM file location pub(crate) fn request_contract_code() -> anyhow::Result { let validator = |path_str: &str| { diff --git a/rusk-wallet/src/clients.rs b/rusk-wallet/src/clients.rs index 2836efa92..368074d86 100644 --- a/rusk-wallet/src/clients.rs +++ b/rusk-wallet/src/clients.rs @@ -11,6 +11,7 @@ use std::sync::{Arc, Mutex}; use dusk_bytes::Serializable; use execution_core::signatures::bls::PublicKey as BlsPublicKey; +use execution_core::stake::{StakeFundOwner, StakeKeys}; use execution_core::transfer::moonlight::AccountData; use execution_core::transfer::phoenix::{Note, NoteLeaf, Prove}; use execution_core::transfer::Transaction; @@ -322,6 +323,33 @@ impl State { Ok(stake_data) } + /// Get the stake owner of a given stake account. + pub(crate) async fn fetch_stake_owner( + &self, + pk: &BlsPublicKey, + ) -> Result, Error> { + let status = self.status; + status("Fetching stake owner..."); + + // the target type of the deserialization has to match the return type + // of the contract-query + let stake_keys: Option = rkyv::from_bytes( + &self + .client + .contract_query::<_, _, 1024>( + STAKE_CONTRACT, + "get_stake_keys", + pk, + ) + .await?, + ) + .map_err(|_| Error::Rkyv)?; + + let stake_owner = stake_keys.map(|keys| keys.owner); + + Ok(stake_owner) + } + pub(crate) fn store(&self) -> &LocalStore { &self.store } diff --git a/rusk-wallet/src/wallet/transaction.rs b/rusk-wallet/src/wallet/transaction.rs index ae64f6951..cea494d5a 100644 --- a/rusk-wallet/src/wallet/transaction.rs +++ b/rusk-wallet/src/wallet/transaction.rs @@ -7,6 +7,7 @@ use std::fmt::Debug; use execution_core::signatures::bls::PublicKey as BlsPublicKey; +use execution_core::stake::StakeFundOwner; use execution_core::transfer::data::TransactionData; use execution_core::transfer::phoenix::PublicKey as PhoenixPublicKey; use execution_core::transfer::Transaction; @@ -20,7 +21,7 @@ use wallet_core::transaction::{ use zeroize::Zeroize; use super::file::SecureWalletFile; -use super::Wallet; +use super::{Address, Wallet}; use crate::clients::Prover; use crate::currency::Dusk; use crate::gas::Gas; @@ -236,6 +237,7 @@ impl Wallet { pub async fn phoenix_stake( &self, profile_idx: u8, + owner_idx: Option, amt: Dusk, gas: Gas, ) -> Result { @@ -263,6 +265,22 @@ impl Wallet { } } + let stake_owner_idx = match self.find_stake_owner_idx(stake_pk).await { + Ok(state_idx) => { + if let Some(owner_idx) = owner_idx { + if state_idx != owner_idx { + return Err(Error::Unauthorized); + } + } + state_idx + } + Err(Error::NotStaked) => owner_idx.unwrap_or(profile_idx), + Err(e) => { + return Err(e); + } + }; + let mut stake_owner_sk = self.derive_bls_sk(stake_owner_idx); + let tx_cost = amt + gas.limit * gas.price; let inputs = state .tx_input_notes(profile_idx, tx_cost) @@ -275,12 +293,22 @@ impl Wallet { let chain_id = state.fetch_chain_id().await?; let stake = phoenix_stake( - &mut rng, &sender_sk, &stake_sk, inputs, root, gas.limit, - gas.price, chain_id, amt, &Prover, + &mut rng, + &sender_sk, + &stake_sk, + &stake_owner_sk, + inputs, + root, + gas.limit, + gas.price, + chain_id, + amt, + &Prover, )?; sender_sk.zeroize(); stake_sk.zeroize(); + stake_owner_sk.zeroize(); let stake = state.prove(stake).await?; state.propagate(stake).await @@ -290,6 +318,7 @@ impl Wallet { pub async fn moonlight_stake( &self, profile_idx: u8, + owner_idx: Option, amt: Dusk, gas: Gas, ) -> Result { @@ -317,9 +346,26 @@ impl Wallet { } } + let stake_owner_idx = match self.find_stake_owner_idx(stake_pk).await { + Ok(state_idx) => { + if let Some(owner_idx) = owner_idx { + if state_idx != owner_idx { + return Err(Error::Unauthorized); + } + } + state_idx + } + Err(Error::NotStaked) => owner_idx.unwrap_or(profile_idx), + Err(e) => { + return Err(e); + } + }; + let mut stake_owner_sk = self.derive_bls_sk(stake_owner_idx); + let stake = moonlight_stake( &stake_sk, &stake_sk, + &stake_owner_sk, amt, gas.limit, gas.price, @@ -328,6 +374,7 @@ impl Wallet { )?; stake_sk.zeroize(); + stake_owner_sk.zeroize(); state.propagate(stake).await } @@ -344,9 +391,13 @@ impl Wallet { let mut sender_sk = self.derive_phoenix_sk(profile_idx); let mut stake_sk = self.derive_bls_sk(profile_idx); + let stake_pk = BlsPublicKey::from(&stake_sk); + + let stake_owner_idx = self.find_stake_owner_idx(&stake_pk).await?; + let mut stake_owner_sk = self.derive_bls_sk(stake_owner_idx); let unstake_value = state - .fetch_stake(&BlsPublicKey::from(&stake_sk)) + .fetch_stake(&stake_pk) .await? .and_then(|s| s.amount) .map(|s| s.total_funds()) @@ -366,6 +417,7 @@ impl Wallet { &mut rng, &sender_sk, &stake_sk, + &stake_owner_sk, inputs, root, unstake_value, @@ -377,6 +429,7 @@ impl Wallet { sender_sk.zeroize(); stake_sk.zeroize(); + stake_owner_sk.zeroize(); let unstake = state.prove(unstake).await?; state.propagate(unstake).await @@ -392,13 +445,13 @@ impl Wallet { let state = self.state()?; let mut stake_sk = self.derive_bls_sk(profile_idx); - let pk = self.public_key(profile_idx)?; + let stake_pk = self.public_key(profile_idx)?; let chain_id = state.fetch_chain_id().await?; - let account_nonce = state.fetch_account(pk).await?.nonce + 1; + let account_nonce = state.fetch_account(stake_pk).await?.nonce + 1; let unstake_value = state - .fetch_stake(pk) + .fetch_stake(stake_pk) .await? .and_then(|s| s.amount) .map(|s| s.total_funds()) @@ -408,10 +461,14 @@ impl Wallet { return Err(Error::NotStaked); } + let stake_owner_idx = self.find_stake_owner_idx(stake_pk).await?; + let mut stake_owner_sk = self.derive_bls_sk(stake_owner_idx); + let unstake = moonlight_unstake( &mut rng, &stake_sk, &stake_sk, + &stake_owner_sk, unstake_value, gas.limit, gas.price, @@ -420,6 +477,7 @@ impl Wallet { )?; stake_sk.zeroize(); + stake_owner_sk.zeroize(); state.propagate(unstake).await } @@ -442,16 +500,22 @@ impl Wallet { let root = state.fetch_root().await?; let chain_id = state.fetch_chain_id().await?; + let stake_pk = BlsPublicKey::from(&stake_sk); + let reward_amount = state - .fetch_stake(&BlsPublicKey::from(&stake_sk)) + .fetch_stake(&stake_pk) .await? .map(|s| s.reward) .unwrap_or(0); + let stake_owner_idx = self.find_stake_owner_idx(&stake_pk).await?; + let mut stake_owner_sk = self.derive_bls_sk(stake_owner_idx); + let withdraw = phoenix_stake_reward( &mut rng, &sender_sk, &stake_sk, + &stake_owner_sk, inputs, root, reward_amount, @@ -463,6 +527,7 @@ impl Wallet { sender_sk.zeroize(); stake_sk.zeroize(); + stake_owner_sk.zeroize(); let withdraw = state.prove(withdraw).await?; state.propagate(withdraw).await @@ -485,13 +550,22 @@ impl Wallet { let reward = Dusk::from(reward); let mut sender_sk = self.derive_bls_sk(sender_idx); + let mut stake_owner_sk = self.derive_bls_sk(sender_idx); let withdraw = moonlight_stake_reward( - &mut rng, &sender_sk, &sender_sk, *reward, gas.limit, gas.price, - nonce, chain_id, + &mut rng, + &sender_sk, + &sender_sk, + &stake_owner_sk, + *reward, + gas.limit, + gas.price, + nonce, + chain_id, )?; sender_sk.zeroize(); + stake_owner_sk.zeroize(); state.propagate(withdraw).await } @@ -644,4 +718,31 @@ impl Wallet { state.propagate(deploy).await } + + /// Finds the index of the stake owner account. + pub async fn find_stake_owner_idx( + &self, + stake_pk: &BlsPublicKey, + ) -> Result { + self.find_index(&self.find_stake_owner_account(stake_pk).await?) + } + + /// Finds the address of the stake owner account. + pub async fn find_stake_owner_account( + &self, + stake_pk: &BlsPublicKey, + ) -> Result { + let stake_owner = self + .state()? + .fetch_stake_owner(stake_pk) + .await? + .ok_or(Error::NotStaked)?; + + match stake_owner { + StakeFundOwner::Account(public_key) => { + Ok(Address::Public(public_key)) + } + StakeFundOwner::Contract(_) => Err(Error::Unauthorized), + } + } }