1
// Copyright (C) Moondance Labs Ltd.
2
// This file is part of Tanssi.
3

            
4
// Tanssi is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8

            
9
// Tanssi is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13

            
14
// You should have received a copy of the GNU General Public License
15
// along with Tanssi.  If not, see <http://www.gnu.org/licenses/>
16

            
17
//! This pallet keep tracks of the validators reward points.
18
//! Storage will be cleared after a period of time.
19

            
20
#![cfg_attr(not(feature = "std"), no_std)]
21
extern crate alloc;
22

            
23
#[cfg(test)]
24
mod mock;
25

            
26
#[cfg(test)]
27
mod tests;
28

            
29
#[cfg(feature = "runtime-benchmarks")]
30
mod benchmarking;
31

            
32
pub mod weights;
33

            
34
pub use pallet::*;
35

            
36
use {
37
    alloc::{collections::btree_set::BTreeSet, vec::Vec},
38
    frame_support::traits::{
39
        fungible::{self, Mutate},
40
        Defensive, Get, ValidatorSet,
41
    },
42
    parity_scale_codec::Encode,
43
    polkadot_primitives::ValidatorIndex,
44
    runtime_parachains::session_info,
45
    snowbridge_core::{ChannelId, TokenId},
46
    snowbridge_merkle_tree::{merkle_proof, merkle_root, verify_proof, MerkleProof},
47
    sp_core::H256,
48
    sp_runtime::traits::{Hash, MaybeEquivalence, Zero},
49
    sp_staking::SessionIndex,
50
    tp_bridge::{Command, DeliverMessage, TanssiMessage, TicketInfo, ValidateMessage},
51
    tp_traits::ExternalIndexProvider,
52
    xcm::prelude::*,
53
};
54

            
55
/// Utils needed to generate/verify merkle roots/proofs inside this pallet.
56
#[derive(Debug, PartialEq, Eq, Clone)]
57
pub struct EraRewardsUtils {
58
    pub rewards_merkle_root: H256,
59
    pub leaves: Vec<H256>,
60
    pub leaf_index: Option<u64>,
61
    pub total_points: u128,
62
}
63

            
64
#[frame_support::pallet]
65
pub mod pallet {
66
    pub use crate::weights::WeightInfo;
67
    use {
68
        super::*, alloc::collections::btree_map::BTreeMap, frame_support::pallet_prelude::*,
69
        sp_runtime::Saturating, tp_traits::EraIndexProvider,
70
    };
71

            
72
    /// The current storage version.
73
    const STORAGE_VERSION: StorageVersion = StorageVersion::new(0);
74

            
75
    pub type RewardPoints = u32;
76
    pub type EraIndex = u32;
77

            
78
    #[pallet::config]
79
    pub trait Config: frame_system::Config {
80
        /// How to fetch the current era info.
81
        type EraIndexProvider: EraIndexProvider;
82

            
83
        /// For how many eras points are kept in storage.
84
        #[pallet::constant]
85
        type HistoryDepth: Get<EraIndex>;
86

            
87
        /// The amount of era points given by backing a candidate that is included.
88
        #[pallet::constant]
89
        type BackingPoints: Get<u32>;
90

            
91
        /// The amount of era points given by dispute voting on a candidate.
92
        #[pallet::constant]
93
        type DisputeStatementPoints: Get<u32>;
94

            
95
        /// Provider to know how may tokens were inflated (added) in a specific era.
96
        type EraInflationProvider: Get<u128>;
97

            
98
        /// Provider to retrieve the current external index indetifying the validators
99
        type ExternalIndexProvider: ExternalIndexProvider;
100

            
101
        type GetWhitelistedValidators: Get<Vec<Self::AccountId>>;
102

            
103
        /// Hashing tool used to generate/verify merkle roots and proofs.
104
        type Hashing: Hash<Output = H256>;
105

            
106
        /// Validate a message that will be sent to Ethereum.
107
        type ValidateMessage: ValidateMessage;
108

            
109
        /// Send a message to Ethereum. Needs to be validated first.
110
        type OutboundQueue: DeliverMessage<
111
            Ticket = <<Self as pallet::Config>::ValidateMessage as ValidateMessage>::Ticket,
112
        >;
113

            
114
        /// Currency the rewards are minted in
115
        type Currency: fungible::Inspect<Self::AccountId, Balance: From<u128>>
116
            + fungible::Mutate<Self::AccountId>;
117

            
118
        /// Ethereum Sovereign Account where rewards will be minted
119
        type RewardsEthereumSovereignAccount: Get<Self::AccountId>;
120

            
121
        /// Token Location from the external chain's point of view.
122
        type TokenLocationReanchored: Get<Location>;
123

            
124
        /// How to convert from a given Location to a specific TokenId.
125
        type TokenIdFromLocation: MaybeEquivalence<TokenId, Location>;
126

            
127
        /// The weight information of this pallet.
128
        type WeightInfo: WeightInfo;
129

            
130
        #[cfg(feature = "runtime-benchmarks")]
131
        type BenchmarkHelper: tp_bridge::TokenChannelSetterBenchmarkHelperTrait;
132
    }
133

            
134
    #[pallet::pallet]
135
    #[pallet::storage_version(STORAGE_VERSION)]
136
    pub struct Pallet<T>(_);
137

            
138
    #[pallet::event]
139
    #[pallet::generate_deposit(pub(super) fn deposit_event)]
140
    pub enum Event<T: Config> {
141
        /// The rewards message was sent correctly.
142
        RewardsMessageSent {
143
            message_id: H256,
144
            rewards_command: Command,
145
        },
146
    }
147

            
148
    /// Keep tracks of distributed points per validator and total.
149
    #[derive(RuntimeDebug, Encode, Decode, PartialEq, Eq, TypeInfo)]
150
    pub struct EraRewardPoints<AccountId> {
151
        pub total: RewardPoints,
152
        pub individual: BTreeMap<AccountId, RewardPoints>,
153
    }
154

            
155
    impl<AccountId: Ord + sp_runtime::traits::Debug + Parameter> EraRewardPoints<AccountId> {
156
        // Helper function used to generate the following utils:
157
        //  - rewards_merkle_root: merkle root corresponding [(validatorId, rewardPoints)]
158
        //      for the era_index specified.
159
        //  - leaves: that were used to generate the previous merkle root.
160
        //  - leaf_index: index of the validatorId's leaf in the previous leaves array (if any).
161
        //  - total_points: number of total points of the era_index specified.
162
53
        pub fn generate_era_rewards_utils<Hasher: sp_runtime::traits::Hash<Output = H256>>(
163
53
            &self,
164
53
            era_index: EraIndex,
165
53
            maybe_account_id_check: Option<AccountId>,
166
53
        ) -> Option<EraRewardsUtils> {
167
53
            let total_points: u128 = u128::from(self.total);
168
53
            let mut leaves = Vec::with_capacity(self.individual.len());
169
53
            let mut leaf_index = None;
170

            
171
53
            if let Some(account) = &maybe_account_id_check {
172
24
                if !self.individual.contains_key(account) {
173
18
                    log::error!(
174
18
                        target: "ext_validators_rewards",
175
18
                        "AccountId {:?} not found for era {:?}!",
176
                        account,
177
                        era_index
178
                    );
179
18
                    return None;
180
6
                }
181
29
            }
182

            
183
49
            for (index, (account_id, reward_points)) in self.individual.iter().enumerate() {
184
47
                let encoded = (account_id, reward_points).encode();
185
47
                let hashed = <Hasher as sp_runtime::traits::Hash>::hash(&encoded);
186

            
187
47
                leaves.push(hashed);
188

            
189
47
                if let Some(ref check_account_id) = maybe_account_id_check {
190
10
                    if account_id == check_account_id {
191
6
                        leaf_index = Some(index as u64);
192
6
                    }
193
37
                }
194
            }
195

            
196
35
            let rewards_merkle_root = merkle_root::<Hasher, _>(leaves.iter().cloned());
197

            
198
35
            Some(EraRewardsUtils {
199
35
                rewards_merkle_root,
200
35
                leaves,
201
35
                leaf_index,
202
35
                total_points,
203
35
            })
204
53
        }
205
    }
206

            
207
    impl<AccountId> Default for EraRewardPoints<AccountId> {
208
48
        fn default() -> Self {
209
48
            EraRewardPoints {
210
48
                total: Default::default(),
211
48
                individual: BTreeMap::new(),
212
48
            }
213
48
        }
214
    }
215

            
216
    /// Store reward points per era.
217
    /// Note: EraRewardPoints is actually bounded by the amount of validators.
218
    #[pallet::storage]
219
    #[pallet::unbounded]
220
    pub type RewardPointsForEra<T: Config> =
221
        StorageMap<_, Twox64Concat, EraIndex, EraRewardPoints<T::AccountId>, ValueQuery>;
222

            
223
    impl<T: Config> Pallet<T> {
224
        /// Reward validators. Does not check if the validators are valid, caller needs to make sure of that.
225
465
        pub fn reward_by_ids(points: impl IntoIterator<Item = (T::AccountId, RewardPoints)>) {
226
465
            let active_era = T::EraIndexProvider::active_era();
227

            
228
465
            RewardPointsForEra::<T>::mutate(active_era.index, |era_rewards| {
229
479
                for (validator, points) in points.into_iter() {
230
467
                    (*era_rewards.individual.entry(validator.clone()).or_default())
231
467
                        .saturating_accrue(points);
232
467
                    era_rewards.total.saturating_accrue(points);
233
467
                }
234
465
            })
235
465
        }
236

            
237
24
        pub fn generate_rewards_merkle_proof(
238
24
            account_id: T::AccountId,
239
24
            era_index: EraIndex,
240
24
        ) -> Option<MerkleProof> {
241
24
            let era_rewards = RewardPointsForEra::<T>::get(era_index);
242
24
            let utils = era_rewards.generate_era_rewards_utils::<<T as Config>::Hashing>(
243
24
                era_index,
244
24
                Some(account_id),
245
18
            )?;
246
6
            utils.leaf_index.map(|index| {
247
6
                merkle_proof::<<T as Config>::Hashing, _>(utils.leaves.into_iter(), index)
248
6
            })
249
24
        }
250

            
251
6
        pub fn verify_rewards_merkle_proof(merkle_proof: MerkleProof) -> bool {
252
6
            verify_proof::<<T as Config>::Hashing, _, _>(
253
6
                &merkle_proof.root,
254
6
                merkle_proof.proof,
255
6
                merkle_proof.number_of_leaves,
256
6
                merkle_proof.leaf_index,
257
6
                merkle_proof.leaf,
258
            )
259
6
        }
260
    }
261

            
262
    impl<T: Config> tp_traits::OnEraStart for Pallet<T> {
263
903
        fn on_era_start(era_index: EraIndex, _session_start: u32, _external_idx: u64) {
264
903
            let Some(era_index_to_delete) = era_index.checked_sub(T::HistoryDepth::get()) else {
265
901
                return;
266
            };
267

            
268
2
            RewardPointsForEra::<T>::remove(era_index_to_delete);
269
903
        }
270
    }
271

            
272
    impl<T: Config> tp_traits::OnEraEnd for Pallet<T> {
273
176
        fn on_era_end(era_index: EraIndex) {
274
            // Will send a ReportRewards message to Ethereum unless:
275
            // - the reward token is misconfigured
276
            // - the tokens inflation is 0 (misconfigured inflation)
277
            // - the total points is 0 (no rewards to distribute)
278
            // - it fails to mint the tokens in the Ethereum Sovereign Account
279
            // - the generated message doesn't pass validation
280

            
281
176
            let token_location = T::TokenLocationReanchored::get();
282
176
            let token_id = T::TokenIdFromLocation::convert_back(&token_location);
283

            
284
176
            if let Some(token_id) = token_id {
285
23
                let era_rewards = RewardPointsForEra::<T>::get(era_index);
286
23
                if let Some(utils) = era_rewards
287
23
                    .generate_era_rewards_utils::<<T as Config>::Hashing>(era_index, None)
288
                {
289
23
                    let tokens_inflated = T::EraInflationProvider::get();
290

            
291
23
                    if tokens_inflated.is_zero() {
292
1
                        log::error!(target: "ext_validators_rewards", "Not sending message because tokens_inflated is 0");
293
1
                        return;
294
22
                    }
295

            
296
22
                    if utils.total_points.is_zero() {
297
7
                        log::error!(target: "ext_validators_rewards", "Not sending message because total_points is 0");
298
7
                        return;
299
15
                    }
300

            
301
15
                    let ethereum_sovereign_account = T::RewardsEthereumSovereignAccount::get();
302
                    if let Err(err) =
303
15
                        T::Currency::mint_into(&ethereum_sovereign_account, tokens_inflated.into())
304
                    {
305
                        log::error!(target: "ext_validators_rewards", "Failed to mint inflation into Ethereum Soverein Account: {err:?}");
306
                        log::error!(target: "ext_validators_rewards", "Not sending message since there are no rewards to distribute");
307
                        return;
308
15
                    }
309

            
310
15
                    let command = Command::ReportRewards {
311
15
                        external_idx: T::ExternalIndexProvider::get_external_index(),
312
15
                        era_index,
313
15
                        total_points: utils.total_points,
314
15
                        tokens_inflated,
315
15
                        rewards_merkle_root: utils.rewards_merkle_root,
316
15
                        token_id,
317
15
                    };
318

            
319
15
                    let channel_id: ChannelId = snowbridge_core::PRIMARY_GOVERNANCE_CHANNEL;
320

            
321
15
                    let outbound_message = TanssiMessage {
322
15
                        id: None,
323
15
                        channel_id,
324
15
                        command: command.clone(),
325
15
                    };
326

            
327
                    // Validate and deliver the message
328
15
                    match T::ValidateMessage::validate(&outbound_message) {
329
15
                        Ok((ticket, _fee)) => {
330
15
                            let message_id = ticket.message_id();
331
15
                            if let Err(err) = T::OutboundQueue::deliver(ticket) {
332
                                log::error!(target: "ext_validators_rewards", "OutboundQueue delivery of message failed. {err:?}");
333
15
                            } else {
334
15
                                Self::deposit_event(Event::RewardsMessageSent {
335
15
                                    message_id,
336
15
                                    rewards_command: command,
337
15
                                });
338
15
                            }
339
                        }
340
                        Err(err) => {
341
                            log::error!(target: "ext_validators_rewards", "OutboundQueue validation of message failed. {err:?}");
342
                        }
343
                    }
344

            
345
15
                    frame_system::Pallet::<T>::register_extra_weight_unchecked(
346
15
                        T::WeightInfo::on_era_end(),
347
15
                        DispatchClass::Mandatory,
348
                    );
349
                } else {
350
                    // Unreachable, this should never happen as we are sending
351
                    // None as the second param in Self::generate_era_rewards_utils.
352
                    log::error!(
353
                        target: "ext_validators_rewards",
354
                        "Outbound message not sent for era {:?}!",
355
                        era_index
356
                    );
357
                }
358
            } else {
359
153
                log::error!(target: "ext_validators_rewards", "no token id found for location {:?}", token_location);
360
            }
361
176
        }
362
    }
363
}
364

            
365
/// Rewards validators for participating in parachains with era points in pallet-staking.
366
pub struct RewardValidatorsWithEraPoints<C>(core::marker::PhantomData<C>);
367

            
368
impl<C> RewardValidatorsWithEraPoints<C>
369
where
370
    C: pallet::Config + session_info::Config,
