feat(operator): Add External Validators Rewards Pallet (#72)

This PR introduces and integrates the new
`pallet-external-validators-rewards` into the operator runtimes. This
pallet is responsible for managing and distributing rewards to external
validators. The pallet was originally introduced by Tanssi. It was
heavily modified to abstract the message building and sending with
Snowbridge.

### Key Changes Related to `pallet-external-validators-rewards`:

*   **Pallet and Runtime API Addition:**
* The `pallet-external-validators-rewards` has been added to `mainnet`,
`stagenet`, and `testnet` runtimes

* **Runtime Configuration
(`operator/runtime/{mainnet,stagenet,testnet}/src/configs/mod.rs`):**
* **`RewardsRegistryAddress` Parameter:** A new dynamic runtime
parameter `RewardsRegistryAddress` (type `H160`) has been added to
`runtime_params.rs`. This will hold the Ethereum address of the Rewards
Registry contract.
    *   **Author Rewards with `RewardsPoints`:**
* A new struct `RewardsPoints` implements
`pallet_authorship::EventHandler`.
* Its `note_author` function rewards block authors (if they are not
whitelisted validators) by calling
`ExternalValidatorsRewards::reward_by_ids`.
* The `EventHandler` in `pallet_authorship::Config` is updated to
`(RewardsPoints, ImOnline)`.
    *   **Integration with `pallet-external-validators`:**
* The `OnEraStart` and `OnEraEnd` hooks in
`pallet_external_validators::Config` are now set to
`ExternalValidatorsRewards`, allowing the rewards pallet to react to era
changes.
    *   **`RewardsSendAdapter` for Ethereum Communication:**
* A new struct `RewardsSendAdapter` implements
`pallet_external_validators_rewards::types::SendMessage`.
* `build()`: Constructs an `OutboundMessage` to call the
`updateRewardsMerkleRoot(bytes32)` function on the
`RewardsRegistryAddress` on Ethereum. The calldata includes the selector
(hashed from `UpdateRewardsMerkleRootSignature`) and the
`rewards_merkle_root`.
* `validate()` and `deliver()`: Utilize `OutboundQueueV2` for message
validation and delivery.
* A constant `UpdateRewardsMerkleRootSignature` (
`b"updateRewardsMerkleRoot(bytes32)"`) is defined for this purpose.
  
* **Runtime API
(`operator/runtime/{mainnet,stagenet,testnet}/src/lib.rs`):**
* The runtime implements
`pallet_external_validators_rewards_runtime_api::ExternalValidatorsRewardsApi`.
    *   This exposes two functions:
* `generate_rewards_merkle_proof(account_id: AccountId, era_index:
EraIndex) -> Option<MerkleProof>`
* `verify_rewards_merkle_proof(merkle_proof: MerkleProof) -> bool`

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced a rewards system for external validators with era-based
tracking, Merkle proof generation, and verification.
- Added runtime APIs to generate and verify validator rewards Merkle
proofs.
- Enabled rewards distribution via outbound messaging to an external
registry contract.
  - Configured runtime parameters for the rewards registry address.
- Added fixed author reward points and integrated rewards logic into
block authorship events.
- Provided a mock runtime environment and benchmarking support for the
rewards pallet.

- **Tests**
- Added comprehensive tests and benchmarks for validator rewards
distribution and Merkle proof functionality.

- **Chores**
- Integrated new pallets and runtime APIs into mainnet, stagenet, and
testnet configurations.
- Updated dependencies and feature groups to include new rewards-related
pallets.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
This commit is contained in:
Ahmad Kaouk 2025-05-22 02:51:24 +02:00 committed by GitHub
parent c29fc8f06d
commit d76f70686e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1774 additions and 85 deletions

46
operator/Cargo.lock generated
View file

@ -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"

View file

@ -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 }

View file

@ -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",
]

View file

@ -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",
]

View file

@ -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 <http://www.gnu.org/licenses/>
//! 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<AccountId, EraIndex>
where
AccountId: parity_scale_codec::Codec,
EraIndex: parity_scale_codec::Codec,
{
fn generate_rewards_merkle_proof(account_id: AccountId, era_index: EraIndex) -> Option<MerkleProof>;
fn verify_rewards_merkle_proof(merkle_proof: MerkleProof) -> bool;
}
}

View file

