From d500a595c2f2e2226dde153fd5da2b32fbb69cde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Leegwater=20Sim=C3=B5es?= Date: Fri, 6 Sep 2024 15:01:11 +0200 Subject: [PATCH 1/3] execution-core: add `Reward` and `RewardReason` Allows for the host to call the stake contract with a vector of `Reward` to rewards multiple accounts simultaneously. --- execution-core/CHANGELOG.md | 2 ++ execution-core/src/stake.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/execution-core/CHANGELOG.md b/execution-core/CHANGELOG.md index 413f91d0e0..260809b2aa 100644 --- a/execution-core/CHANGELOG.md +++ b/execution-core/CHANGELOG.md @@ -100,6 +100,8 @@ stake::{ StakeData; StakeEvent; Withdraw; + Reward; + RewardReason; EPOCH; STAKE_CONTRACT; STAKE_WARNINGS; diff --git a/execution-core/src/stake.rs b/execution-core/src/stake.rs index eb6066c665..adbd3f93ba 100644 --- a/execution-core/src/stake.rs +++ b/execution-core/src/stake.rs @@ -457,3 +457,30 @@ impl Serializable for StakeAmount { buf } } + +/// Used in a `reward` call to reward a given account with an amount of Dusk, +/// and emitted as an event, once a reward succeeds. +#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)] +#[archive_attr(derive(CheckBytes))] +pub struct Reward { + /// The account to be rewarded. + pub account: BlsPublicKey, + /// The amount to reward. + pub value: u64, + /// The reason for the reward. + pub reason: RewardReason, +} + +/// The reason that a reward is issued. +#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)] +#[archive_attr(derive(CheckBytes))] +pub enum RewardReason { + /// The fixed amount awarded to a generator. + GeneratorFixed, + /// Extra amount awarded to a generator. + GeneratorExtra, + /// Amount awarded to a voter. + Voter, + /// Amount awarded for another reason, such as rewarding Dusk. + Other, +} From e17101d0dc5886373da981105639cc67c5d666e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Leegwater=20Sim=C3=B5es?= Date: Fri, 6 Sep 2024 15:05:19 +0200 Subject: [PATCH 2/3] stake-contract: change `reward` to take `Vec` --- contracts/stake/src/lib.rs | 4 ++-- contracts/stake/src/state.rs | 24 +++++++++++++-------- contracts/stake/tests/common/assert.rs | 9 +++++++- contracts/stake/tests/events.rs | 30 +++++++++++++++----------- contracts/stake/tests/stake.rs | 18 ++++++++++------ 5 files changed, 53 insertions(+), 32 deletions(-) diff --git a/contracts/stake/src/lib.rs b/contracts/stake/src/lib.rs index 1237da3170..3bc2d29d35 100644 --- a/contracts/stake/src/lib.rs +++ b/contracts/stake/src/lib.rs @@ -92,9 +92,9 @@ unsafe fn insert_stake(arg_len: u32) -> u32 { #[no_mangle] unsafe fn reward(arg_len: u32) -> u32 { - rusk_abi::wrap_call(arg_len, |(pk, value)| { + rusk_abi::wrap_call(arg_len, |arg| { assert_external_caller(); - STATE.reward(&pk, value); + STATE.reward(arg); }) } diff --git a/contracts/stake/src/state.rs b/contracts/stake/src/state.rs index d46afddf9e..84b3a5e4b2 100644 --- a/contracts/stake/src/state.rs +++ b/contracts/stake/src/state.rs @@ -5,6 +5,8 @@ // Copyright (c) DUSK NETWORK. All rights reserved. use alloc::collections::BTreeMap; +use alloc::vec::Vec; + use core::cmp::min; use dusk_bytes::Serializable; @@ -12,7 +14,7 @@ use dusk_bytes::Serializable; use execution_core::{ signatures::bls::PublicKey as BlsPublicKey, stake::{ - next_epoch, Stake, StakeAmount, StakeData, StakeEvent, + next_epoch, Reward, Stake, StakeAmount, StakeData, StakeEvent, StakeWithReceiverEvent, Withdraw, EPOCH, MINIMUM_STAKE, STAKE_CONTRACT, STAKE_WARNINGS, }, @@ -263,19 +265,23 @@ impl StakeState { /// Rewards a `account` with the given `value`. If a stake does not exist /// in the map for the key one will be created. - pub fn reward(&mut self, account: &BlsPublicKey, value: u64) { + pub fn reward(&mut self, rewards: Vec) { + // since we assure that reward is called at least once per block, this + // call is necessary to ensure that there are no inconsistencies in + // `prev_block_state`. self.check_new_block(); - let stake = self.load_or_create_stake_mut(account); + for reward in &rewards { + let stake = self.load_or_create_stake_mut(&reward.account); - // Reset faults counters - stake.faults = 0; - stake.hard_faults = 0; + // Reset faults counters + stake.faults = 0; + stake.hard_faults = 0; - stake.reward += value; + stake.reward += reward.value; + } - let account = *account; - rusk_abi::emit("reward", StakeEvent { account, value }); + rusk_abi::emit("reward", rewards); } /// Total amount burned since the genesis diff --git a/contracts/stake/tests/common/assert.rs b/contracts/stake/tests/common/assert.rs index 9b3cf0b0ac..bbbac2580e 100644 --- a/contracts/stake/tests/common/assert.rs +++ b/contracts/stake/tests/common/assert.rs @@ -9,7 +9,7 @@ use rkyv::{check_archived_root, Deserialize, Infallible}; use execution_core::{ signatures::bls::PublicKey as BlsPublicKey, - stake::{StakeEvent, StakeWithReceiverEvent}, + stake::{Reward, StakeEvent, StakeWithReceiverEvent}, Event, }; @@ -37,6 +37,13 @@ pub fn assert_event( .expect("Infallible"); assert_eq!(staking_event_data.value, should_amount); assert_eq!(staking_event_data.account.to_bytes(), should_pk.to_bytes()); + } else if topic == "reward" { + let reward_event_data = rkyv::from_bytes::>(&event.data) + .expect("Reward event data should deserialize correctly"); + + assert!(reward_event_data.iter().any(|reward| { + &reward.account == should_pk && reward.value == should_amount + })) } else { let staking_event_data = check_archived_root::(event.data.as_slice()) diff --git a/contracts/stake/tests/events.rs b/contracts/stake/tests/events.rs index 780d0b08ff..0bc0ccaa25 100644 --- a/contracts/stake/tests/events.rs +++ b/contracts/stake/tests/events.rs @@ -12,7 +12,7 @@ use rand::SeedableRng; use execution_core::{ dusk, signatures::bls::{PublicKey as BlsPublicKey, SecretKey as BlsSecretKey}, - stake::{StakeAmount, StakeData, STAKE_CONTRACT}, + stake::{Reward, RewardReason, StakeAmount, StakeData, STAKE_CONTRACT}, transfer::{ phoenix::{ PublicKey as PhoenixPublicKey, SecretKey as PhoenixSecretKey, @@ -72,12 +72,14 @@ fn reward_slash() -> Result<(), PiecrustError> { u64::MAX, )?; - let receipt = session.call::<_, ()>( - STAKE_CONTRACT, - "reward", - &(stake_pk, reward_amount), - u64::MAX, - )?; + let rewards = vec![Reward { + account: stake_pk, + value: reward_amount, + reason: RewardReason::Other, + }]; + + let receipt = + session.call::<_, ()>(STAKE_CONTRACT, "reward", &rewards, u64::MAX)?; assert_event(&receipt.events, "reward", &stake_pk, reward_amount); let receipt = session.call::<_, ()>( @@ -197,12 +199,14 @@ fn stake_hard_slash() -> Result<(), PiecrustError> { assert_event(&receipt.events, "hard_slash", &stake_pk, hard_slash_amount); cur_balance -= hard_slash_amount; - let receipt = session.call::<_, ()>( - STAKE_CONTRACT, - "reward", - &(stake_pk, reward_amount), - u64::MAX, - )?; + let rewards = vec![Reward { + account: stake_pk, + value: reward_amount, + reason: RewardReason::Other, + }]; + + let receipt = + session.call::<_, ()>(STAKE_CONTRACT, "reward", &rewards, u64::MAX)?; assert_event(&receipt.events, "reward", &stake_pk, reward_amount); // Simple hard fault post-reward (slash 10%) diff --git a/contracts/stake/tests/stake.rs b/contracts/stake/tests/stake.rs index 8c8d4d72b2..9834c00e4f 100644 --- a/contracts/stake/tests/stake.rs +++ b/contracts/stake/tests/stake.rs @@ -13,7 +13,10 @@ use rand::SeedableRng; use execution_core::{ dusk, signatures::bls::{PublicKey as BlsPublicKey, SecretKey as BlsSecretKey}, - stake::{Stake, StakeData, Withdraw as StakeWithdraw, STAKE_CONTRACT}, + stake::{ + Reward, RewardReason, Stake, StakeData, Withdraw as StakeWithdraw, + STAKE_CONTRACT, + }, transfer::{ data::ContractCall, phoenix::{ @@ -132,13 +135,14 @@ fn stake_withdraw_unstake() { const REWARD_AMOUNT: u64 = dusk(5.0); + let rewards = vec![Reward { + account: stake_pk, + value: REWARD_AMOUNT, + reason: RewardReason::Other, + }]; + let receipt = session - .call::<_, ()>( - STAKE_CONTRACT, - "reward", - &(stake_pk, REWARD_AMOUNT), - POINT_LIMIT, - ) + .call::<_, ()>(STAKE_CONTRACT, "reward", &rewards, POINT_LIMIT) .expect("Rewarding a key should succeed"); assert_event(&receipt.events, "reward", &stake_pk, REWARD_AMOUNT); From a62a59fa85781f61b627c5208efe3bc8df2ad438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Leegwater=20Sim=C3=B5es?= Date: Fri, 6 Sep 2024 15:06:06 +0200 Subject: [PATCH 3/3] rusk: handle changed `reward` call The call now takes a `Vec` and emits all of the rewards simultaneously as a vector. --- rusk/src/lib/node/rusk.rs | 105 +++++++++------------ rusk/tests/services/contract_deployment.rs | 2 +- rusk/tests/services/owner_calls.rs | 2 +- 3 files changed, 49 insertions(+), 60 deletions(-) diff --git a/rusk/src/lib/node/rusk.rs b/rusk/src/lib/node/rusk.rs index 39e3095c1b..8b46b47e03 100644 --- a/rusk/src/lib/node/rusk.rs +++ b/rusk/src/lib/node/rusk.rs @@ -15,7 +15,7 @@ use sha3::{Digest, Sha3_256}; use tokio::task; use tracing::{debug, info, warn}; -use dusk_bytes::{DeserializableSlice, Serializable}; +use dusk_bytes::DeserializableSlice; use dusk_consensus::config::{ ratification_extra, ratification_quorum, validation_extra, validation_quorum, RATIFICATION_COMMITTEE_CREDITS, @@ -24,7 +24,7 @@ use dusk_consensus::config::{ use dusk_consensus::operations::{CallParams, VerificationOutput, Voter}; use execution_core::{ signatures::bls::PublicKey as BlsPublicKey, - stake::{StakeData, STAKE_CONTRACT}, + stake::{Reward, RewardReason, StakeData, STAKE_CONTRACT}, transfer::{ data::{ContractBytecode, ContractDeploy}, moonlight::AccountData, @@ -699,12 +699,8 @@ fn reward_slash_and_update_root( slashing: Vec, voters: Option<&[Voter]>, ) -> Result> { - let ( - dusk_value, - generator_fixed_reward, - generator_extra_reward, - voters_reward, - ) = coinbase_value(block_height, dusk_spent); + let (dusk_value, generator_reward, generator_extra_reward, voters_reward) = + coinbase_value(block_height, dusk_spent); let credits = voters .unwrap_or_default() @@ -716,40 +712,43 @@ fn reward_slash_and_update_root( return Err(InvalidCreditsCount(block_height, 0)); } - let r = session.call::<_, ()>( - STAKE_CONTRACT, - "reward", - &(*DUSK_KEY, dusk_value), - u64::MAX, - )?; - - let mut events = r.events; + let generator_extra_reward = + calc_generator_extra_reward(generator_extra_reward, credits); - debug!( - event = "Dusk rewarded", - voter = to_bs58(&DUSK_KEY), - reward = dusk_value - ); + // We first start with only the generator (fixed) and Dusk + let mut num_rewards = 2; - let generator_curr_extra_reward = - calc_generator_extra_reward(generator_extra_reward, credits); + // If there is an extra reward we add it. + if generator_extra_reward != 0 { + num_rewards += 1; + } - let generator_reward = generator_fixed_reward + generator_curr_extra_reward; - let r = session.call::<_, ()>( - STAKE_CONTRACT, - "reward", - &(*generator, generator_reward), - u64::MAX, - )?; - events.extend(r.events); + // Additionally we also reward the voters. + if let Some(voters) = &voters { + num_rewards += voters.len(); + } - debug!( - event = "generator rewarded", - generator = to_bs58(generator), - total_reward = generator_reward, - extra_reward = generator_curr_extra_reward, - credits, - ); + let mut rewards = Vec::with_capacity(num_rewards); + + rewards.push(Reward { + account: *generator, + value: generator_reward, + reason: RewardReason::GeneratorFixed, + }); + + rewards.push(Reward { + account: *DUSK_KEY, + value: dusk_value, + reason: RewardReason::Other, + }); + + if generator_extra_reward != 0 { + rewards.push(Reward { + account: *generator, + value: generator_extra_reward, + reason: RewardReason::GeneratorExtra, + }); + } let credit_reward = voters_reward / (VALIDATION_COMMITTEE_CREDITS + RATIFICATION_COMMITTEE_CREDITS) @@ -758,22 +757,18 @@ fn reward_slash_and_update_root( for (to_voter, credits) in voters.unwrap_or_default() { let voter = to_voter.inner(); let voter_reward = *credits as u64 * credit_reward; - let r = session.call::<_, ()>( - STAKE_CONTRACT, - "reward", - &(*voter, voter_reward), - u64::MAX, - )?; - events.extend(r.events); - - debug!( - event = "validator of prev block rewarded", - voter = to_bs58(voter), - credits = *credits, - reward = voter_reward - ) + rewards.push(Reward { + account: *voter, + value: voter_reward, + reason: RewardReason::Voter, + }); } + let r = + session.call::<_, ()>(STAKE_CONTRACT, "reward", &rewards, u64::MAX)?; + + let mut events = r.events; + events.extend(slash(session, slashing)?); let r = session.call::<_, ()>( @@ -806,12 +801,6 @@ fn calc_generator_extra_reward( credits.saturating_sub(sum as u64) * reward_per_quota } -fn to_bs58(pk: &BlsPublicKey) -> String { - let mut pk = bs58::encode(&pk.to_bytes()).into_string(); - pk.truncate(16); - pk -} - fn slash(session: &mut Session, slash: Vec) -> Result> { let mut events = vec![]; for s in slash { diff --git a/rusk/tests/services/contract_deployment.rs b/rusk/tests/services/contract_deployment.rs index 0aa17d5751..268f6c4a2f 100644 --- a/rusk/tests/services/contract_deployment.rs +++ b/rusk/tests/services/contract_deployment.rs @@ -81,7 +81,7 @@ fn initial_state>(dir: P, deploy_bob: bool) -> Result { bob_bytecode, ContractData::builder() .owner(OWNER) - .constructor_arg(&BOB_INIT_VALUE) + .init_arg(&BOB_INIT_VALUE) .contract_id(gen_contract_id( &bob_bytecode, 0u64, diff --git a/rusk/tests/services/owner_calls.rs b/rusk/tests/services/owner_calls.rs index d3a932bdc5..92c8e13da9 100644 --- a/rusk/tests/services/owner_calls.rs +++ b/rusk/tests/services/owner_calls.rs @@ -64,7 +64,7 @@ fn initial_state>( bob_bytecode, ContractData::builder() .owner(owner.as_ref()) - .constructor_arg(&BOB_INIT_VALUE) + .init_arg(&BOB_INIT_VALUE) .contract_id(gen_contract_id(&bob_bytecode, 0u64, owner)), POINT_LIMIT, )