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

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

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

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

            
31
pub mod weights;
32

            
33
pub use pallet::*;
34

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

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

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

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

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

            
77
    #[pallet::config]
78
    pub trait Config: frame_system::Config {
79
        /// Overarching event type.
80
        type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
81

            
82
        /// How to fetch the current era info.
83
        type EraIndexProvider: EraIndexProvider;
84

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

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

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

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

            
100
        /// Provider to retrieve the current external index indetifying the validators
101
        type ExternalIndexProvider: ExternalIndexProvider;
102

            
103
        type GetWhitelistedValidators: Get<Vec<Self::AccountId>>;
104

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

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

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

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

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

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

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

            
129
        /// The weight information of this pallet.
130
        type WeightInfo: WeightInfo;
131

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

            
136
329
    #[pallet::pallet]
137
    #[pallet::storage_version(STORAGE_VERSION)]
138
    pub struct Pallet<T>(_);
139

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

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

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

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

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

            
189
47
                leaves.push(hashed);
190

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

            
198
33
            let rewards_merkle_root = merkle_root::<Hasher, _>(leaves.iter().cloned());
199
33

            
200
33
            Some(EraRewardsUtils {
201
33
                rewards_merkle_root,
202
33
                leaves,
203
33
                leaf_index,
204
33
                total_points,
205
33
            })
206
51
        }
207
    }
208

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

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

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

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

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

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

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

            
270
2
            RewardPointsForEra::<T>::remove(era_index_to_delete);
271
667
        }
272
    }
273

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

            
283
170
            let token_location = T::TokenLocationReanchored::get();
284
170
            let token_id = T::TokenIdFromLocation::convert_back(&token_location);
285

            
286
170
            if let Some(token_id) = token_id {
287
21
                let era_rewards = RewardPointsForEra::<T>::get(&era_index);
288
21
                if let Some(utils) = era_rewards
289
21
                    .generate_era_rewards_utils::<<T as Config>::Hashing>(era_index, None)
290
                {
291
21
                    let tokens_inflated = T::EraInflationProvider::get();
292
21

            
293
21
                    if tokens_inflated.is_zero() {
294
1
                        log::error!(target: "ext_validators_rewards", "Not sending message because tokens_inflated is 0");
295
1
                        return;
296
20
                    }
297
20

            
298
20
                    if utils.total_points.is_zero() {
299
5
                        log::error!(target: "ext_validators_rewards", "Not sending message because total_points is 0");
300
5
                        return;
301
15
                    }
302
15

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

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

            
321
15
                    let channel_id: ChannelId = snowbridge_core::PRIMARY_GOVERNANCE_CHANNEL;
322
15

            
323
15
                    let outbound_message = Message {
324
15
                        id: None,
325
15
                        channel_id,
326
15
                        command: command.clone(),
327
15
                    };
328
15

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

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

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

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

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

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

            
404
18
        pallet::Pallet::<C>::reward_by_ids(rewards);
405
18
    }
406
}
407

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

            
418
    fn reward_bitfields(_validators: impl IntoIterator<Item = ValidatorIndex>) {}
419
}
420

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