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, Message, 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
        /// Overarching event type.
81
        type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
82

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
190
47
                leaves.push(hashed);
191

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

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

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

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

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

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

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

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

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

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

            
271
2
            RewardPointsForEra::<T>::remove(era_index_to_delete);
272
685
        }
273
    }
274

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

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

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

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

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

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

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

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

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

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

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