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_outbound_queue_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,
49
    sp_std::vec,
50
    sp_std::vec::Vec,
51
    tp_bridge::{Command, DeliverMessage, Message, TicketInfo, ValidateMessage},
52
    tp_traits::ExternalIndexProvider,
53
    xcm::prelude::*,
54
};
55

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
159
    impl<AccountId> Default for EraRewardPoints<AccountId> {
160
26
        fn default() -> Self {
161
26
            EraRewardPoints {
162
26
                total: Default::default(),
163
26
                individual: BTreeMap::new(),
164
26
            }
165
26
        }
166
    }
167

            
168
    /// Store reward points per era.
169
    /// Note: EraRewardPoints is actually bounded by the amount of validators.
170
294
    #[pallet::storage]
171
    #[pallet::unbounded]
172
    pub type RewardPointsForEra<T: Config> =
173
        StorageMap<_, Twox64Concat, EraIndex, EraRewardPoints<T::AccountId>, ValueQuery>;
174

            
175
    impl<T: Config> Pallet<T> {
176
        /// Reward validators. Does not check if the validators are valid, caller needs to make sure of that.
177
236
        pub fn reward_by_ids(points: impl IntoIterator<Item = (T::AccountId, RewardPoints)>) {
178
236
            let active_era = T::EraIndexProvider::active_era();
179
236

            
180
236
            RewardPointsForEra::<T>::mutate(active_era.index, |era_rewards| {
181
250
                for (validator, points) in points.into_iter() {
182
244
                    *era_rewards.individual.entry(validator).or_default() += points;
183
244
                    era_rewards.total += points;
184
244
                }
185
236
            })
186
236
        }
187

            
188
        // Helper function used to generate the following utils:
189
        //  - rewards_merkle_root: merkle root corresponding [(validatorId, rewardPoints)]
190
        //      for the era_index specified.
191
        //  - leaves: that were used to generate the previous merkle root.
192
        //  - leaf_index: index of the validatorId's leaf in the previous leaves array (if any).
193
        //  - total_points: number of total points of the era_index specified.
194
29
        pub fn generate_era_rewards_utils(
195
29
            era_index: EraIndex,
196
29
            maybe_account_id_check: Option<T::AccountId>,
197
29
        ) -> Option<EraRewardsUtils> {
198
29
            let era_rewards = RewardPointsForEra::<T>::get(&era_index);
199
29
            let total_points: u128 = era_rewards.total as u128;
200
29
            let mut leaves = Vec::with_capacity(era_rewards.individual.len());
201
29
            let mut leaf_index = None;
202

            
203
29
            if let Some(account) = &maybe_account_id_check {
204
12
                if !era_rewards.individual.contains_key(account) {
205
9
                    log::error!(
206
9
                        target: "ext_validators_rewards",
207
9
                        "AccountId {:?} not found for era {:?}!",
208
                        account,
209
                        era_index
210
                    );
211
9
                    return None;
212
3
                }
213
17
            }
214

            
215
34
            for (index, (account_id, reward_points)) in era_rewards.individual.iter().enumerate() {
216
34
                let encoded = (account_id, reward_points).encode();
217
34
                let hashed = <T as Config>::Hashing::hash(&encoded);
218
34
                leaves.push(hashed);
219

            
220
34
                if let Some(ref check_account_id) = maybe_account_id_check {
221
5
                    if account_id == check_account_id {
222
3
                        leaf_index = Some(index as u64);
223
3
                    }
224
29
                }
225
            }
226

            
227
20
            let rewards_merkle_root =
228
20
                merkle_root::<<T as Config>::Hashing, _>(leaves.iter().cloned());
229
20

            
230
20
            Some(EraRewardsUtils {
231
20
                rewards_merkle_root,
232
20
                leaves,
233
20
                leaf_index,
234
20
                total_points,
235
20
            })
236
29
        }
237

            
238
12
        pub fn generate_rewards_merkle_proof(
239
12
            account_id: T::AccountId,
240
12
            era_index: EraIndex,
241
12
        ) -> Option<MerkleProof> {
242
12
            let utils = Self::generate_era_rewards_utils(era_index, Some(account_id))?;
243
3
            utils.leaf_index.map(|index| {
244
3
                merkle_proof::<<T as Config>::Hashing, _>(utils.leaves.into_iter(), index)
245
3
            })
246
12
        }
247

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

            
259
    impl<T: Config> tp_traits::OnEraStart for Pallet<T> {
260
245
        fn on_era_start(era_index: EraIndex, _session_start: u32, _external_idx: u64) {
261
245
            let Some(era_index_to_delete) = era_index.checked_sub(T::HistoryDepth::get()) else {
262
243
                return;
263
            };
264

            
265
2
            RewardPointsForEra::<T>::remove(era_index_to_delete);
266
245
        }
267
    }
268

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

            
278
86
            let token_location = T::TokenLocationReanchored::get();
279
86
            let token_id = T::TokenIdFromLocation::convert_back(&token_location);
280

            
281
86
            let Some(token_id) = token_id else {
282
74
                log::error!(target: "ext_validators_rewards", "no token id found for location {:?}", token_location);
283
74
                return;
284
            };
285

            
286
12
            let Some(utils) = Self::generate_era_rewards_utils(era_index, None) else {
287
                // Unreachable, this should never happen as we are sending
288
                // None as the second param in Self::generate_era_rewards_utils.
289
                log::error!(
290
                    target: "ext_validators_rewards",
291
                    "Outbound message not sent for era {:?}!",
292
                    era_index
293
                );
294
                return;
295
            };
296

            
297
12
            let tokens_inflated = T::EraInflationProvider::get();
298
12

            
299
12
            if tokens_inflated.is_zero() {
300
1
                log::error!(target: "ext_validators_rewards", "Not sending message because tokens_inflated is 0");
301
1
                return;
302
11
            }
303
11

            
304
11
            if utils.total_points.is_zero() {
305
3
                log::error!(target: "ext_validators_rewards", "Not sending message because total_points is 0");
306
3
                return;
307
8
            }
308
8

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

            
318
8
            let command = Command::ReportRewards {
319
8
                external_idx: T::ExternalIndexProvider::get_external_index(),
320
8
                era_index,
321
8
                total_points: utils.total_points,
322
8
                tokens_inflated,
323
8
                rewards_merkle_root: utils.rewards_merkle_root,
324
8
                token_id,
325
8
            };
326
8

            
327
8
            let channel_id: ChannelId = snowbridge_core::PRIMARY_GOVERNANCE_CHANNEL;
328
8

            
329
8
            let outbound_message = Message {
330
8
                id: None,
331
8
                channel_id,
332
8
                command: command.clone(),
333
8
            };
334
8

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

            
353
8
            frame_system::Pallet::<T>::register_extra_weight_unchecked(
354
8
                T::WeightInfo::on_era_end(),
355
8
                DispatchClass::Mandatory,
356
8
            );
357
86
        }
358
    }
359
}
360

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

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

            
385
9
        // Remove whitelisted validators, we don't want to reward them
386
9
        let whitelisted_validators = C::GetWhitelistedValidators::get();
387
21
        for validator in whitelisted_validators {
388
12
            active_set.remove(&validator);
389
12
        }
390

            
391
9
        let rewards = indices
392
9
            .into_iter()
393
9
            .filter_map(|i| validators.get(i.0 as usize).cloned())
394
9
            .filter(|v| active_set.contains(v))
395
9
            .map(|v| (v, points));
396
9

            
397
9
        pallet::Pallet::<C>::reward_by_ids(rewards);
398
9
    }
399
}
400

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

            
411
    fn reward_bitfields(_validators: impl IntoIterator<Item = ValidatorIndex>) {}
412
}
413

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