@ -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 <http://www.gnu.org/licenses/>
//! 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<T: Config + pallet_balances::Config>(
string: &'static str,
n: u32,
balance_factor: u32,
) -> T::AccountId {
let user = account(string, n, SEED);
let balance = <pallet_balances::Pallet<T> as Currency<T::AccountId>>::minimum_balance()
* balance_factor.into();
let _ = <pallet_balances::Pallet<T> as Currency<T::AccountId>>::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::<T>::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::<T>("candidate", i, 100);
era_reward_points.individual.insert(account_id, 20);
}
T::BenchmarkHelper::setup();
<RewardPointsForEra<T>>::insert(1u32, era_reward_points);
#[block]
{
<ExternalValidatorsRewards<T> as OnEraEnd>::on_era_end(1u32);
}
Ok(())
}
impl_benchmark_test_suite!(
ExternalValidatorsRewards,
crate::mock::new_test_ext(),
crate::mock::Test,
);
}

View file

@ -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 <http://www.gnu.org/licenses/>
//! 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<Event<Self>> + IsType<<Self as frame_system::Config>::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<EraIndex>;
/// The amount of era points given by backing a candidate that is included.
#[pallet::constant]
type BackingPoints: Get<u32>;
/// The amount of era points given by dispute voting on a candidate.
#[pallet::constant]
type DisputeStatementPoints: Get<u32>;
/// Provider to know how may tokens were inflated (added) in a specific era.
type EraInflationProvider: Get<u128>;
/// Provider to retrieve the current external index indetifying the validators
type ExternalIndexProvider: ExternalIndexProvider;
type GetWhitelistedValidators: Get<Vec<Self::AccountId>>;
/// Hashing tool used to generate/verify merkle roots and proofs.
type Hashing: Hash<Output = H256>;
/// Currency the rewards are minted in
type Currency: fungible::Inspect<Self::AccountId, Balance: From<u128>>
+ fungible::Mutate<Self::AccountId>;
/// Ethereum Sovereign Account where rewards will be minted
type RewardsEthereumSovereignAccount: Get<Self::AccountId>;
/// 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<Self::AccountId>;
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper: types::BenchmarkHelper;
}
#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet<T>(_);
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// 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<AccountId> {
pub total: RewardPoints,
pub individual: BTreeMap<AccountId, RewardPoints>,
}
impl<AccountId: Ord + sp_runtime::traits::Debug + Parameter> EraRewardPoints<AccountId> {
// 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<Hasher: sp_runtime::traits::Hash<Output = H256>>(
&self,
era_index: EraIndex,
maybe_account_id_check: Option<AccountId>,
) -> Option<EraRewardsUtils> {
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 = <Hasher as sp_runtime::traits::Hash>::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::<Hasher, _>(leaves.iter().cloned());
Some(EraRewardsUtils {
rewards_merkle_root,
leaves,
leaf_index,
total_points,
})
}
}
impl<AccountId> Default for EraRewardPoints<AccountId> {
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<T: Config> =
StorageMap<_, Twox64Concat, EraIndex, EraRewardPoints<T::AccountId>, ValueQuery>;
impl<T: Config> Pallet<T> {
/// 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<Item = (T::AccountId, RewardPoints)>) {
let active_era = T::EraIndexProvider::active_era();
RewardPointsForEra::<T>::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<MerkleProof> {
let era_rewards = RewardPointsForEra::<T>::get(&era_index);
let utils = era_rewards.generate_era_rewards_utils::<<T as Config>::Hashing>(
era_index,
Some(account_id),
)?;
utils.leaf_index.map(|index| {
merkle_proof::<<T as Config>::Hashing, _>(utils.leaves.into_iter(), index)
})
}
pub fn verify_rewards_merkle_proof(merkle_proof: MerkleProof) -> bool {
verify_proof::<<T as Config>::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<H256> {
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<T: Config> OnEraStart for Pallet<T> {
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::<T>::remove(era_index_to_delete);
}
}
impl<T: Config> OnEraEnd for Pallet<T> {
fn on_era_end(era_index: EraIndex) {
let utils = match RewardPointsForEra::<T>::get(&era_index)
.generate_era_rewards_utils::<<T as Config>::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(&ethereum_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::<T>::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<C>(core::marker::PhantomData<C>);
impl<C> RewardValidatorsWithEraPoints<C>
where
C: pallet::Config + session_info::Config,
C::ValidatorSet: ValidatorSet<C::AccountId, ValidatorId = C::AccountId>,
{
/// 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<Item = ValidatorIndex>,
points: u32,
) {
let validators = session_info::AccountKeys::<C>::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::<C>::reward_by_ids(rewards);
}
}
impl<C> runtime_parachains::inclusion::RewardValidators for RewardValidatorsWithEraPoints<C>
where
C: pallet::Config + runtime_parachains::shared::Config + session_info::Config,
C::ValidatorSet: ValidatorSet<C::AccountId, ValidatorId = C::AccountId>,
{
fn reward_backing(indices: impl IntoIterator<Item = ValidatorIndex>) {
let session_index = runtime_parachains::shared::CurrentSessionIndex::<C>::get();
Self::reward_only_active(session_index, indices, C::BackingPoints::get());
}
fn reward_bitfields(_validators: impl IntoIterator<Item = ValidatorIndex>) {}
}
impl<C> runtime_parachains::disputes::RewardValidators for RewardValidatorsWithEraPoints<C>
where
C: pallet::Config + session_info::Config,
C::ValidatorSet: ValidatorSet<C::AccountId, ValidatorId = C::AccountId>,
{
fn reward_dispute_statement(
session: SessionIndex,
validators: impl IntoIterator<Item = ValidatorIndex>,
) {
Self::reward_only_active(session, validators, C::DisputeStatementPoints::get());
}
}

View file

@ -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 <http://www.gnu.org/licenses/>
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<Test>;
// 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<Self::AccountId>;
type RuntimeEvent = RuntimeEvent;
type BlockHashCount = BlockHashCount;
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = AccountData<u128>;
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<Self::Ticket> {
Some(())
}
fn validate(_: Self::Ticket) -> Result<Self::Ticket, SendError> {
Ok(())
}
fn deliver(_: Self::Ticket) -> Result<H256, SendError> {
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<Test>;
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<u64> 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"));
}
<Test as pallet_external_validators_rewards::Config>::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<ActiveEraInfo>,
pub era_inflation: Option<u128>,
}
#[pallet::config]
pub trait Config: frame_system::Config {}
#[pallet::call]
impl<T: Config> Pallet<T> {}
#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);
#[pallet::storage]
pub(super) type Mock<T: Config> = StorageValue<_, Mocks, ValueQuery>;
impl<T: Config> Pallet<T> {
pub fn mock() -> Mocks {
Mock::<T>::get()
}
pub fn mutate<F, R>(f: F) -> R
where
F: FnOnce(&mut Mocks) -> R,
{
Mock::<T>::mutate(f)
}
}
impl<T: Config> EraIndexProvider for Pallet<T> {
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<u32> {
unimplemented!()
}
}
}
pub fn new_test_ext() -> sp_io::TestExternalities {
let mut t = frame_system::GenesisConfig::<Test>::default()
.build_storage()
.unwrap();
let balances = vec![(1, 100), (2, 100), (3, 100), (4, 100), (5, 100)];
pallet_balances::GenesisConfig::<Test> { 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);
}
}

View file

@ -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 <http://www.gnu.org/licenses/>
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::<Test>::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::<Test>::iter().count();
assert_eq!(storage_eras, 1);
let era_points = pallet_external_validators_rewards::RewardPointsForEra::<Test>::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::<Test>::iter().count();
assert_eq!(storage_eras, 1);
ExternalValidatorsRewards::on_era_start(10, 0, 10);
let storage_eras =
pallet_external_validators_rewards::RewardPointsForEra::<Test>::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::<Test>::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::<Test>::get(1);
let rewards_utils = era_rewards.generate_era_rewards_utils::<<Test as pallet_external_validators_rewards::Config>::Hashing>(1, None);
let root = rewards_utils.unwrap().rewards_merkle_root;
let inflation = <Test as pallet_external_validators_rewards::Config>::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::<Test>::get(1);
let rewards_utils = era_rewards.generate_era_rewards_utils::<<Test as pallet_external_validators_rewards::Config>::Hashing>(1, None);
let root = rewards_utils.unwrap().rewards_merkle_root;
let inflation = <Test as pallet_external_validators_rewards::Config>::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::<Test>::get(1);
let rewards_utils = era_rewards.generate_era_rewards_utils::<<Test as pallet_external_validators_rewards::Config>::Hashing>(1, None);
let root = rewards_utils.unwrap().rewards_merkle_root;
let inflation = <Test as pallet_external_validators_rewards::Config>::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",
);
})
}

