diff --git a/rusk/tests/config/stake.toml b/rusk/tests/config/stake.toml index 9aca8ee612..2eb458bd9b 100644 --- a/rusk/tests/config/stake.toml +++ b/rusk/tests/config/stake.toml @@ -14,6 +14,10 @@ notes = [ 10_000_000_000_000, ] +[[moonlight_account]] +address = "qe1FbZxf6YaCAeFNSvL1G82cBhG4Q4gBf4vKYo527Vws3b23jdbBuzKSFsdUHnZeBgsTnyNJLkApEpRyJw87sdzR9g9iESJrG5ZgpCs9jq88m6d4qMY5txGpaXskRQmkzE3" +balance = 10_000_000_000_000 + [[stake]] address = "qe1FbZxf6YaCAeFNSvL1G82cBhG4Q4gBf4vKYo527Vws3b23jdbBuzKSFsdUHnZeBgsTnyNJLkApEpRyJw87sdzR9g9iESJrG5ZgpCs9jq88m6d4qMY5txGpaXskRQmkzE3" amount = 1_000_000_000_000 diff --git a/rusk/tests/services/mod.rs b/rusk/tests/services/mod.rs index e75aadbe57..7c63583148 100644 --- a/rusk/tests/services/mod.rs +++ b/rusk/tests/services/mod.rs @@ -7,8 +7,9 @@ pub mod contract_deployment; pub mod conversion; pub mod gas_behavior; +pub mod moonlight_stake; pub mod multi_transfer; pub mod owner_calls; -pub mod stake; +pub mod phoenix_stake; pub mod transfer; pub mod unspendable; diff --git a/rusk/tests/services/moonlight_stake.rs b/rusk/tests/services/moonlight_stake.rs new file mode 100644 index 0000000000..1fdfb0bc5c --- /dev/null +++ b/rusk/tests/services/moonlight_stake.rs @@ -0,0 +1,240 @@ +// 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 std::path::Path; +use std::sync::{Arc, RwLock}; + +use execution_core::stake::MINIMUM_STAKE; + +use rand::prelude::*; +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::*; + +const BLOCK_HEIGHT: u64 = 1; +const BLOCK_GAS_LIMIT: u64 = 100_000_000_000; +const GAS_LIMIT: u64 = 10_000_000_000; +const GAS_PRICE: u64 = 1; + +// Creates the Rusk initial state for the tests below +fn stake_state>(dir: P) -> Result { + let snapshot = toml::from_str(include_str!("../config/stake.toml")) + .expect("Cannot deserialize config"); + + new_state(dir, &snapshot, BLOCK_GAS_LIMIT) +} + +/// Stakes an amount Dusk and produces a block with this single transaction, +/// checking the stake is set successfully. It then proceeds to withdraw the +/// stake and checking it is correctly withdrawn. +fn wallet_stake( + rusk: &Rusk, + wallet: &wallet::Wallet, + value: u64, +) { + let mut rng = StdRng::seed_from_u64(0xdead); + + wallet + .get_stake(0) + .expect("stakeinfo to be found") + .amount + .expect("stake amount to be found"); + + assert!( + wallet + .get_stake(2) + .expect("stakeinfo to be found") + .amount + .is_none(), + "stake amount not to be found" + ); + + let tx = wallet + .moonlight_stake(0, 2, value, GAS_LIMIT, GAS_PRICE) + .expect("Failed to create a stake transaction"); + let executed_txs = generator_procedure( + rusk, + &[tx], + BLOCK_HEIGHT, + BLOCK_GAS_LIMIT, + vec![], + None, + ) + .expect("generator procedure to succeed"); + if let Some(e) = &executed_txs + .first() + .expect("Transaction must be executed") + .err + { + panic!("Stake transaction failed due to {e}") + } + + let stake = wallet.get_stake(2).expect("stake to be found"); + let stake_value = stake.amount.expect("stake should have an amount").value; + + assert_eq!(stake_value, value); + + wallet + .get_stake(0) + .expect("stakeinfo to be found") + .amount + .expect("stake amount to be found"); + + let tx = wallet + .moonlight_unstake(&mut rng, 0, 0, GAS_LIMIT, GAS_PRICE) + .expect("Failed to unstake"); + let spent_txs = generator_procedure( + rusk, + &[tx], + BLOCK_HEIGHT, + BLOCK_GAS_LIMIT, + vec![], + None, + ) + .expect("generator procedure to succeed"); + let spent_tx = spent_txs.first().expect("Unstake tx to be included"); + assert_eq!(spent_tx.err, None, "unstake to be successfull"); + + let stake = wallet.get_stake(0).expect("stake should still be state"); + assert_eq!(stake.amount, None); + + let tx = wallet + .moonlight_stake_withdraw(&mut rng, 0, 1, GAS_LIMIT, GAS_PRICE) + .expect("failed to withdraw reward"); + generator_procedure( + rusk, + &[tx], + BLOCK_HEIGHT, + BLOCK_GAS_LIMIT, + vec![], + None, + ) + .expect("generator procedure to succeed"); + + let stake = wallet.get_stake(1).expect("stake should still be state"); + assert_eq!(stake.reward, 0); +} + +#[tokio::test(flavor = "multi_thread")] +pub async fn stake() -> Result<()> { + // Setup the logger + logger(); + + let tmp = tempdir().expect("Should be able to create temporary directory"); + let rusk = stake_state(&tmp)?; + + let cache = Arc::new(RwLock::new(HashMap::new())); + + // Create a wallet + let wallet = wallet::Wallet::new( + TestStore, + TestStateClient { + rusk: rusk.clone(), + cache, + }, + ); + + let original_root = rusk.state_root(); + + info!("Original Root: {:?}", hex::encode(original_root)); + + // Perform some staking actions. + wallet_stake(&rusk, &wallet, MINIMUM_STAKE); + + // Check the state's root is changed from the original one + let new_root = rusk.state_root(); + info!( + "New root after the 1st transfer: {:?}", + hex::encode(new_root) + ); + assert_ne!(original_root, new_root, "Root should have changed"); + + // let recv = kadcast_recv.try_recv(); + // let (_, _, h) = recv.expect("Transaction has not been locally + // propagated"); assert_eq!(h, 0, "Transaction locally propagated with + // wrong height"); + + Ok(()) +} + +/// Attempt to submit a management transaction intending it to fail. Verify that +/// the reward amount remains unchanged and confirm that the transaction indeed +/// fails +fn wallet_reward( + rusk: &Rusk, + wallet: &wallet::Wallet, +) { + let mut rng = StdRng::seed_from_u64(0xdead); + + let stake = wallet.get_stake(2).expect("stake to be found"); + assert_eq!(stake.reward, 0, "stake reward must be empty"); + + let tx = wallet + .moonlight_stake_withdraw(&mut rng, 0, 2, GAS_LIMIT, GAS_PRICE) + .expect("Creating reward transaction should succeed"); + + let executed_txs = generator_procedure( + rusk, + &[tx], + BLOCK_HEIGHT, + BLOCK_GAS_LIMIT, + vec![], + None, + ) + .expect("generator procedure to succeed"); + let _ = executed_txs + .first() + .expect("Transaction must be executed") + .err + .as_ref() + .expect("reward transaction to fail"); + let stake = wallet.get_stake(2).expect("stake to be found"); + assert_eq!(stake.reward, 0, "stake reward must be empty"); +} + +#[tokio::test(flavor = "multi_thread")] +pub async fn reward() -> Result<()> { + // Setup the logger + logger(); + + let tmp = tempdir().expect("Should be able to create temporary directory"); + let rusk = stake_state(&tmp)?; + + let cache = Arc::new(RwLock::new(HashMap::new())); + + // Create a wallet + let wallet = wallet::Wallet::new( + TestStore, + TestStateClient { + rusk: rusk.clone(), + cache, + }, + ); + + let original_root = rusk.state_root(); + + info!("Original Root: {:?}", hex::encode(original_root)); + + // Perform some staking actions. + wallet_reward(&rusk, &wallet); + + // Check the state's root is changed from the original one + let new_root = rusk.state_root(); + info!( + "New root after the 1st transfer: {:?}", + hex::encode(new_root) + ); + assert_ne!(original_root, new_root, "Root should have changed"); + + Ok(()) +} diff --git a/rusk/tests/services/stake.rs b/rusk/tests/services/phoenix_stake.rs similarity index 100% rename from rusk/tests/services/stake.rs rename to rusk/tests/services/phoenix_stake.rs diff --git a/test-wallet/src/imp.rs b/test-wallet/src/imp.rs index f1564c8593..7340ef3826 100644 --- a/test-wallet/src/imp.rs +++ b/test-wallet/src/imp.rs @@ -38,7 +38,8 @@ use wallet_core::{ keys::{derive_bls_sk, derive_phoenix_sk}, phoenix_balance, transaction::{ - moonlight_to_phoenix, phoenix as phoenix_transaction, phoenix_stake, + moonlight_stake, moonlight_stake_reward, moonlight_to_phoenix, + moonlight_unstake, phoenix as phoenix_transaction, phoenix_stake, phoenix_stake_reward, phoenix_to_moonlight, phoenix_unstake, }, BalanceInfo, @@ -711,6 +712,147 @@ where 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 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( + &sender_sk, + &staker_sk, + stake_value, + gas_limit, + gas_price, + sender_account.nonce, + staker_data.nonce, + 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, + unstake_value, + gas_limit, + gas_price, + sender_account.nonce, + 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_data.reward, + gas_limit, + gas_price, + sender_account.nonce, + chain_id, + )?; + + sender_sk.zeroize(); + staker_sk.zeroize(); + + Ok(tx) + } + /// Convert some Moonlight Dusk into Phoenix Dusk. pub fn moonlight_to_phoenix( &self, @@ -733,7 +875,7 @@ where .fetch_account(&moonlight_sender_pk) .map_err(Error::from_state_err)?; - let nonce = moonlight_sender_account.nonce + 1; + let nonce = moonlight_sender_account.nonce; let chain_id = self.state.fetch_chain_id().map_err(Error::from_state_err)?; diff --git a/wallet-core/src/transaction.rs b/wallet-core/src/transaction.rs index 75f46a79f8..51f9a313c0 100644 --- a/wallet-core/src/transaction.rs +++ b/wallet-core/src/transaction.rs @@ -84,7 +84,8 @@ pub fn phoenix( .into()) } -/// Creates a totally generic Moonlight transaction, all fields being variable +/// Creates a totally generic Moonlight [`Transaction`], all fields being +/// variable. /// /// # Errors /// The creation of a transaction is not possible and will error if: @@ -125,7 +126,6 @@ pub fn moonlight( /// - the `inputs` vector contains duplicate `Note`s /// - the `Prove` trait is implemented incorrectly #[allow(clippy::too_many_arguments)] -#[allow(clippy::missing_panics_doc)] pub fn phoenix_stake( rng: &mut R, phoenix_sender_sk: &PhoenixSecretKey, @@ -145,8 +145,9 @@ pub fn phoenix_stake( let transfer_value = 0; let obfuscated_transaction = false; let deposit = stake_value; + let nonce = current_nonce + 1; - let stake = Stake::new(stake_sk, stake_value, current_nonce + 1, chain_id); + let stake = Stake::new(stake_sk, stake_value, nonce, chain_id); let contract_call = ContractCall::new(STAKE_CONTRACT, "stake", &stake)?; @@ -168,6 +169,44 @@ pub fn phoenix_stake( ) } +/// Create a [`Transaction`] to stake from a Moonlight account. +/// +/// # Errors +/// The creation of this transaction doesn't error, but still returns a result +/// for the sake of API consistency. +#[allow(clippy::too_many_arguments)] +pub fn moonlight_stake( + moonlight_sender_sk: &BlsSecretKey, + stake_sk: &BlsSecretKey, + stake_value: u64, + gas_limit: u64, + gas_price: u64, + moonlight_current_nonce: u64, + stake_current_nonce: u64, + chain_id: u8, +) -> Result { + let transfer_value = 0; + let deposit = stake_value; + let moonlight_nonce = moonlight_current_nonce + 1; + let stake_nonce = stake_current_nonce + 1; + + let stake = Stake::new(stake_sk, stake_value, stake_nonce, chain_id); + + let contract_call = ContractCall::new(STAKE_CONTRACT, "stake", &stake)?; + + moonlight( + moonlight_sender_sk, + None, + transfer_value, + deposit, + gas_limit, + gas_price, + moonlight_nonce, + chain_id, + Some(contract_call), + ) +} + /// Create an unproven [`Transaction`] to withdraw stake rewards into a /// phoenix-note. /// @@ -179,7 +218,6 @@ pub fn phoenix_stake( /// - the `inputs` vector contains duplicate `Note`s /// - the `Prove` trait is implemented incorrectly #[allow(clippy::too_many_arguments)] -#[allow(clippy::missing_panics_doc)] pub fn phoenix_stake_reward( rng: &mut R, phoenix_sender_sk: &PhoenixSecretKey, @@ -237,6 +275,49 @@ pub fn phoenix_stake_reward( ) } +/// Create a [`Transaction`] to withdraw stake rewards into Moonlight account. +/// +/// # Errors +/// The creation of this transaction doesn't error, but still returns a result +/// for the sake of API consistency. +#[allow(clippy::too_many_arguments)] +pub fn moonlight_stake_reward( + rng: &mut R, + moonlight_sender_sk: &BlsSecretKey, + stake_sk: &BlsSecretKey, + reward_amount: u64, + gas_limit: u64, + gas_price: u64, + current_nonce: u64, + chain_id: u8, +) -> Result { + let transfer_value = 0; + let deposit = 0; + let nonce = current_nonce + 1; + + let gas_payment_token = WithdrawReplayToken::Moonlight(nonce); + + let contract_call = stake_reward_to_moonlight( + rng, + moonlight_sender_sk, + stake_sk, + gas_payment_token, + reward_amount, + )?; + + moonlight( + moonlight_sender_sk, + None, + transfer_value, + deposit, + gas_limit, + gas_price, + nonce, + chain_id, + Some(contract_call), + ) +} + /// Create an unproven [`Transaction`] to unstake into a phoenix-note. /// /// # Errors @@ -247,7 +328,6 @@ pub fn phoenix_stake_reward( /// - the `inputs` vector contains duplicate `Note`s /// - the `Prove` trait is implemented incorrectly #[allow(clippy::too_many_arguments)] -#[allow(clippy::missing_panics_doc)] pub fn phoenix_unstake( rng: &mut R, phoenix_sender_sk: &PhoenixSecretKey, @@ -305,6 +385,49 @@ pub fn phoenix_unstake( ) } +/// Create a [`Transaction`] to unstake into a Moonlight account. +/// +/// # Errors +/// The creation of a transaction is not possible and will error if: +/// - the Memo provided with `data` is too large +#[allow(clippy::too_many_arguments)] +pub fn moonlight_unstake( + rng: &mut R, + moonlight_sender_sk: &BlsSecretKey, + stake_sk: &BlsSecretKey, + unstake_value: u64, + gas_limit: u64, + gas_price: u64, + current_nonce: u64, + chain_id: u8, +) -> Result { + let transfer_value = 0; + let deposit = 0; + let nonce = current_nonce + 1; + + let gas_payment_token = WithdrawReplayToken::Moonlight(nonce); + + let contract_call = unstake_to_moonlight( + rng, + moonlight_sender_sk, + stake_sk, + gas_payment_token, + unstake_value, + )?; + + moonlight( + moonlight_sender_sk, + None, + transfer_value, + deposit, + gas_limit, + gas_price, + nonce, + chain_id, + Some(contract_call), + ) +} + /// Create an unproven [`Transaction`] to convert Phoenix Dusk into Moonlight /// Dusk. /// @@ -319,7 +442,6 @@ pub fn phoenix_unstake( /// - the `inputs` vector contains duplicate `Note`s /// - the `Prove` trait is implemented incorrectly #[allow(clippy::too_many_arguments)] -#[allow(clippy::missing_panics_doc)] pub fn phoenix_to_moonlight( rng: &mut R, phoenix_sender_sk: &PhoenixSecretKey, @@ -393,12 +515,13 @@ pub fn moonlight_to_phoenix( convert_value: u64, gas_limit: u64, gas_price: u64, - nonce: u64, + current_nonce: u64, chain_id: u8, ) -> Result { let transfer_value = 0; let deposit = convert_value; // a convertion is a simultaneous deposit to *and* withdrawal from the // transfer contract + let nonce = current_nonce + 1; let gas_payment_token = WithdrawReplayToken::Moonlight(nonce); @@ -442,6 +565,26 @@ fn stake_reward_to_phoenix( ContractCall::new(STAKE_CONTRACT, "withdraw", &reward_withdraw) } +fn stake_reward_to_moonlight( + rng: &mut R, + moonlight_receiver_sk: &BlsSecretKey, + stake_sk: &BlsSecretKey, + gas_payment_token: WithdrawReplayToken, + reward_amount: u64, +) -> Result { + let withdraw = withdraw_to_moonlight( + rng, + moonlight_receiver_sk, + STAKE_CONTRACT, + gas_payment_token, + reward_amount, + ); + + let reward_withdraw = StakeWithdraw::new(stake_sk, withdraw); + + ContractCall::new(STAKE_CONTRACT, "withdraw", &reward_withdraw) +} + fn unstake_to_phoenix( rng: &mut R, phoenix_sender_sk: &PhoenixSecretKey, @@ -462,6 +605,26 @@ fn unstake_to_phoenix( ContractCall::new(STAKE_CONTRACT, "unstake", &unstake) } +fn unstake_to_moonlight( + rng: &mut R, + moonlight_receiver_sk: &BlsSecretKey, + stake_sk: &BlsSecretKey, + gas_payment_token: WithdrawReplayToken, + unstake_value: u64, +) -> Result { + let withdraw = withdraw_to_moonlight( + rng, + moonlight_receiver_sk, + STAKE_CONTRACT, + gas_payment_token, + unstake_value, + ); + + let unstake = StakeWithdraw::new(stake_sk, withdraw); + + ContractCall::new(STAKE_CONTRACT, "unstake", &unstake) +} + fn convert_to_moonlight( rng: &mut R, moonlight_receiver_sk: &BlsSecretKey,