371
    C::ValidatorSet: ValidatorSet<C::AccountId, ValidatorId = C::AccountId>,
372
{
373
    /// Reward validators in session with points, but only if they are in the active set.
374
18
    fn reward_only_active(
375
18
        session_index: SessionIndex,
376
18
        indices: impl IntoIterator<Item = ValidatorIndex>,
377
18
        points: u32,
378
18
    ) {
379
18
        let validators = session_info::AccountKeys::<C>::get(session_index);
380
18
        let validators = match validators
381
18
            .defensive_proof("account_keys are present for dispute_period sessions")
382
        {
383
18
            Some(validators) => validators,
384
            None => return,
385
        };
386
        // limit rewards to the active validator set
387
18
        let mut active_set: BTreeSet<_> = C::ValidatorSet::validators().into_iter().collect();
388

            
389
        // Remove whitelisted validators, we don't want to reward them
390
18
        let whitelisted_validators = C::GetWhitelistedValidators::get();
391
42
        for validator in whitelisted_validators {
392
24
            active_set.remove(&validator);
393
24
        }
394

            
395
18
        let rewards = indices
396
18
            .into_iter()
397
18
            .filter_map(|i| validators.get(i.0 as usize).cloned())
398
18
            .filter(|v| active_set.contains(v))
399
18
            .map(|v| (v, points));
400

            
401
18
        pallet::Pallet::<C>::reward_by_ids(rewards);
402
18
    }
403
}
404

            
405
impl<C> runtime_parachains::inclusion::RewardValidators for RewardValidatorsWithEraPoints<C>
406
where
407
    C: pallet::Config + runtime_parachains::shared::Config + session_info::Config,
408
    C::ValidatorSet: ValidatorSet<C::AccountId, ValidatorId = C::AccountId>,
409
{
410
18
    fn reward_backing(indices: impl IntoIterator<Item = ValidatorIndex>) {
411
18
        let session_index = runtime_parachains::shared::CurrentSessionIndex::<C>::get();
412
18
        Self::reward_only_active(session_index, indices, C::BackingPoints::get());
413
18
    }
414

            
415
    fn reward_bitfields(_validators: impl IntoIterator<Item = ValidatorIndex>) {}
416
}
417

            
418
impl<C> runtime_parachains::disputes::RewardValidators for RewardValidatorsWithEraPoints<C>
419
where
420
    C: pallet::Config + session_info::Config,
421
    C::ValidatorSet: ValidatorSet<C::AccountId, ValidatorId = C::AccountId>,
422
{
423
    fn reward_dispute_statement(
424
        session: SessionIndex,
425
        validators: impl IntoIterator<Item = ValidatorIndex>,
426
    ) {
427
        Self::reward_only_active(session, validators, C::DisputeStatementPoints::get());
428
    }
429
}