diff --git a/operator/Cargo.lock b/operator/Cargo.lock index b0c9ddcc..da0b1ee7 100644 --- a/operator/Cargo.lock +++ b/operator/Cargo.lock @@ -2337,6 +2337,7 @@ dependencies = [ "frame-try-runtime", "hex", "hex-literal 0.3.4", + "log", "pallet-authorship", "pallet-babe", "pallet-balances", @@ -2346,6 +2347,8 @@ dependencies = [ "pallet-evm", "pallet-evm-chain-id", "pallet-external-validators", + "pallet-external-validators-rewards", + "pallet-external-validators-rewards-runtime-api", "pallet-grandpa", "pallet-identity", "pallet-im-online", @@ -2517,6 +2520,7 @@ dependencies = [ "frame-try-runtime", "hex", "hex-literal 0.3.4", + "log", "pallet-authorship", "pallet-babe", "pallet-balances", @@ -2526,6 +2530,8 @@ dependencies = [ "pallet-evm", "pallet-evm-chain-id", "pallet-external-validators", + "pallet-external-validators-rewards", + "pallet-external-validators-rewards-runtime-api", "pallet-grandpa", "pallet-identity", "pallet-im-online", @@ -2605,6 +2611,7 @@ dependencies = [ "frame-try-runtime", "hex", "hex-literal 0.3.4", + "log", "pallet-authorship", "pallet-babe", "pallet-balances", @@ -2614,6 +2621,8 @@ dependencies = [ "pallet-evm", "pallet-evm-chain-id", "pallet-external-validators", + "pallet-external-validators-rewards", + "pallet-external-validators-rewards-runtime-api", "pallet-grandpa", "pallet-identity", "pallet-im-online", @@ -7647,6 +7656,43 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-external-validators-rewards" +version = "0.1.0" +dependencies = [ + "cumulus-primitives-core", + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-balances", + "pallet-external-validators", + "pallet-session", + "pallet-timestamp", + "parity-scale-codec", + "polkadot-primitives", + "polkadot-runtime-parachains", + "scale-info", + "snowbridge-core 0.2.0", + "snowbridge-merkle-tree", + "snowbridge-outbound-queue-primitives", + "sp-core", + "sp-io", + "sp-runtime", + "sp-staking", + "sp-std", +] + +[[package]] +name = "pallet-external-validators-rewards-runtime-api" +version = "0.1.0" +dependencies = [ + "parity-scale-codec", + "snowbridge-merkle-tree", + "sp-api", + "sp-core", +] + [[package]] name = "pallet-fast-unstake" version = "38.1.0" diff --git a/operator/Cargo.toml b/operator/Cargo.toml index 4583d483..44633ada 100644 --- a/operator/Cargo.toml +++ b/operator/Cargo.toml @@ -25,6 +25,8 @@ datahaven-stagenet-runtime = { path = "./runtime/stagenet", default-features = f datahaven-testnet-runtime = { path = "./runtime/testnet", default-features = false } dhp-bridge = { path = "./primitives/bridge", default-features = false } pallet-external-validators = { path = "./pallets/external-validators", default-features = false } +pallet-external-validators-rewards = { path = "./pallets/external-validators-rewards", default-features = false } +pallet-external-validators-rewards-runtime-api = { path = "./pallets/external-validators-rewards/runtime-api", default-features = false } pallet-outbound-commitment-store = { path = "./pallets/outbound-commitment-store", default-features = false } # Crates.io (wasm) @@ -57,7 +59,7 @@ parity-bytes = { version = "0.1.2", default-features = false } parity-scale-codec = { version = "3.0.0", default-features = false, features = [ "derive", ] } -rand = { version = "0.8.5", default-features = false } +rand = { version = "0.8.5", default-features = false, features = [ "std_rng" ] } rlp = { version = "0.6.1", default-features = false } scale-info = { version = "2.11.6", default-features = false } serde = { version = "1.0.197", default-features = false, features = ["derive"] } @@ -109,6 +111,7 @@ pallet-xcm = { git = "https://github.com/paritytech/polkadot-sdk", branch = "sta polkadot-parachain-primitives = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } polkadot-primitives = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } polkadot-runtime-common = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } +runtime-parachains = { package = "polkadot-runtime-parachains", git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } sc-basic-authorship = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } sc-cli = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } sc-client-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } diff --git a/operator/pallets/external-validators-rewards/Cargo.toml b/operator/pallets/external-validators-rewards/Cargo.toml new file mode 100644 index 00000000..10c4cc61 --- /dev/null +++ b/operator/pallets/external-validators-rewards/Cargo.toml @@ -0,0 +1,89 @@ +[package] +name = "pallet-external-validators-rewards" +authors = { workspace = true } +description = "Simple pallet to store external validators rewards." +edition = "2021" +license = "GPL-3.0-only" +version = "0.1.0" + +[package.metadata.docs.rs] +targets = [ "x86_64-unknown-linux-gnu" ] + +[lints] +workspace = true + +[dependencies] +log = { workspace = true } +parity-scale-codec = { workspace = true } +scale-info = { workspace = true, features = [ "derive" ] } + +frame-support = { workspace = true } +frame-system = { workspace = true } +sp-core = { workspace = true } +sp-runtime = { workspace = true } +sp-staking = { workspace = true } +sp-std = { workspace = true } + +frame-benchmarking = { workspace = true } + +pallet-balances = { workspace = true, optional = true } +pallet-external-validators = { workspace = true } +pallet-session = { workspace = true, features = [ "historical" ] } +runtime-parachains = { workspace = true } + +snowbridge-core = { workspace = true } +snowbridge-merkle-tree = { workspace = true } +snowbridge-outbound-queue-primitives = { workspace = true } +cumulus-primitives-core = { workspace = true } +polkadot-primitives = { workspace = true } + +[dev-dependencies] +pallet-timestamp = { workspace = true } +sp-io = { workspace = true } + +[features] +default = [ "std" ] +std = [ + "cumulus-primitives-core/std", + "frame-benchmarking/std", + "frame-support/std", + "frame-system/std", + "log/std", + "pallet-balances/std", + "pallet-session/std", + "pallet-timestamp/std", + "parity-scale-codec/std", + "polkadot-primitives/std", + "runtime-parachains/std", + "scale-info/std", + "snowbridge-core/std", + "snowbridge-merkle-tree/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-staking/std", + "sp-std/std", +] +runtime-benchmarks = [ + "cumulus-primitives-core/runtime-benchmarks", + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "pallet-timestamp/runtime-benchmarks", + "polkadot-primitives/runtime-benchmarks", + "runtime-parachains/runtime-benchmarks", + "snowbridge-core/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "sp-staking/runtime-benchmarks", +] + +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "pallet-balances?/try-runtime", + "pallet-session/try-runtime", + "pallet-timestamp/try-runtime", + "runtime-parachains/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/operator/pallets/external-validators-rewards/runtime-api/Cargo.toml b/operator/pallets/external-validators-rewards/runtime-api/Cargo.toml new file mode 100644 index 00000000..d97c7ba6 --- /dev/null +++ b/operator/pallets/external-validators-rewards/runtime-api/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "pallet-external-validators-rewards-runtime-api" +authors = { workspace = true } +description = "Runtime API definition of pallet-external-validators-rewards" +edition = "2021" +license = "GPL-3.0-only" +version = "0.1.0" + +[package.metadata.docs.rs] +targets = [ "x86_64-unknown-linux-gnu" ] + +[lints] +workspace = true + +[dependencies] +parity-scale-codec = { workspace = true } +snowbridge-merkle-tree = { workspace = true } +sp-api = { workspace = true } +sp-core = { workspace = true } + +[features] +default = [ "std" ] +std = [ + "parity-scale-codec/std", + "snowbridge-merkle-tree/std", + "sp-api/std", + "sp-core/std", +] diff --git a/operator/pallets/external-validators-rewards/runtime-api/src/lib.rs b/operator/pallets/external-validators-rewards/runtime-api/src/lib.rs new file mode 100644 index 00000000..713d6078 --- /dev/null +++ b/operator/pallets/external-validators-rewards/runtime-api/src/lib.rs @@ -0,0 +1,32 @@ +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. + +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Tanssi is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see + +//! Runtime API for External Validators Rewards pallet + +#![cfg_attr(not(feature = "std"), no_std)] + +use snowbridge_merkle_tree::MerkleProof; + +sp_api::decl_runtime_apis! { + pub trait ExternalValidatorsRewardsApi + where + AccountId: parity_scale_codec::Codec, + EraIndex: parity_scale_codec::Codec, + { + fn generate_rewards_merkle_proof(account_id: AccountId, era_index: EraIndex) -> Option; + fn verify_rewards_merkle_proof(merkle_proof: MerkleProof) -> bool; + } +} diff --git a/operator/pallets/external-validators-rewards/src/benchmarking.rs b/operator/pallets/external-validators-rewards/src/benchmarking.rs new file mode 100644 index 00000000..335557f4 --- /dev/null +++ b/operator/pallets/external-validators-rewards/src/benchmarking.rs @@ -0,0 +1,80 @@ +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. + +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Tanssi is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see + +//! Benchmarking setup for pallet_external_validators_rewards + +use super::*; + +#[allow(unused)] +use crate::Pallet as ExternalValidatorsRewards; +use { + crate::{types::BenchmarkHelper, OnEraEnd}, + frame_benchmarking::{account, v2::*, BenchmarkError}, + frame_support::traits::Currency, + sp_std::prelude::*, +}; + +const SEED: u32 = 0; + +fn create_funded_user( + string: &'static str, + n: u32, + balance_factor: u32, +) -> T::AccountId { + let user = account(string, n, SEED); + let balance = as Currency>::minimum_balance() + * balance_factor.into(); + let _ = as Currency>::make_free_balance_be( + &user, balance, + ); + user +} + +#[allow(clippy::multiple_bound_locations)] +#[benchmarks(where T: pallet_balances::Config)] +mod benchmarks { + use super::*; + + // worst case for the end of an era. + #[benchmark] + fn on_era_end() -> Result<(), BenchmarkError> { + frame_system::Pallet::::set_block_number(0u32.into()); + + let mut era_reward_points = EraRewardPoints::default(); + era_reward_points.total = 20 * 1000; + + for i in 0..1000 { + let account_id = create_funded_user::("candidate", i, 100); + era_reward_points.individual.insert(account_id, 20); + } + + T::BenchmarkHelper::setup(); + >::insert(1u32, era_reward_points); + + #[block] + { + as OnEraEnd>::on_era_end(1u32); + } + + Ok(()) + } + + impl_benchmark_test_suite!( + ExternalValidatorsRewards, + crate::mock::new_test_ext(), + crate::mock::Test, + ); +} diff --git a/operator/pallets/external-validators-rewards/src/lib.rs b/operator/pallets/external-validators-rewards/src/lib.rs new file mode 100644 index 00000000..d5569392 --- /dev/null +++ b/operator/pallets/external-validators-rewards/src/lib.rs @@ -0,0 +1,402 @@ +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. + +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Tanssi is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see + +//! This pallet keep tracks of the validators reward points. +//! Storage will be cleared after a period of time. + +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +pub mod types; +pub mod weights; + +pub use pallet::*; + +use { + crate::types::{EraRewardsUtils, HandleInflation, SendMessage}, + frame_support::traits::{Defensive, Get, ValidatorSet}, + pallet_external_validators::traits::{ExternalIndexProvider, OnEraEnd, OnEraStart}, + parity_scale_codec::Encode, + polkadot_primitives::ValidatorIndex, + runtime_parachains::session_info, + snowbridge_merkle_tree::{merkle_proof, merkle_root, verify_proof, MerkleProof}, + sp_core::H256, + sp_runtime::traits::{Hash, Zero}, + sp_staking::SessionIndex, + sp_std::{collections::btree_set::BTreeSet, vec::Vec}, +}; + +#[frame_support::pallet] +pub mod pallet { + use frame_support::traits::fungible; + + pub use crate::weights::WeightInfo; + use { + super::*, frame_support::pallet_prelude::*, + pallet_external_validators::traits::EraIndexProvider, sp_runtime::Saturating, + sp_std::collections::btree_map::BTreeMap, + }; + + /// The current storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(0); + + pub type RewardPoints = u32; + pub type EraIndex = u32; + + #[pallet::config] + pub trait Config: frame_system::Config { + /// Overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// How to fetch the current era info. + type EraIndexProvider: EraIndexProvider; + + /// For how many eras points are kept in storage. + #[pallet::constant] + type HistoryDepth: Get; + + /// The amount of era points given by backing a candidate that is included. + #[pallet::constant] + type BackingPoints: Get; + + /// The amount of era points given by dispute voting on a candidate. + #[pallet::constant] + type DisputeStatementPoints: Get; + + /// Provider to know how may tokens were inflated (added) in a specific era. + type EraInflationProvider: Get; + + /// Provider to retrieve the current external index indetifying the validators + type ExternalIndexProvider: ExternalIndexProvider; + + type GetWhitelistedValidators: Get>; + + /// Hashing tool used to generate/verify merkle roots and proofs. + type Hashing: Hash; + + /// Currency the rewards are minted in + type Currency: fungible::Inspect> + + fungible::Mutate; + + /// Ethereum Sovereign Account where rewards will be minted + type RewardsEthereumSovereignAccount: Get; + + /// The weight information of this pallet. + type WeightInfo: WeightInfo; + + /// How to send messages via Snowbridge Outbound Queue V2. + type SendMessage: SendMessage; + + /// Hook for minting inflation tokens. + type HandleInflation: HandleInflation; + + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper: types::BenchmarkHelper; + } + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(_); + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// The rewards message was sent correctly. + RewardsMessageSent { + message_id: H256, + era_index: EraIndex, + total_points: u128, + inflation_amount: u128, + rewards_merkle_root: H256, + }, + } + + /// Keep tracks of distributed points per validator and total. + #[derive(RuntimeDebug, Encode, Decode, PartialEq, Eq, TypeInfo)] + pub struct EraRewardPoints { + pub total: RewardPoints, + pub individual: BTreeMap, + } + + impl EraRewardPoints { + // Helper function used to generate the following utils: + // - rewards_merkle_root: merkle root corresponding [(validatorId, rewardPoints)] + // for the era_index specified. + // - leaves: that were used to generate the previous merkle root. + // - leaf_index: index of the validatorId's leaf in the previous leaves array (if any). + // - total_points: number of total points of the era_index specified. + pub fn generate_era_rewards_utils>( + &self, + era_index: EraIndex, + maybe_account_id_check: Option, + ) -> Option { + let total_points: u128 = self.total as u128; + let mut leaves = Vec::with_capacity(self.individual.len()); + let mut leaf_index = None; + + if let Some(account) = &maybe_account_id_check { + if !self.individual.contains_key(account) { + log::error!( + target: "ext_validators_rewards", + "AccountId {:?} not found for era {:?}!", + account, + era_index + ); + return None; + } + } + + for (index, (account_id, reward_points)) in self.individual.iter().enumerate() { + let encoded = (account_id, reward_points).encode(); + let hashed = ::hash(&encoded); + + leaves.push(hashed); + + if let Some(ref check_account_id) = maybe_account_id_check { + if account_id == check_account_id { + leaf_index = Some(index as u64); + } + } + } + + let rewards_merkle_root = merkle_root::(leaves.iter().cloned()); + + Some(EraRewardsUtils { + rewards_merkle_root, + leaves, + leaf_index, + total_points, + }) + } + } + + impl Default for EraRewardPoints { + fn default() -> Self { + EraRewardPoints { + total: Default::default(), + individual: BTreeMap::new(), + } + } + } + + /// Store reward points per era. + /// Note: EraRewardPoints is actually bounded by the amount of validators. + #[pallet::storage] + #[pallet::unbounded] + pub type RewardPointsForEra = + StorageMap<_, Twox64Concat, EraIndex, EraRewardPoints, ValueQuery>; + + impl Pallet { + /// Reward validators. Does not check if the validators are valid, caller needs to make sure of that. + pub fn reward_by_ids(points: impl IntoIterator) { + let active_era = T::EraIndexProvider::active_era(); + + RewardPointsForEra::::mutate(active_era.index, |era_rewards| { + for (validator, points) in points.into_iter() { + (*era_rewards.individual.entry(validator.clone()).or_default()) + .saturating_accrue(points); + era_rewards.total.saturating_accrue(points); + } + }) + } + + pub fn generate_rewards_merkle_proof( + account_id: T::AccountId, + era_index: EraIndex, + ) -> Option { + let era_rewards = RewardPointsForEra::::get(&era_index); + let utils = era_rewards.generate_era_rewards_utils::<::Hashing>( + era_index, + Some(account_id), + )?; + utils.leaf_index.map(|index| { + merkle_proof::<::Hashing, _>(utils.leaves.into_iter(), index) + }) + } + + pub fn verify_rewards_merkle_proof(merkle_proof: MerkleProof) -> bool { + verify_proof::<::Hashing, _, _>( + &merkle_proof.root, + merkle_proof.proof, + merkle_proof.number_of_leaves, + merkle_proof.leaf_index, + merkle_proof.leaf, + ) + } + + /// Helper to build, validate and deliver an outbound message. + /// Logs any error and returns None on failure. + fn send_rewards_message(utils: &EraRewardsUtils) -> Option { + let outbound = T::SendMessage::build(utils).or_else(|| { + log::error!(target: "ext_validators_rewards", "Failed to build outbound message"); + None + })?; + + let ticket = T::SendMessage::validate(outbound) + .map_err(|e| { + log::error!( + target: "ext_validators_rewards", + "Failed to validate outbound message: {:?}", + e + ); + }) + .ok()?; + + T::SendMessage::deliver(ticket) + .map_err(|e| { + log::error!( + target: "ext_validators_rewards", + "Failed to deliver outbound message: {:?}", + e + ); + }) + .ok() + } + } + + impl OnEraStart for Pallet { + fn on_era_start(era_index: EraIndex, _session_start: u32, _external_idx: u64) { + let Some(era_index_to_delete) = era_index.checked_sub(T::HistoryDepth::get()) else { + return; + }; + + RewardPointsForEra::::remove(era_index_to_delete); + } + } + + impl OnEraEnd for Pallet { + fn on_era_end(era_index: EraIndex) { + let utils = match RewardPointsForEra::::get(&era_index) + .generate_era_rewards_utils::<::Hashing>(era_index, None) + { + Some(utils) if !utils.total_points.is_zero() => utils, + Some(_) => { + log::error!( + target: "ext_validators_rewards", + "Not sending message because total_points is 0" + ); + return; + } + None => { + log::error!( + target: "ext_validators_rewards", + "Failed to generate era rewards utils" + ); + return; + } + }; + + // Mint tokens using the configurable handler + let ethereum_sovereign_account = T::RewardsEthereumSovereignAccount::get(); + let inflation_amount = T::EraInflationProvider::get(); + if let Err(err) = + T::HandleInflation::mint_inflation(ðereum_sovereign_account, inflation_amount) + { + log::error!(target: "ext_validators_rewards", "Failed to handle inflation: {err:?}"); + log::error!(target: "ext_validators_rewards", "Not sending message since there are no rewards to distribute"); + return; + } + + frame_system::Pallet::::register_extra_weight_unchecked( + T::WeightInfo::on_era_end(), + DispatchClass::Mandatory, + ); + + if let Some(message_id) = Self::send_rewards_message(&utils) { + Self::deposit_event(Event::RewardsMessageSent { + message_id, + era_index, + total_points: utils.total_points, + inflation_amount, + rewards_merkle_root: utils.rewards_merkle_root, + }); + } + } + } +} + +/// Rewards validators for participating in parachains with era points in pallet-staking. +pub struct RewardValidatorsWithEraPoints(core::marker::PhantomData); + +impl RewardValidatorsWithEraPoints +where + C: pallet::Config + session_info::Config, + C::ValidatorSet: ValidatorSet, +{ + /// Reward validators in session with points, but only if they are in the active set. + fn reward_only_active( + session_index: SessionIndex, + indices: impl IntoIterator, + points: u32, + ) { + let validators = session_info::AccountKeys::::get(&session_index); + let validators = match validators + .defensive_proof("account_keys are present for dispute_period sessions") + { + Some(validators) => validators, + None => return, + }; + // limit rewards to the active validator set + let mut active_set: BTreeSet<_> = C::ValidatorSet::validators().into_iter().collect(); + + // Remove whitelisted validators, we don't want to reward them + let whitelisted_validators = C::GetWhitelistedValidators::get(); + for validator in whitelisted_validators { + active_set.remove(&validator); + } + + let rewards = indices + .into_iter() + .filter_map(|i| validators.get(i.0 as usize).cloned()) + .filter(|v| active_set.contains(v)) + .map(|v| (v, points)); + + pallet::Pallet::::reward_by_ids(rewards); + } +} + +impl runtime_parachains::inclusion::RewardValidators for RewardValidatorsWithEraPoints +where + C: pallet::Config + runtime_parachains::shared::Config + session_info::Config, + C::ValidatorSet: ValidatorSet, +{ + fn reward_backing(indices: impl IntoIterator) { + let session_index = runtime_parachains::shared::CurrentSessionIndex::::get(); + Self::reward_only_active(session_index, indices, C::BackingPoints::get()); + } + + fn reward_bitfields(_validators: impl IntoIterator) {} +} + +impl runtime_parachains::disputes::RewardValidators for RewardValidatorsWithEraPoints +where + C: pallet::Config + session_info::Config, + C::ValidatorSet: ValidatorSet, +{ + fn reward_dispute_statement( + session: SessionIndex, + validators: impl IntoIterator, + ) { + Self::reward_only_active(session, validators, C::DisputeStatementPoints::get()); + } +} diff --git a/operator/pallets/external-validators-rewards/src/mock.rs b/operator/pallets/external-validators-rewards/src/mock.rs new file mode 100644 index 00000000..bc1ec78e --- /dev/null +++ b/operator/pallets/external-validators-rewards/src/mock.rs @@ -0,0 +1,268 @@ +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. + +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// Tanssi is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see + +use { + crate as pallet_external_validators_rewards, + crate::types::HandleInflation, + frame_support::{ + parameter_types, + traits::{fungible::Mutate, ConstU32, ConstU64}, + }, + pallet_balances::AccountData, + pallet_external_validators::traits::ExternalIndexProvider, + snowbridge_outbound_queue_primitives::{SendError, SendMessageFeeProvider}, + sp_core::H256, + sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup, Keccak256}, + BuildStorage, DispatchError, + }, +}; + +type Block = frame_system::mocking::MockBlock; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system, + ExternalValidatorsRewards: pallet_external_validators_rewards, + // Session: pallet_session, + Balances: pallet_balances, + Timestamp: pallet_timestamp, + Mock: mock_data, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; +} + +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; + type Nonce = u64; + type Block = Block; + type RuntimeTask = (); + type SingleBlockMigrations = (); + type MultiBlockMigrator = (); + type PreInherents = (); + type PostInherents = (); + type PostTransactions = (); + type ExtensionsWeightInfo = (); +} + +parameter_types! { + pub const ExistentialDeposit: u64 = 5; + pub const MaxReserves: u32 = 50; +} + +impl pallet_balances::Config for Test { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); + type Balance = u128; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type ReserveIdentifier = [u8; 8]; + type RuntimeHoldReason = (); + type RuntimeFreezeReason = (); + type FreezeIdentifier = (); + type MaxLocks = (); + type MaxReserves = MaxReserves; + type MaxFreezes = ConstU32<0>; + type DoneSlashHandler = (); +} + +impl pallet_timestamp::Config for Test { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = ConstU64<5>; + type WeightInfo = (); +} + +impl mock_data::Config for Test {} + +pub struct MockOkOutboundQueue; +impl crate::types::SendMessage for MockOkOutboundQueue { + type Ticket = (); + type Message = (); + fn build(_: &crate::types::EraRewardsUtils) -> Option { + Some(()) + } + fn validate(_: Self::Ticket) -> Result { + Ok(()) + } + fn deliver(_: Self::Ticket) -> Result { + Ok(H256::zero()) + } +} + +impl SendMessageFeeProvider for MockOkOutboundQueue { + type Balance = u128; + + fn local_fee() -> Self::Balance { + 1 + } +} + +pub struct TimestampProvider; +impl ExternalIndexProvider for TimestampProvider { + fn get_external_index() -> u64 { + Timestamp::get() + } +} + +parameter_types! { + pub const RewardsEthereumSovereignAccount: u64 + = 0xffffffffffffffff; + pub EraInflationProvider: u128 = Mock::mock().era_inflation.unwrap_or(42); +} + +impl pallet_external_validators_rewards::Config for Test { + type RuntimeEvent = RuntimeEvent; + type EraIndexProvider = mock_data::Pallet; + type HistoryDepth = ConstU32<10>; + type BackingPoints = ConstU32<20>; + type DisputeStatementPoints = ConstU32<20>; + type EraInflationProvider = EraInflationProvider; + type ExternalIndexProvider = TimestampProvider; + type GetWhitelistedValidators = (); + type Hashing = Keccak256; + type SendMessage = MockOkOutboundQueue; + type HandleInflation = InflationMinter; + type Currency = Balances; + type RewardsEthereumSovereignAccount = RewardsEthereumSovereignAccount; + type WeightInfo = (); + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} + +pub struct InflationMinter; +impl HandleInflation for InflationMinter { + fn mint_inflation(account: &u64, amount: u128) -> sp_runtime::DispatchResult { + if amount == 0 { + log::error!(target: "ext_validators_rewards", "No rewards to distribute"); + return Err(DispatchError::Other("No rewards to distribute")); + } + ::Currency::mint_into( + account, + amount.into(), + ) + .map(|_| ()) + .map_err(|_| DispatchError::Other("Failed to mint inflation")) + } +} + +// Pallet to provide some mock data, used to test +#[frame_support::pallet] +pub mod mock_data { + use { + frame_support::pallet_prelude::*, + pallet_external_validators::traits::{ActiveEraInfo, EraIndex, EraIndexProvider}, + }; + + #[derive(Clone, Default, Encode, Decode, sp_core::RuntimeDebug, scale_info::TypeInfo)] + pub struct Mocks { + pub active_era: Option, + pub era_inflation: Option, + } + + #[pallet::config] + pub trait Config: frame_system::Config {} + + #[pallet::call] + impl Pallet {} + + #[pallet::pallet] + #[pallet::without_storage_info] + pub struct Pallet(_); + + #[pallet::storage] + pub(super) type Mock = StorageValue<_, Mocks, ValueQuery>; + + impl Pallet { + pub fn mock() -> Mocks { + Mock::::get() + } + + pub fn mutate(f: F) -> R + where + F: FnOnce(&mut Mocks) -> R, + { + Mock::::mutate(f) + } + } + + impl EraIndexProvider for Pallet { + fn active_era() -> ActiveEraInfo { + Self::mock() + .active_era + .expect("active_era should be set in test") + .clone() + } + + fn era_to_session_start(_era_index: EraIndex) -> Option { + unimplemented!() + } + } +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default() + .build_storage() + .unwrap(); + + let balances = vec![(1, 100), (2, 100), (3, 100), (4, 100), (5, 100)]; + pallet_balances::GenesisConfig:: { balances } + .assimilate_storage(&mut t) + .unwrap(); + + let ext: sp_io::TestExternalities = t.into(); + + ext +} + +pub const INIT_TIMESTAMP: u64 = 30_000; +pub const BLOCK_TIME: u64 = 1000; + +pub fn run_to_block(n: u64) { + let old_block_number = System::block_number(); + + for x in old_block_number..n { + System::reset_events(); + System::set_block_number(x + 1); + Timestamp::set_timestamp(System::block_number() * BLOCK_TIME + INIT_TIMESTAMP); + } +} diff --git a/operator/pallets/external-validators-rewards/src/tests.rs b/operator/pallets/external-validators-rewards/src/tests.rs new file mode 100644 index 00000000..92e2aefc --- /dev/null +++ b/operator/pallets/external-validators-rewards/src/tests.rs @@ -0,0 +1,213 @@ +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. + +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Tanssi is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see + +use { + crate::{self as pallet_external_validators_rewards, mock::*}, + pallet_external_validators::traits::{ActiveEraInfo, OnEraEnd, OnEraStart}, + sp_std::collections::btree_map::BTreeMap, +}; + +#[test] +fn basic_setup_works() { + new_test_ext().execute_with(|| { + // Mock::mutate(|mock| mock.active_era = Some(ActiveEraInfo { index: 0, start: None})); + let storage_eras = + pallet_external_validators_rewards::RewardPointsForEra::::iter().count(); + assert_eq!(storage_eras, 0); + }); +} + +#[test] +fn can_reward_validators() { + new_test_ext().execute_with(|| { + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 1, + start: None, + }) + }); + ExternalValidatorsRewards::reward_by_ids([(1, 10), (3, 30), (5, 50)]); + ExternalValidatorsRewards::reward_by_ids([(1, 10), (3, 10), (5, 10)]); + + let storage_eras = + pallet_external_validators_rewards::RewardPointsForEra::::iter().count(); + assert_eq!(storage_eras, 1); + + let era_points = pallet_external_validators_rewards::RewardPointsForEra::::get(1); + let mut expected_map = BTreeMap::new(); + expected_map.insert(1, 20); + expected_map.insert(3, 40); + expected_map.insert(5, 60); + assert_eq!(era_points.individual, expected_map); + assert_eq!(era_points.total, 20 + 40 + 60); + }) +} + +#[test] +fn history_limit() { + new_test_ext().execute_with(|| { + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 1, + start: None, + }) + }); + ExternalValidatorsRewards::reward_by_ids([(1, 10), (3, 30), (5, 50)]); + + let storage_eras = + pallet_external_validators_rewards::RewardPointsForEra::::iter().count(); + assert_eq!(storage_eras, 1); + + ExternalValidatorsRewards::on_era_start(10, 0, 10); + let storage_eras = + pallet_external_validators_rewards::RewardPointsForEra::::iter().count(); + assert_eq!(storage_eras, 1, "shouldn't erase data yet"); + + ExternalValidatorsRewards::on_era_start(11, 0, 11); + let storage_eras = + pallet_external_validators_rewards::RewardPointsForEra::::iter().count(); + assert_eq!(storage_eras, 0, "data should be erased now"); + }) +} + +#[test] +fn test_on_era_end() { + new_test_ext().execute_with(|| { + run_to_block(1); + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 1, + start: None, + }) + }); + let points = vec![10u32, 30u32, 50u32]; + let total_points: u32 = points.iter().cloned().sum(); + let accounts = vec![1u64, 3u64, 5u64]; + let accounts_points: Vec<(u64, crate::RewardPoints)> = accounts + .iter() + .cloned() + .zip(points.iter().cloned()) + .collect(); + ExternalValidatorsRewards::reward_by_ids(accounts_points); + ExternalValidatorsRewards::on_era_end(1); + + let era_rewards = pallet_external_validators_rewards::RewardPointsForEra::::get(1); + let rewards_utils = era_rewards.generate_era_rewards_utils::<::Hashing>(1, None); + + let root = rewards_utils.unwrap().rewards_merkle_root; + let inflation = ::EraInflationProvider::get(); + System::assert_last_event(RuntimeEvent::ExternalValidatorsRewards( + crate::Event::RewardsMessageSent { + message_id: Default::default(), + era_index: 1, + total_points: total_points as u128, + inflation_amount: inflation, + rewards_merkle_root: root, + }, + )); + }) +} + +#[test] +fn test_on_era_end_with_zero_inflation() { + new_test_ext().execute_with(|| { + run_to_block(1); + + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 1, + start: None, + }); + mock.era_inflation = Some(0); + }); + let points = vec![10u32, 30u32, 50u32]; + let total_points: u32 = points.iter().cloned().sum(); + let accounts = vec![1u64, 3u64, 5u64]; + let accounts_points: Vec<(u64, crate::RewardPoints)> = accounts + .iter() + .cloned() + .zip(points.iter().cloned()) + .collect(); + ExternalValidatorsRewards::reward_by_ids(accounts_points); + ExternalValidatorsRewards::on_era_end(1); + + let era_rewards = pallet_external_validators_rewards::RewardPointsForEra::::get(1); + let rewards_utils = era_rewards.generate_era_rewards_utils::<::Hashing>(1, None); + let root = rewards_utils.unwrap().rewards_merkle_root; + let inflation = ::EraInflationProvider::get(); + let expected_not_thrown_event = RuntimeEvent::ExternalValidatorsRewards( + crate::Event::RewardsMessageSent { + message_id: Default::default(), + era_index: 1, + total_points: total_points as u128, + inflation_amount: inflation, + rewards_merkle_root: root, + } + ); + let events = System::events(); + assert!( + !events + .iter() + .any(|record| record.event == expected_not_thrown_event), + "event should not have been thrown", + ); + }) +} + +#[test] +fn test_on_era_end_with_zero_points() { + new_test_ext().execute_with(|| { + run_to_block(1); + + Mock::mutate(|mock| { + mock.active_era = Some(ActiveEraInfo { + index: 1, + start: None, + }); + }); + let points = vec![0u32, 0u32, 0u32]; + let total_points: u32 = points.iter().cloned().sum(); + let accounts = vec![1u64, 3u64, 5u64]; + let accounts_points: Vec<(u64, crate::RewardPoints)> = accounts + .iter() + .cloned() + .zip(points.iter().cloned()) + .collect(); + ExternalValidatorsRewards::reward_by_ids(accounts_points); + ExternalValidatorsRewards::on_era_end(1); + + let era_rewards = pallet_external_validators_rewards::RewardPointsForEra::::get(1); + let rewards_utils = era_rewards.generate_era_rewards_utils::<::Hashing>(1, None); + let root = rewards_utils.unwrap().rewards_merkle_root; + let inflation = ::EraInflationProvider::get(); + let expected_not_thrown_event = RuntimeEvent::ExternalValidatorsRewards( + crate::Event::RewardsMessageSent { + message_id: Default::default(), + era_index: 1, + total_points: total_points as u128, + inflation_amount: inflation, + rewards_merkle_root: root, + } + ); + let events = System::events(); + assert!( + !events + .iter() + .any(|record| record.event == expected_not_thrown_event), + "event should not have been thrown", + ); + }) +} diff --git a/operator/pallets/external-validators-rewards/src/types.rs b/operator/pallets/external-validators-rewards/src/types.rs new file mode 100644 index 00000000..44a20f66 --- /dev/null +++ b/operator/pallets/external-validators-rewards/src/types.rs @@ -0,0 +1,52 @@ +use snowbridge_outbound_queue_primitives::SendError; +use sp_core::H256; +use sp_std::vec::Vec; + +pub trait DeliverMessage { + type Ticket; + + fn deliver(ticket: Self::Ticket) -> Result; +} + +/// Utils needed to generate/verify merkle roots/proofs inside this pallet. +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct EraRewardsUtils { + pub rewards_merkle_root: H256, + pub leaves: Vec, + pub leaf_index: Option, + pub total_points: u128, +} + +pub trait SendMessage { + type Message; + type Ticket; + + fn build(utils: &EraRewardsUtils) -> Option; + + fn validate(message: Self::Message) -> Result; + + fn deliver(ticket: Self::Ticket) -> Result; +} + +// Trait for handling inflation +pub trait HandleInflation { + fn mint_inflation(who: &AccountId, amount: u128) -> sp_runtime::DispatchResult; +} + +impl HandleInflation for () { + fn mint_inflation(_: &AccountId, _: u128) -> sp_runtime::DispatchResult { + Ok(()) + } +} + +#[cfg(feature = "runtime-benchmarks")] +pub trait BenchmarkHelper { + fn setup(); +} + +#[cfg(feature = "runtime-benchmarks")] +impl BenchmarkHelper for () { + fn setup() { + () + } +} diff --git a/operator/pallets/external-validators-rewards/src/weights.rs b/operator/pallets/external-validators-rewards/src/weights.rs new file mode 100644 index 00000000..766adfcf --- /dev/null +++ b/operator/pallets/external-validators-rewards/src/weights.rs @@ -0,0 +1,116 @@ +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. + +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Tanssi is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see + + +//! Autogenerated weights for pallet_external_validators_rewards +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 43.0.0 +//! DATE: 2024-12-05, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `COV0768`, CPU: `AMD Ryzen 9 7950X 16-Core Processor` +//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("dancelight-dev"), DB CACHE: 1024 + +// Executed Command: +// target/release/tanssi-relay +// benchmark +// pallet +// --execution=wasm +// --wasm-execution=compiled +// --pallet +// pallet_external_validators_rewards +// --extrinsic +// * +// --chain=dancelight-dev +// --steps +// 50 +// --repeat +// 20 +// --template=benchmarking/frame-weight-pallet-template.hbs +// --json-file +// raw.json +// --output +// pallets/external-validators-rewards/src/pallet_external_validators_rewards.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_external_validators_rewards. +pub trait WeightInfo { + fn on_era_end() -> Weight; +} + +/// Weights for pallet_external_validators_rewards using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `ExternalValidatorsRewards::RewardPointsForEra` (r:1 w:0) + /// Proof: `ExternalValidatorsRewards::RewardPointsForEra` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `EthereumSystem::Channels` (r:1 w:0) + /// Proof: `EthereumSystem::Channels` (`max_values`: None, `max_size`: Some(76), added: 2551, mode: `MaxEncodedLen`) + /// Storage: `MessageQueue::BookStateFor` (r:1 w:1) + /// Proof: `MessageQueue::BookStateFor` (`max_values`: None, `max_size`: Some(136), added: 2611, mode: `MaxEncodedLen`) + /// Storage: `MessageQueue::ServiceHead` (r:1 w:1) + /// Proof: `MessageQueue::ServiceHead` (`max_values`: Some(1), `max_size`: Some(33), added: 528, mode: `MaxEncodedLen`) + /// Storage: UNKNOWN KEY `0x3a72656c61795f64697370617463685f71756575655f72656d61696e696e675f` (r:0 w:1) + /// Proof: UNKNOWN KEY `0x3a72656c61795f64697370617463685f71756575655f72656d61696e696e675f` (r:0 w:1) + /// Storage: `MessageQueue::Pages` (r:0 w:1) + /// Proof: `MessageQueue::Pages` (`max_values`: None, `max_size`: Some(32845), added: 35320, mode: `MaxEncodedLen`) + /// Storage: UNKNOWN KEY `0xf5207f03cfdce586301014700e2c2593fad157e461d71fd4c1f936839a5f1f3e` (r:0 w:1) + /// Proof: UNKNOWN KEY `0xf5207f03cfdce586301014700e2c2593fad157e461d71fd4c1f936839a5f1f3e` (r:0 w:1) + fn on_era_end() -> Weight { + // Proof Size summary in bytes: + // Measured: `36522` + // Estimated: `39987` + // Minimum execution time: 1_042_933_000 picoseconds. + Weight::from_parts(1_136_401_000, 39987) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// Storage: `ExternalValidatorsRewards::RewardPointsForEra` (r:1 w:0) + /// Proof: `ExternalValidatorsRewards::RewardPointsForEra` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `EthereumSystem::Channels` (r:1 w:0) + /// Proof: `EthereumSystem::Channels` (`max_values`: None, `max_size`: Some(76), added: 2551, mode: `MaxEncodedLen`) + /// Storage: `MessageQueue::BookStateFor` (r:1 w:1) + /// Proof: `MessageQueue::BookStateFor` (`max_values`: None, `max_size`: Some(136), added: 2611, mode: `MaxEncodedLen`) + /// Storage: `MessageQueue::ServiceHead` (r:1 w:1) + /// Proof: `MessageQueue::ServiceHead` (`max_values`: Some(1), `max_size`: Some(33), added: 528, mode: `MaxEncodedLen`) + /// Storage: UNKNOWN KEY `0x3a72656c61795f64697370617463685f71756575655f72656d61696e696e675f` (r:0 w:1) + /// Proof: UNKNOWN KEY `0x3a72656c61795f64697370617463685f71756575655f72656d61696e696e675f` (r:0 w:1) + /// Storage: `MessageQueue::Pages` (r:0 w:1) + /// Proof: `MessageQueue::Pages` (`max_values`: None, `max_size`: Some(32845), added: 35320, mode: `MaxEncodedLen`) + /// Storage: UNKNOWN KEY `0xf5207f03cfdce586301014700e2c2593fad157e461d71fd4c1f936839a5f1f3e` (r:0 w:1) + /// Proof: UNKNOWN KEY `0xf5207f03cfdce586301014700e2c2593fad157e461d71fd4c1f936839a5f1f3e` (r:0 w:1) + fn on_era_end() -> Weight { + // Proof Size summary in bytes: + // Measured: `36522` + // Estimated: `39987` + // Minimum execution time: 1_042_933_000 picoseconds. + Weight::from_parts(1_136_401_000, 39987) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } +} diff --git a/operator/pallets/external-validators/src/lib.rs b/operator/pallets/external-validators/src/lib.rs index cd3ce193..8b471e3b 100644 --- a/operator/pallets/external-validators/src/lib.rs +++ b/operator/pallets/external-validators/src/lib.rs @@ -49,7 +49,7 @@ use { #[cfg(test)] mod mock; -mod traits; +pub mod traits; #[cfg(test)] mod tests; diff --git a/operator/runtime/mainnet/Cargo.toml b/operator/runtime/mainnet/Cargo.toml index 1a37e1e8..bbb51c43 100644 --- a/operator/runtime/mainnet/Cargo.toml +++ b/operator/runtime/mainnet/Cargo.toml @@ -31,6 +31,7 @@ frame-system-rpc-runtime-api = { workspace = true } frame-try-runtime = { workspace = true, optional = true } hex = { workspace = true } hex-literal = { workspace = true } +log = { workspace = true } pallet-authorship = { workspace = true } pallet-babe = { workspace = true } pallet-balances = { workspace = true } @@ -40,6 +41,8 @@ pallet-ethereum = { workspace = true } pallet-evm = { workspace = true } pallet-evm-chain-id = { workspace = true } pallet-external-validators = { workspace = true } +pallet-external-validators-rewards = { workspace = true } +pallet-external-validators-rewards-runtime-api = { workspace = true } pallet-grandpa = { workspace = true } pallet-identity = { workspace = true } pallet-im-online = { workspace = true } @@ -123,6 +126,8 @@ std = [ "pallet-evm-chain-id/std", "pallet-evm/std", "pallet-external-validators/std", + "pallet-external-validators-rewards/std", + "pallet-external-validators-rewards-runtime-api/std", "pallet-grandpa/std", "pallet-identity/std", "pallet-im-online/std", @@ -189,6 +194,7 @@ runtime-benchmarks = [ "pallet-ethereum/runtime-benchmarks", "pallet-evm/runtime-benchmarks", "pallet-external-validators/runtime-benchmarks", + "pallet-external-validators-rewards/runtime-benchmarks", "pallet-grandpa/runtime-benchmarks", "pallet-identity/runtime-benchmarks", "pallet-im-online/runtime-benchmarks", @@ -228,6 +234,7 @@ try-runtime = [ "pallet-ethereum/try-runtime", "pallet-evm/try-runtime", "pallet-external-validators/try-runtime", + "pallet-external-validators-rewards/try-runtime", "pallet-grandpa/try-runtime", "pallet-identity/try-runtime", "pallet-im-online/try-runtime", diff --git a/operator/runtime/mainnet/src/configs/mod.rs b/operator/runtime/mainnet/src/configs/mod.rs index 8c7926d4..212a825a 100644 --- a/operator/runtime/mainnet/src/configs/mod.rs +++ b/operator/runtime/mainnet/src/configs/mod.rs @@ -27,11 +27,11 @@ mod runtime_params; use super::{ deposit, AccountId, Babe, Balance, Balances, BeefyMmrLeaf, Block, BlockNumber, - EthereumBeaconClient, EvmChainId, ExternalValidators, Hash, Historical, ImOnline, MessageQueue, - Nonce, Offences, OriginCaller, OutboundCommitmentStore, OutboundQueueV2, PalletInfo, Preimage, - Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, RuntimeHoldReason, RuntimeOrigin, - RuntimeTask, Session, SessionKeys, Signature, System, Timestamp, EXISTENTIAL_DEPOSIT, - SLOT_DURATION, STORAGE_BYTE_FEE, SUPPLY_FACTOR, UNIT, VERSION, + EthereumBeaconClient, EvmChainId, ExternalValidators, ExternalValidatorsRewards, Hash, + Historical, ImOnline, MessageQueue, Nonce, Offences, OriginCaller, OutboundCommitmentStore, + OutboundQueueV2, PalletInfo, Preimage, Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, + RuntimeHoldReason, RuntimeOrigin, RuntimeTask, Session, SessionKeys, Signature, System, + Timestamp, EXISTENTIAL_DEPOSIT, SLOT_DURATION, STORAGE_BYTE_FEE, SUPPLY_FACTOR, UNIT, VERSION, }; use codec::{Decode, Encode}; use datahaven_runtime_common::{ @@ -55,7 +55,7 @@ use frame_support::{ }; use frame_system::{ limits::{BlockLength, BlockWeights}, - EnsureRoot, EnsureRootWithSuccess, + unique, EnsureRoot, EnsureRootWithSuccess, }; use pallet_ethereum::PostLogContent; use pallet_evm::{ @@ -75,7 +75,7 @@ use snowbridge_core::{gwei, meth, AgentIdOf, PricingParameters, Rewards}; use snowbridge_inbound_queue_primitives::RewardLedger; use snowbridge_outbound_queue_primitives::{ v1::{Fee, Message, SendMessage}, - v2::ConstantGasMeter, + v2::{Command, ConstantGasMeter, Message as OutboundMessage, SendMessage as SendMessageV2}, SendError, SendMessageFeeProvider, }; use snowbridge_pallet_outbound_queue_v2::OnNewCommitment; @@ -127,6 +127,7 @@ parameter_types! { pub const MaxAuthorities: u32 = 32; pub const BondingDuration: EraIndex = polkadot_runtime_common::prod_or_fast!(28, 3); pub const SessionsPerEra: SessionIndex = polkadot_runtime_common::prod_or_fast!(6, 1); + pub const AuthorRewardPoints: u32 = 20; } //╔═══════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ @@ -236,9 +237,22 @@ impl pallet_balances::Config for Runtime { type DoneSlashHandler = (); } +pub struct RewardsPoints; + +impl pallet_authorship::EventHandler for RewardsPoints { + fn note_author(author: AccountId) { + let whitelisted_validators = + pallet_external_validators::WhitelistedValidatorsActiveEra::::get(); + // Do not reward whitelisted validators + if !whitelisted_validators.contains(&author) { + ExternalValidatorsRewards::reward_by_ids(vec![(author, AuthorRewardPoints::get())]) + } + } +} + impl pallet_authorship::Config for Runtime { type FindAuthor = pallet_session::FindAccountFromAuthorIndex; - type EventHandler = ImOnline; + type EventHandler = (RewardsPoints, ImOnline); } impl pallet_offences::Config for Runtime { @@ -637,6 +651,7 @@ parameter_types! { multiplier: FixedU128::from_rational(1, 1), }; pub EthereumLocation: Location = Location::new(1, EthereumNetwork::get()); + // TODO: Change to the actual treasury account pub TreasuryAccountId: AccountId = AccountId::from([0u8; 20]); } @@ -887,10 +902,82 @@ impl pallet_external_validators::Config for Runtime { type ValidatorRegistration = Session; type UnixTime = Timestamp; type SessionsPerEra = SessionsPerEra; - // TODO: Implement OnEraStart and OnEraEnd when ExternalValidatorsRewards is added - type OnEraStart = (); - type OnEraEnd = (); + type OnEraStart = ExternalValidatorsRewards; + type OnEraEnd = ExternalValidatorsRewards; type WeightInfo = (); #[cfg(feature = "runtime-benchmarks")] type Currency = Balances; } + +pub struct GetWhitelistedValidators; +impl Get> for GetWhitelistedValidators { + fn get() -> Vec { + pallet_external_validators::WhitelistedValidatorsActiveEra::::get().into() + } +} + +// Stub SendMessage implementation for rewards pallet +pub struct RewardsSendAdapter; +impl pallet_external_validators_rewards::types::SendMessage for RewardsSendAdapter { + type Message = OutboundMessage; + type Ticket = OutboundMessage; + fn build( + rewards_utils: &pallet_external_validators_rewards::types::EraRewardsUtils, + ) -> Option { + let selector = runtime_params::dynamic_params::runtime_config::RewardsUpdateSelector::get(); + + let mut calldata = Vec::new(); + calldata.extend_from_slice(&selector); + calldata.extend_from_slice(rewards_utils.rewards_merkle_root.as_bytes()); + + let command = Command::CallContract { + target: runtime_params::dynamic_params::runtime_config::RewardsRegistryAddress::get(), + calldata, + gas: 100_0000, // TODO: Determine appropriate gas value after testing + value: 0, + }; + let message = OutboundMessage { + origin: runtime_params::dynamic_params::runtime_config::RewardsOrigin::get(), + // TODO: Determine appropriate id value + id: unique(rewards_utils.rewards_merkle_root).into(), + fee: 0, + commands: match vec![command].try_into() { + Ok(cmds) => cmds, + Err(_) => { + log::error!( + target: "rewards_send_adapter", + "Failed to convert commands: too many commands" + ); + return None; + } + }, + }; + Some(message) + } + + fn validate(message: Self::Message) -> Result { + OutboundQueueV2::validate(&message) + } + fn deliver(message: Self::Ticket) -> Result { + OutboundQueueV2::deliver(message) + } +} + +impl pallet_external_validators_rewards::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type EraIndexProvider = ExternalValidators; + type HistoryDepth = ConstU32<64>; + type BackingPoints = ConstU32<20>; + type DisputeStatementPoints = ConstU32<20>; + type EraInflationProvider = ConstU128<0>; + type ExternalIndexProvider = ExternalValidators; + type GetWhitelistedValidators = GetWhitelistedValidators; + type Hashing = Keccak256; + type Currency = Balances; + type RewardsEthereumSovereignAccount = TreasuryAccountId; + type WeightInfo = (); + type SendMessage = RewardsSendAdapter; + type HandleInflation = (); + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} diff --git a/operator/runtime/mainnet/src/configs/runtime_params.rs b/operator/runtime/mainnet/src/configs/runtime_params.rs index a8b7ace8..24cfac1c 100644 --- a/operator/runtime/mainnet/src/configs/runtime_params.rs +++ b/operator/runtime/mainnet/src/configs/runtime_params.rs @@ -1,7 +1,9 @@ -use frame_support::dynamic_params::{dynamic_pallet_params, dynamic_params}; -use sp_core::H160; - use crate::Runtime; +use frame_support::dynamic_params::{dynamic_pallet_params, dynamic_params}; +use hex_literal::hex; +use sp_core::{ConstU32, H160, H256}; +use sp_runtime::BoundedVec; +use sp_std::vec; #[dynamic_params(RuntimeParameters, pallet_parameters::Parameters::)] pub mod dynamic_params { @@ -18,6 +20,26 @@ pub mod dynamic_params { /// The fact that this is a parameter means that we can set it initially to the zero address, /// and then change it later via governance, to the actual address of the deployed contract. pub static EthereumGatewayAddress: H160 = H160::repeat_byte(0x0); + + #[codec(index = 1)] + #[allow(non_upper_case_globals)] + /// Set the initial address of the Rewards Registry contract on Ethereum. + /// The fact that this is a parameter means that we can set it initially to the zero address, + /// and then change it later via governance, to the actual address of the deployed contract. + pub static RewardsRegistryAddress: H160 = H160::repeat_byte(0x0); + + #[codec(index = 2)] + #[allow(non_upper_case_globals)] + /// The Selector is the first 4 bytes of the keccak256 hash of the function signature("updateRewardsMerkleRoot(bytes32)") + pub static RewardsUpdateSelector: BoundedVec> = + BoundedVec::truncate_from(vec![0xdc, 0x3d, 0x04, 0xec]); + + #[codec(index = 3)] + #[allow(non_upper_case_globals)] + /// The Origin is the hash of the string "external_validators_rewards" + pub static RewardsOrigin: H256 = H256::from_slice(&hex!( + "c505dfb2df107d106d08bd0f1a0acd10052ca9aa078629a4ccfd0c90c6e69b65" + )); } } diff --git a/operator/runtime/mainnet/src/lib.rs b/operator/runtime/mainnet/src/lib.rs index 85852b73..5736a67c 100644 --- a/operator/runtime/mainnet/src/lib.rs +++ b/operator/runtime/mainnet/src/lib.rs @@ -23,9 +23,11 @@ pub use frame_system::Call as SystemCall; pub use pallet_balances::Call as BalancesCall; use pallet_ethereum::{Call::transact, Transaction as EthereumTransaction}; use pallet_evm::{Account as EVMAccount, FeeCalculator, GasWeightMapping, Runner}; +use pallet_external_validators::traits::EraIndex; use pallet_grandpa::{fg_primitives, AuthorityId as GrandpaId}; pub use pallet_timestamp::Call as TimestampCall; use snowbridge_core::AgentId; +use snowbridge_merkle_tree::MerkleProof; use sp_api::impl_runtime_apis; use sp_consensus_beefy::{ ecdsa_crypto::{AuthorityId as BeefyId, Signature as BeefySignature}, @@ -341,6 +343,9 @@ mod runtime { #[runtime::pallet_index(100)] pub type OutboundCommitmentStore = pallet_outbound_commitment_store; + #[runtime::pallet_index(101)] + pub type ExternalValidatorsRewards = pallet_external_validators_rewards; + // ╚═══════════════════ DataHaven-specific Pallets ══════════════════╝ } @@ -778,6 +783,19 @@ impl_runtime_apis! { } } + impl pallet_external_validators_rewards_runtime_api::ExternalValidatorsRewardsApi for Runtime + where + EraIndex: codec::Codec, + { + fn generate_rewards_merkle_proof(account_id: AccountId, era_index: EraIndex) -> Option { + ExternalValidatorsRewards::generate_rewards_merkle_proof(account_id, era_index) + } + + fn verify_rewards_merkle_proof(merkle_proof: MerkleProof) -> bool { + ExternalValidatorsRewards::verify_rewards_merkle_proof(merkle_proof) + } + } + #[cfg(feature = "runtime-benchmarks")] impl frame_benchmarking::Benchmark for Runtime { fn benchmark_metadata(extra: bool) -> ( diff --git a/operator/runtime/stagenet/Cargo.toml b/operator/runtime/stagenet/Cargo.toml index 25dd17ea..de52b881 100644 --- a/operator/runtime/stagenet/Cargo.toml +++ b/operator/runtime/stagenet/Cargo.toml @@ -31,6 +31,7 @@ frame-system-rpc-runtime-api = { workspace = true } frame-try-runtime = { workspace = true, optional = true } hex = { workspace = true } hex-literal = { workspace = true } +log = { workspace = true } pallet-authorship = { workspace = true } pallet-babe = { workspace = true } pallet-balances = { workspace = true } @@ -40,6 +41,8 @@ pallet-ethereum = { workspace = true } pallet-evm = { workspace = true } pallet-evm-chain-id = { workspace = true } pallet-external-validators = { workspace = true } +pallet-external-validators-rewards = { workspace = true } +pallet-external-validators-rewards-runtime-api = { workspace = true } pallet-grandpa = { workspace = true } pallet-identity = { workspace = true } pallet-im-online = { workspace = true } @@ -123,6 +126,8 @@ std = [ "pallet-evm-chain-id/std", "pallet-evm/std", "pallet-external-validators/std", + "pallet-external-validators-rewards/std", + "pallet-external-validators-rewards-runtime-api/std", "pallet-grandpa/std", "pallet-identity/std", "pallet-im-online/std", @@ -189,6 +194,7 @@ runtime-benchmarks = [ "pallet-ethereum/runtime-benchmarks", "pallet-evm/runtime-benchmarks", "pallet-external-validators/runtime-benchmarks", + "pallet-external-validators-rewards/runtime-benchmarks", "pallet-grandpa/runtime-benchmarks", "pallet-identity/runtime-benchmarks", "pallet-im-online/runtime-benchmarks", @@ -228,6 +234,7 @@ try-runtime = [ "pallet-ethereum/try-runtime", "pallet-evm/try-runtime", "pallet-external-validators/try-runtime", + "pallet-external-validators-rewards/try-runtime", "pallet-grandpa/try-runtime", "pallet-identity/try-runtime", "pallet-im-online/try-runtime", diff --git a/operator/runtime/stagenet/src/configs/mod.rs b/operator/runtime/stagenet/src/configs/mod.rs index c0222a52..124d25f5 100644 --- a/operator/runtime/stagenet/src/configs/mod.rs +++ b/operator/runtime/stagenet/src/configs/mod.rs @@ -27,11 +27,11 @@ mod runtime_params; use super::{ deposit, AccountId, Babe, Balance, Balances, BeefyMmrLeaf, Block, BlockNumber, - EthereumBeaconClient, EvmChainId, ExternalValidators, Hash, Historical, ImOnline, MessageQueue, - Nonce, Offences, OriginCaller, OutboundCommitmentStore, OutboundQueueV2, PalletInfo, Preimage, - Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, RuntimeHoldReason, RuntimeOrigin, - RuntimeTask, Session, SessionKeys, Signature, System, Timestamp, EXISTENTIAL_DEPOSIT, - SLOT_DURATION, STORAGE_BYTE_FEE, SUPPLY_FACTOR, UNIT, VERSION, + EthereumBeaconClient, EvmChainId, ExternalValidators, ExternalValidatorsRewards, Hash, + Historical, ImOnline, MessageQueue, Nonce, Offences, OriginCaller, OutboundCommitmentStore, + OutboundQueueV2, PalletInfo, Preimage, Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, + RuntimeHoldReason, RuntimeOrigin, RuntimeTask, Session, SessionKeys, Signature, System, + Timestamp, EXISTENTIAL_DEPOSIT, SLOT_DURATION, STORAGE_BYTE_FEE, SUPPLY_FACTOR, UNIT, VERSION, }; use codec::{Decode, Encode}; use datahaven_runtime_common::{ @@ -55,7 +55,7 @@ use frame_support::{ }; use frame_system::{ limits::{BlockLength, BlockWeights}, - EnsureRoot, EnsureRootWithSuccess, + unique, EnsureRoot, EnsureRootWithSuccess, }; use pallet_ethereum::PostLogContent; use pallet_evm::{ @@ -75,7 +75,7 @@ use snowbridge_core::{gwei, meth, AgentIdOf, PricingParameters, Rewards}; use snowbridge_inbound_queue_primitives::RewardLedger; use snowbridge_outbound_queue_primitives::{ v1::{Fee, Message, SendMessage}, - v2::ConstantGasMeter, + v2::{Command, ConstantGasMeter, Message as OutboundMessage, SendMessage as SendMessageV2}, SendError, SendMessageFeeProvider, }; use snowbridge_pallet_outbound_queue_v2::OnNewCommitment; @@ -127,6 +127,7 @@ parameter_types! { pub const MaxAuthorities: u32 = 32; pub const BondingDuration: EraIndex = polkadot_runtime_common::prod_or_fast!(28, 3); pub const SessionsPerEra: SessionIndex = polkadot_runtime_common::prod_or_fast!(6, 1); + pub const AuthorRewardPoints: u32 = 20; } //╔═══════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ @@ -236,9 +237,22 @@ impl pallet_balances::Config for Runtime { type DoneSlashHandler = (); } +pub struct RewardsPoints; + +impl pallet_authorship::EventHandler for RewardsPoints { + fn note_author(author: AccountId) { + let whitelisted_validators = + pallet_external_validators::WhitelistedValidatorsActiveEra::::get(); + // Do not reward whitelisted validators + if !whitelisted_validators.contains(&author) { + ExternalValidatorsRewards::reward_by_ids(vec![(author, AuthorRewardPoints::get())]) + } + } +} + impl pallet_authorship::Config for Runtime { type FindAuthor = pallet_session::FindAccountFromAuthorIndex; - type EventHandler = ImOnline; + type EventHandler = (RewardsPoints, ImOnline); } impl pallet_offences::Config for Runtime { @@ -636,6 +650,7 @@ parameter_types! { multiplier: FixedU128::from_rational(1, 1), }; pub EthereumLocation: Location = Location::new(1, EthereumNetwork::get()); + // TODO: Change to the actual treasury account pub TreasuryAccountId: AccountId = AccountId::from([0u8; 20]); } @@ -694,49 +709,6 @@ impl snowbridge_pallet_system_v2::Config for Runtime { // The version numbers are taken from looking at the Dora explorer when launching the // kurtosis Ethereum network. Hovering over the fork names, shows the version numbers. // These version numbers need to match, otherwise the aggregated signature verification will fail. -#[cfg(any( - feature = "std", - feature = "fast-runtime", - feature = "runtime-benchmarks", - test -))] -parameter_types! { - pub const ChainForkVersions: ForkVersions = ForkVersions { - genesis: Fork { - version: [16, 0, 0, 56], // 0x10000038 - epoch: 0, - }, - altair: Fork { - version: [32, 0, 0, 56], // 0x20000038 - epoch: 0, - }, - bellatrix: Fork { - version: [48, 0, 0, 56], // 0x30000038 - epoch: 0, - }, - capella: Fork { - version: [64, 0, 0, 56], // 0x40000038 - epoch: 0, - }, - deneb: Fork { - version: [80, 0, 0, 56], // 0x50000038 - epoch: 0, - }, - electra: Fork { - version: [96, 0, 0, 56], // 0x60000038 - epoch: 0, - }, - }; -} - -// Holesky: https://github.com/eth-clients/holesky -// Fork versions: https://github.com/eth-clients/holesky/blob/main/metadata/config.yaml -#[cfg(not(any( - feature = "std", - feature = "fast-runtime", - feature = "runtime-benchmarks", - test -)))] parameter_types! { pub const ChainForkVersions: ForkVersions = ForkVersions { genesis: Fork { @@ -889,10 +861,82 @@ impl pallet_external_validators::Config for Runtime { type ValidatorRegistration = Session; type UnixTime = Timestamp; type SessionsPerEra = SessionsPerEra; - // TODO: Implement OnEraStart and OnEraEnd when ExternalValidatorsRewards is added - type OnEraStart = (); - type OnEraEnd = (); + type OnEraStart = ExternalValidatorsRewards; + type OnEraEnd = ExternalValidatorsRewards; type WeightInfo = (); #[cfg(feature = "runtime-benchmarks")] type Currency = Balances; } + +pub struct GetWhitelistedValidators; +impl Get> for GetWhitelistedValidators { + fn get() -> Vec { + pallet_external_validators::WhitelistedValidatorsActiveEra::::get().into() + } +} + +// Stub SendMessage implementation for rewards pallet +pub struct RewardsSendAdapter; +impl pallet_external_validators_rewards::types::SendMessage for RewardsSendAdapter { + type Message = OutboundMessage; + type Ticket = OutboundMessage; + fn build( + rewards_utils: &pallet_external_validators_rewards::types::EraRewardsUtils, + ) -> Option { + let selector = runtime_params::dynamic_params::runtime_config::RewardsUpdateSelector::get(); + + let mut calldata = Vec::new(); + calldata.extend_from_slice(&selector); + calldata.extend_from_slice(rewards_utils.rewards_merkle_root.as_bytes()); + + let command = Command::CallContract { + target: runtime_params::dynamic_params::runtime_config::RewardsRegistryAddress::get(), + calldata, + gas: 100_0000, // TODO: Determine appropriate gas value after testing + value: 0, + }; + let message = OutboundMessage { + origin: runtime_params::dynamic_params::runtime_config::RewardsOrigin::get(), + // TODO: Determine appropriate id value + id: unique(rewards_utils.rewards_merkle_root).into(), + fee: 0, + commands: match vec![command].try_into() { + Ok(cmds) => cmds, + Err(_) => { + log::error!( + target: "rewards_send_adapter", + "Failed to convert commands: too many commands" + ); + return None; + } + }, + }; + Some(message) + } + + fn validate(message: Self::Message) -> Result { + OutboundQueueV2::validate(&message) + } + fn deliver(message: Self::Ticket) -> Result { + OutboundQueueV2::deliver(message) + } +} + +impl pallet_external_validators_rewards::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type EraIndexProvider = ExternalValidators; + type HistoryDepth = ConstU32<64>; + type BackingPoints = ConstU32<20>; + type DisputeStatementPoints = ConstU32<20>; + type EraInflationProvider = ConstU128<0>; + type ExternalIndexProvider = ExternalValidators; + type GetWhitelistedValidators = GetWhitelistedValidators; + type Hashing = Keccak256; + type Currency = Balances; + type RewardsEthereumSovereignAccount = TreasuryAccountId; + type WeightInfo = (); + type SendMessage = RewardsSendAdapter; + type HandleInflation = (); + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} diff --git a/operator/runtime/stagenet/src/configs/runtime_params.rs b/operator/runtime/stagenet/src/configs/runtime_params.rs index a8b7ace8..24cfac1c 100644 --- a/operator/runtime/stagenet/src/configs/runtime_params.rs +++ b/operator/runtime/stagenet/src/configs/runtime_params.rs @@ -1,7 +1,9 @@ -use frame_support::dynamic_params::{dynamic_pallet_params, dynamic_params}; -use sp_core::H160; - use crate::Runtime; +use frame_support::dynamic_params::{dynamic_pallet_params, dynamic_params}; +use hex_literal::hex; +use sp_core::{ConstU32, H160, H256}; +use sp_runtime::BoundedVec; +use sp_std::vec; #[dynamic_params(RuntimeParameters, pallet_parameters::Parameters::)] pub mod dynamic_params { @@ -18,6 +20,26 @@ pub mod dynamic_params { /// The fact that this is a parameter means that we can set it initially to the zero address, /// and then change it later via governance, to the actual address of the deployed contract. pub static EthereumGatewayAddress: H160 = H160::repeat_byte(0x0); + + #[codec(index = 1)] + #[allow(non_upper_case_globals)] + /// Set the initial address of the Rewards Registry contract on Ethereum. + /// The fact that this is a parameter means that we can set it initially to the zero address, + /// and then change it later via governance, to the actual address of the deployed contract. + pub static RewardsRegistryAddress: H160 = H160::repeat_byte(0x0); + + #[codec(index = 2)] + #[allow(non_upper_case_globals)] + /// The Selector is the first 4 bytes of the keccak256 hash of the function signature("updateRewardsMerkleRoot(bytes32)") + pub static RewardsUpdateSelector: BoundedVec> = + BoundedVec::truncate_from(vec![0xdc, 0x3d, 0x04, 0xec]); + + #[codec(index = 3)] + #[allow(non_upper_case_globals)] + /// The Origin is the hash of the string "external_validators_rewards" + pub static RewardsOrigin: H256 = H256::from_slice(&hex!( + "c505dfb2df107d106d08bd0f1a0acd10052ca9aa078629a4ccfd0c90c6e69b65" + )); } } diff --git a/operator/runtime/stagenet/src/lib.rs b/operator/runtime/stagenet/src/lib.rs index 3c88a73d..56893b20 100644 --- a/operator/runtime/stagenet/src/lib.rs +++ b/operator/runtime/stagenet/src/lib.rs @@ -23,9 +23,11 @@ pub use frame_system::Call as SystemCall; pub use pallet_balances::Call as BalancesCall; use pallet_ethereum::{Call::transact, Transaction as EthereumTransaction}; use pallet_evm::{Account as EVMAccount, FeeCalculator, GasWeightMapping, Runner}; +use pallet_external_validators::traits::EraIndex; use pallet_grandpa::{fg_primitives, AuthorityId as GrandpaId}; pub use pallet_timestamp::Call as TimestampCall; use snowbridge_core::AgentId; +use snowbridge_merkle_tree::MerkleProof; use sp_api::impl_runtime_apis; use sp_consensus_beefy::{ ecdsa_crypto::{AuthorityId as BeefyId, Signature as BeefySignature}, @@ -340,6 +342,9 @@ mod runtime { // Start with index 100 #[runtime::pallet_index(100)] pub type OutboundCommitmentStore = pallet_outbound_commitment_store; + + #[runtime::pallet_index(101)] + pub type ExternalValidatorsRewards = pallet_external_validators_rewards; // ╚═══════════════════ DataHaven-specific Pallets ══════════════════╝ } @@ -777,6 +782,19 @@ impl_runtime_apis! { } } + impl pallet_external_validators_rewards_runtime_api::ExternalValidatorsRewardsApi for Runtime + where + EraIndex: codec::Codec, + { + fn generate_rewards_merkle_proof(account_id: AccountId, era_index: EraIndex) -> Option { + ExternalValidatorsRewards::generate_rewards_merkle_proof(account_id, era_index) + } + + fn verify_rewards_merkle_proof(merkle_proof: MerkleProof) -> bool { + ExternalValidatorsRewards::verify_rewards_merkle_proof(merkle_proof) + } + } + #[cfg(feature = "runtime-benchmarks")] impl frame_benchmarking::Benchmark for Runtime { fn benchmark_metadata(extra: bool) -> ( diff --git a/operator/runtime/testnet/Cargo.toml b/operator/runtime/testnet/Cargo.toml index 4ad85397..3c036ff6 100644 --- a/operator/runtime/testnet/Cargo.toml +++ b/operator/runtime/testnet/Cargo.toml @@ -31,6 +31,7 @@ frame-system-rpc-runtime-api = { workspace = true } frame-try-runtime = { workspace = true, optional = true } hex = { workspace = true } hex-literal = { workspace = true } +log = { workspace = true } pallet-authorship = { workspace = true } pallet-babe = { workspace = true } pallet-balances = { workspace = true } @@ -40,6 +41,8 @@ pallet-ethereum = { workspace = true } pallet-evm = { workspace = true } pallet-evm-chain-id = { workspace = true } pallet-external-validators = { workspace = true } +pallet-external-validators-rewards = { workspace = true } +pallet-external-validators-rewards-runtime-api = { workspace = true } pallet-grandpa = { workspace = true } pallet-identity = { workspace = true } pallet-im-online = { workspace = true } @@ -175,6 +178,8 @@ std = [ "substrate-wasm-builder", "pallet-outbound-commitment-store/std", "pallet-external-validators/std", + "pallet-external-validators-rewards/std", + "pallet-external-validators-rewards-runtime-api/std", ] runtime-benchmarks = [ @@ -212,6 +217,7 @@ runtime-benchmarks = [ "snowbridge-pallet-system/runtime-benchmarks", "pallet-outbound-commitment-store/runtime-benchmarks", "pallet-external-validators/runtime-benchmarks", + "pallet-external-validators-rewards/runtime-benchmarks", ] try-runtime = [ @@ -251,6 +257,7 @@ try-runtime = [ "snowbridge-pallet-system/try-runtime", "pallet-outbound-commitment-store/try-runtime", "pallet-external-validators/try-runtime", + "pallet-external-validators-rewards/try-runtime", ] fast-runtime = ["datahaven-runtime-common/fast-runtime"] diff --git a/operator/runtime/testnet/src/configs/mod.rs b/operator/runtime/testnet/src/configs/mod.rs index 995b75d2..a21828a4 100644 --- a/operator/runtime/testnet/src/configs/mod.rs +++ b/operator/runtime/testnet/src/configs/mod.rs @@ -27,11 +27,11 @@ mod runtime_params; use super::{ deposit, AccountId, Babe, Balance, Balances, BeefyMmrLeaf, Block, BlockNumber, - EthereumBeaconClient, EvmChainId, ExternalValidators, Hash, Historical, ImOnline, MessageQueue, - Nonce, Offences, OriginCaller, OutboundCommitmentStore, OutboundQueueV2, PalletInfo, Preimage, - Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, RuntimeHoldReason, RuntimeOrigin, - RuntimeTask, Session, SessionKeys, Signature, System, Timestamp, EXISTENTIAL_DEPOSIT, - SLOT_DURATION, STORAGE_BYTE_FEE, SUPPLY_FACTOR, UNIT, VERSION, + EthereumBeaconClient, EvmChainId, ExternalValidators, ExternalValidatorsRewards, Hash, + Historical, ImOnline, MessageQueue, Nonce, Offences, OriginCaller, OutboundCommitmentStore, + OutboundQueueV2, PalletInfo, Preimage, Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, + RuntimeHoldReason, RuntimeOrigin, RuntimeTask, Session, SessionKeys, Signature, System, + Timestamp, EXISTENTIAL_DEPOSIT, SLOT_DURATION, STORAGE_BYTE_FEE, SUPPLY_FACTOR, UNIT, VERSION, }; use codec::{Decode, Encode}; use datahaven_runtime_common::{ @@ -55,7 +55,7 @@ use frame_support::{ }; use frame_system::{ limits::{BlockLength, BlockWeights}, - EnsureRoot, EnsureRootWithSuccess, + unique, EnsureRoot, EnsureRootWithSuccess, }; use pallet_ethereum::PostLogContent; use pallet_evm::{ @@ -75,7 +75,7 @@ use snowbridge_core::{gwei, meth, AgentIdOf, PricingParameters, Rewards}; use snowbridge_inbound_queue_primitives::RewardLedger; use snowbridge_outbound_queue_primitives::{ v1::{Fee, Message, SendMessage}, - v2::ConstantGasMeter, + v2::{Command, ConstantGasMeter, Message as OutboundMessage, SendMessage as SendMessageV2}, SendError, SendMessageFeeProvider, }; use snowbridge_pallet_outbound_queue_v2::OnNewCommitment; @@ -127,6 +127,7 @@ parameter_types! { pub const MaxAuthorities: u32 = 32; pub const BondingDuration: EraIndex = polkadot_runtime_common::prod_or_fast!(28, 3); pub const SessionsPerEra: SessionIndex = polkadot_runtime_common::prod_or_fast!(6, 1); + pub const AuthorRewardPoints: u32 = 20; } //╔═══════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ @@ -236,9 +237,22 @@ impl pallet_balances::Config for Runtime { type DoneSlashHandler = (); } +pub struct RewardsPoints; + +impl pallet_authorship::EventHandler for RewardsPoints { + fn note_author(author: AccountId) { + let whitelisted_validators = + pallet_external_validators::WhitelistedValidatorsActiveEra::::get(); + // Do not reward whitelisted validators + if !whitelisted_validators.contains(&author) { + ExternalValidatorsRewards::reward_by_ids(vec![(author, AuthorRewardPoints::get())]) + } + } +} + impl pallet_authorship::Config for Runtime { type FindAuthor = pallet_session::FindAccountFromAuthorIndex; - type EventHandler = ImOnline; + type EventHandler = (RewardsPoints, ImOnline); } impl pallet_offences::Config for Runtime { @@ -636,6 +650,7 @@ parameter_types! { multiplier: FixedU128::from_rational(1, 1), }; pub EthereumLocation: Location = Location::new(1, EthereumNetwork::get()); + // TODO: Change to the actual treasury account pub TreasuryAccountId: AccountId = AccountId::from([0u8; 20]); } @@ -886,10 +901,82 @@ impl pallet_external_validators::Config for Runtime { type ValidatorRegistration = Session; type UnixTime = Timestamp; type SessionsPerEra = SessionsPerEra; - // TODO: Implement OnEraStart and OnEraEnd when ExternalValidatorsRewards is added - type OnEraStart = (); - type OnEraEnd = (); + type OnEraStart = ExternalValidatorsRewards; + type OnEraEnd = ExternalValidatorsRewards; type WeightInfo = (); #[cfg(feature = "runtime-benchmarks")] type Currency = Balances; } + +pub struct GetWhitelistedValidators; +impl Get> for GetWhitelistedValidators { + fn get() -> Vec { + pallet_external_validators::WhitelistedValidatorsActiveEra::::get().into() + } +} + +// Stub SendMessage implementation for rewards pallet +pub struct RewardsSendAdapter; +impl pallet_external_validators_rewards::types::SendMessage for RewardsSendAdapter { + type Message = OutboundMessage; + type Ticket = OutboundMessage; + fn build( + rewards_utils: &pallet_external_validators_rewards::types::EraRewardsUtils, + ) -> Option { + let selector = runtime_params::dynamic_params::runtime_config::RewardsUpdateSelector::get(); + + let mut calldata = Vec::new(); + calldata.extend_from_slice(&selector); + calldata.extend_from_slice(rewards_utils.rewards_merkle_root.as_bytes()); + + let command = Command::CallContract { + target: runtime_params::dynamic_params::runtime_config::RewardsRegistryAddress::get(), + calldata, + gas: 100_0000, // TODO: Determine appropriate gas value after testing + value: 0, + }; + let message = OutboundMessage { + origin: runtime_params::dynamic_params::runtime_config::RewardsOrigin::get(), + // TODO: Determine appropriate id value + id: unique(rewards_utils.rewards_merkle_root).into(), + fee: 0, + commands: match vec![command].try_into() { + Ok(cmds) => cmds, + Err(_) => { + log::error!( + target: "rewards_send_adapter", + "Failed to convert commands: too many commands" + ); + return None; + } + }, + }; + Some(message) + } + + fn validate(message: Self::Message) -> Result { + OutboundQueueV2::validate(&message) + } + fn deliver(message: Self::Ticket) -> Result { + OutboundQueueV2::deliver(message) + } +} + +impl pallet_external_validators_rewards::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type EraIndexProvider = ExternalValidators; + type HistoryDepth = ConstU32<64>; + type BackingPoints = ConstU32<20>; + type DisputeStatementPoints = ConstU32<20>; + type EraInflationProvider = ConstU128<0>; + type ExternalIndexProvider = ExternalValidators; + type GetWhitelistedValidators = GetWhitelistedValidators; + type Hashing = Keccak256; + type Currency = Balances; + type RewardsEthereumSovereignAccount = TreasuryAccountId; + type WeightInfo = (); + type SendMessage = RewardsSendAdapter; + type HandleInflation = (); + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} diff --git a/operator/runtime/testnet/src/configs/runtime_params.rs b/operator/runtime/testnet/src/configs/runtime_params.rs index a8b7ace8..38781ef4 100644 --- a/operator/runtime/testnet/src/configs/runtime_params.rs +++ b/operator/runtime/testnet/src/configs/runtime_params.rs @@ -1,5 +1,8 @@ use frame_support::dynamic_params::{dynamic_pallet_params, dynamic_params}; -use sp_core::H160; +use hex_literal::hex; +use sp_core::{ConstU32, H160, H256}; +use sp_runtime::BoundedVec; +use sp_std::vec; use crate::Runtime; @@ -18,6 +21,26 @@ pub mod dynamic_params { /// The fact that this is a parameter means that we can set it initially to the zero address, /// and then change it later via governance, to the actual address of the deployed contract. pub static EthereumGatewayAddress: H160 = H160::repeat_byte(0x0); + + #[codec(index = 1)] + #[allow(non_upper_case_globals)] + /// Set the initial address of the Rewards Registry contract on Ethereum. + /// The fact that this is a parameter means that we can set it initially to the zero address, + /// and then change it later via governance, to the actual address of the deployed contract. + pub static RewardsRegistryAddress: H160 = H160::repeat_byte(0x0); + + #[codec(index = 2)] + #[allow(non_upper_case_globals)] + /// The Selector is the first 4 bytes of the keccak256 hash of the function signature("updateRewardsMerkleRoot(bytes32)") + pub static RewardsUpdateSelector: BoundedVec> = + BoundedVec::truncate_from(vec![0xdc, 0x3d, 0x04, 0xec]); + + #[codec(index = 3)] + #[allow(non_upper_case_globals)] + /// The Origin is the hash of the string "external_validators_rewards" + pub static RewardsOrigin: H256 = H256::from_slice(&hex!( + "c505dfb2df107d106d08bd0f1a0acd10052ca9aa078629a4ccfd0c90c6e69b65" + )); } } diff --git a/operator/runtime/testnet/src/lib.rs b/operator/runtime/testnet/src/lib.rs index c6c285d1..4e99e62d 100644 --- a/operator/runtime/testnet/src/lib.rs +++ b/operator/runtime/testnet/src/lib.rs @@ -23,9 +23,11 @@ pub use frame_system::Call as SystemCall; pub use pallet_balances::Call as BalancesCall; use pallet_ethereum::{Call::transact, Transaction as EthereumTransaction}; use pallet_evm::{Account as EVMAccount, FeeCalculator, GasWeightMapping, Runner}; +use pallet_external_validators::traits::EraIndex; use pallet_grandpa::{fg_primitives, AuthorityId as GrandpaId}; pub use pallet_timestamp::Call as TimestampCall; use snowbridge_core::AgentId; +use snowbridge_merkle_tree::MerkleProof; use sp_api::impl_runtime_apis; use sp_consensus_beefy::{ ecdsa_crypto::{AuthorityId as BeefyId, Signature as BeefySignature}, @@ -341,6 +343,9 @@ mod runtime { #[runtime::pallet_index(100)] pub type OutboundCommitmentStore = pallet_outbound_commitment_store; + #[runtime::pallet_index(101)] + pub type ExternalValidatorsRewards = pallet_external_validators_rewards; + // ╚═══════════════════ DataHaven-specific Pallets ══════════════════╝ } @@ -778,6 +783,19 @@ impl_runtime_apis! { } } + impl pallet_external_validators_rewards_runtime_api::ExternalValidatorsRewardsApi for Runtime + where + EraIndex: codec::Codec, + { + fn generate_rewards_merkle_proof(account_id: AccountId, era_index: EraIndex) -> Option { + ExternalValidatorsRewards::generate_rewards_merkle_proof(account_id, era_index) + } + + fn verify_rewards_merkle_proof(merkle_proof: MerkleProof) -> bool { + ExternalValidatorsRewards::verify_rewards_merkle_proof(merkle_proof) + } + } + #[cfg(feature = "runtime-benchmarks")] impl frame_benchmarking::Benchmark for Runtime { fn benchmark_metadata(extra: bool) -> (