View file

@ -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<H256, SendError>;
}
/// 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<H256>,
pub leaf_index: Option<u64>,
pub total_points: u128,
}
pub trait SendMessage {
type Message;
type Ticket;
fn build(utils: &EraRewardsUtils) -> Option<Self::Message>;
fn validate(message: Self::Message) -> Result<Self::Ticket, SendError>;
fn deliver(ticket: Self::Ticket) -> Result<H256, SendError>;
}
// Trait for handling inflation
pub trait HandleInflation<AccountId> {
fn mint_inflation(who: &AccountId, amount: u128) -> sp_runtime::DispatchResult;
}
impl<AccountId> HandleInflation<AccountId> 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() {
()
}
}

View file

@ -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 <http://www.gnu.org/licenses/>
//! 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<T>(PhantomData<T>);
impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
/// 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))
}
}

View file

@ -49,7 +49,7 @@ use {
#[cfg(test)]
mod mock;
mod traits;
pub mod traits;
#[cfg(test)]
mod tests;

View file

@ -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",

View file

@ -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<AccountId, BlockNumber> for RewardsPoints {
fn note_author(author: AccountId) {
let whitelisted_validators =
pallet_external_validators::WhitelistedValidatorsActiveEra::<Runtime>::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<Self, Babe>;
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<Vec<AccountId>> for GetWhitelistedValidators {
fn get() -> Vec<AccountId> {
pallet_external_validators::WhitelistedValidatorsActiveEra::<Runtime>::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<Self::Message> {
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<Self::Ticket, SendError> {
OutboundQueueV2::validate(&message)
}
fn deliver(message: Self::Ticket) -> Result<H256, SendError> {
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 = ();
}

View file

@ -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::<Runtime>)]
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<u8, ConstU32<4>> =
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"
));
}
}

View file

@ -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<Block, AccountId, EraIndex> for Runtime
where
EraIndex: codec::Codec,
{
fn generate_rewards_merkle_proof(account_id: AccountId, era_index: EraIndex) -> Option<MerkleProof> {
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<Block> for Runtime {
fn benchmark_metadata(extra: bool) -> (

View file

@ -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",

View file

@ -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<AccountId, BlockNumber> for RewardsPoints {
fn note_author(author: AccountId) {
let whitelisted_validators =
pallet_external_validators::WhitelistedValidatorsActiveEra::<Runtime>::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<Self, Babe>;
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<Vec<AccountId>> for GetWhitelistedValidators {
fn get() -> Vec<AccountId> {
pallet_external_validators::WhitelistedValidatorsActiveEra::<Runtime>::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<Self::Message> {
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<Self::Ticket, SendError> {
OutboundQueueV2::validate(&message)
}
fn deliver(message: Self::Ticket) -> Result<H256, SendError> {
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 = ();
}

View file

@ -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::<Runtime>)]
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<u8, ConstU32<4>> =
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"
));
}
}

View file

@ -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<Block, AccountId, EraIndex> for Runtime
where
EraIndex: codec::Codec,
{
fn generate_rewards_merkle_proof(account_id: AccountId, era_index: EraIndex) -> Option<MerkleProof> {
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<Block> for Runtime {
fn benchmark_metadata(extra: bool) -> (

View file

@ -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"]

View file

@ -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<AccountId, BlockNumber> for RewardsPoints {
fn note_author(author: AccountId) {
let whitelisted_validators =
pallet_external_validators::WhitelistedValidatorsActiveEra::<Runtime>::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<Self, Babe>;
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<Vec<AccountId>> for GetWhitelistedValidators {
fn get() -> Vec<AccountId> {
pallet_external_validators::WhitelistedValidatorsActiveEra::<Runtime>::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<Self::Message> {
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<Self::Ticket, SendError> {
OutboundQueueV2::validate(&message)
}
fn deliver(message: Self::Ticket) -> Result<H256, SendError> {
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 = ();
}

View file

@ -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<u8, ConstU32<4>> =
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"
));
}
}

View file

@ -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<Block, AccountId, EraIndex> for Runtime
where
EraIndex: codec::Codec,
{
fn generate_rewards_merkle_proof(account_id: AccountId, era_index: EraIndex) -> Option<MerkleProof> {
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<Block> for Runtime {
fn benchmark_metadata(extra: bool) -> (