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
//! # XCM Core Buyer Pallet
18
//!
19
//! This pallet allows collators to buy parathread cores on demand.
20

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

            
24
use frame_support::{Deserialize, Serialize};
25
pub use pallet::*;
26
use sp_runtime::{SaturatedConversion, Saturating};
27

            
28
#[cfg(test)]
29
mod mock;
30

            
31
#[cfg(test)]
32
mod tests;
33

            
34
#[cfg(any(test, feature = "runtime-benchmarks"))]
35
mod benchmarks;
36
pub mod weights;
37
pub use weights::WeightInfo;
38

            
39
#[cfg(feature = "runtime-benchmarks")]
40
use tp_traits::BlockNumber;
41
use {
42
    alloc::{vec, vec::Vec},
43
    dp_core::ParaId,
44
    frame_support::{
45
        dispatch::GetDispatchInfo,
46
        pallet_prelude::*,
47
        traits::fungible::{Balanced, Inspect},
48
    },
49
    frame_system::pallet_prelude::*,
50
    parity_scale_codec::EncodeLike,
51
    sp_consensus_slots::Slot,
52
    sp_runtime::traits::{AccountIdConversion, Convert, Get},
53
    tp_traits::{
54
        AuthorNotingHook, AuthorNotingInfo, LatestAuthorInfoFetcher, ParathreadParams,
55
        SlotFrequency,
56
    },
57
    tp_xcm_core_buyer::BuyCoreCollatorProof,
58
    xcm::{
59
        latest::{Asset, Assets, InteriorLocation, Response, Xcm},
60
        prelude::*,
61
    },
62
};
63

            
64
pub trait XCMNotifier<T: Config> {
65
    fn new_notify_query(
66
        responder: impl Into<Location>,
67
        notify: impl Into<<T as Config>::RuntimeCall>,
68
        timeout: BlockNumberFor<T>,
69
        match_querier: impl Into<Location>,
70
    ) -> u64;
71
}
72

            
73
/// Dummy implementation. Should only be used for testing.
74
impl<T: Config> XCMNotifier<T> for () {
75
20
    fn new_notify_query(
76
20
        _responder: impl Into<Location>,
77
20
        _notify: impl Into<<T as Config>::RuntimeCall>,
78
20
        _timeout: BlockNumberFor<T>,
79
20
        _match_querier: impl Into<Location>,
80
20
    ) -> u64 {
81
20
        0
82
20
    }
83
}
84

            
85
#[derive(
86
    RuntimeDebug,
87
    PartialEq,
88
    Eq,
89
    Encode,
90
    Decode,
91
    Clone,
92
    TypeInfo,
93
    Serialize,
94
    Deserialize,
95
    MaxEncodedLen,
96
)]
97
pub struct InFlightCoreBuyingOrder<BN> {
98
    para_id: ParaId,
99
    query_id: QueryId,
100
    ttl: BN,
101
}
102

            
103
#[derive(
104
    Debug, Clone, PartialEq, Eq, Encode, Decode, scale_info::TypeInfo, Serialize, Deserialize,
105
)]
106
pub enum BuyingError<BlockNumber> {
107
    OrderAlreadyExists {
108
        ttl: BlockNumber,
109
        current_block_number: BlockNumber,
110
    },
111
    BlockProductionPending {
112
        ttl: BlockNumber,
113
        current_block_number: BlockNumber,
114
    },
115
    NotAParathread,
116
    NotAllowedToProduceBlockRightNow {
117
        slot_frequency: SlotFrequency,
118
        max_slot_earlier_core_buying_permitted: Slot,
119
        last_block_production_slot: Slot,
120
    },
121
}
122

            
123
impl<T: Config> AuthorNotingHook<T::AccountId> for Pallet<T> {
124
102
    fn on_container_authors_noted(info: &[AuthorNotingInfo<T::AccountId>]) -> Weight {
125
102
        if info.is_empty() {
126
            return Weight::zero();
127
102
        }
128

            
129
102
        let writes = info.len().saturated_into();
130

            
131
214
        for info in info {
132
112
            let para_id = info.para_id;
133
112
            PendingBlocks::<T>::remove(para_id);
134
112
        }
135

            
136
102
        T::DbWeight::get().writes(writes)
137
102
    }
138

            
139
    #[cfg(feature = "runtime-benchmarks")]
140
    fn prepare_worst_case_for_bench(
141
        _author: &T::AccountId,
142
        _block_number: BlockNumber,
143
        para_id: ParaId,
144
    ) {
145
        // We insert the some data in the storage being removed.
146
        // Not sure if this is necessary.
147
        PendingBlocks::<T>::insert(para_id, BlockNumberFor::<T>::from(42u32));
148
    }
149
}
150

            
151
#[frame_support::pallet]
152
pub mod pallet {
153
    use {
154
        super::*,
155
        nimbus_primitives::SlotBeacon,
156
        pallet_xcm::ensure_response,
157
        sp_runtime::{app_crypto::AppCrypto, RuntimeAppPublic},
158
    };
159

            
160
    #[pallet::pallet]
161
    pub struct Pallet<T>(PhantomData<T>);
162

            
163
    #[pallet::config]
164
    pub trait Config: frame_system::Config {
165
        type Currency: Inspect<Self::AccountId> + Balanced<Self::AccountId>;
166

            
167
        type XcmSender: SendXcm;
168
        /// Get encoded call to buy a core in the relay chain. This will be passed to the XCM
169
        /// `Transact` instruction.
170
        type GetPurchaseCoreCall: GetPurchaseCoreCall<Self::RelayChain>;
171
        /// How to convert a `ParaId` into an `AccountId32`. Used to derive the parathread tank
172
        /// account in `interior_multilocation`.
173
        type GetParathreadAccountId: Convert<ParaId, [u8; 32]>;
174
        /// The max price that the parathread is willing to pay for a core, in relay chain currency.
175
        /// If `None`, defaults to `u128::MAX`, the parathread will pay the market price with no
176
        /// upper bound.
177
        type GetParathreadMaxCorePrice: GetParathreadMaxCorePrice;
178
        /// Orchestartor chain `ParaId`. Used in `absolute_multilocation` to convert the
179
        /// `interior_multilocation` into what the relay chain needs to allow to `DepositAsset`.
180
        type SelfParaId: Get<ParaId>;
181
        type RelayChain: Default
182
            + Encode
183
            + Decode
184
            + TypeInfo
185
            + EncodeLike
186
            + Clone
187
            + PartialEq
188
            + alloc::fmt::Debug
189
            + MaxEncodedLen;
190

            
191
        /// Get the parathread params. Used to verify that the para id is a parathread.
192
        // TODO: and in the future to restrict the ability to buy a core depending on slot frequency
193
        type GetParathreadParams: GetParathreadParams;
194
        /// Validate if particular account id and public key pair belongs to a collator and the collator
195
        /// is selected to collate for particular para id.
196
        type CheckCollatorValidity: CheckCollatorValidity<Self::AccountId, Self::CollatorPublicKey>;
197
        /// A configuration for base priority of unsigned transactions.
198
        ///
199
        /// This is exposed so that it can be tuned for particular runtime, when
200
        /// multiple pallets send unsigned transactions.
201
        #[pallet::constant]
202
        type UnsignedPriority: Get<TransactionPriority>;
203

            
204
        /// TTL for pending blocks entry, which prevents anyone to submit another core buying xcm.
205
        #[pallet::constant]
206
        type PendingBlocksTtl: Get<BlockNumberFor<Self>>;
207

            
208
        /// TTL to be used in xcm's notify query
209
        #[pallet::constant]
210
        type CoreBuyingXCMQueryTtl: Get<BlockNumberFor<Self>>;
211

            
212
        /// Additional ttl for in flight orders (total would be CoreBuyingXCMQueryTtl + AdditionalTtlForInflightOrders)
213
        /// after which the in flight orders can be cleaned up by anyone.
214
        #[pallet::constant]
215
        type AdditionalTtlForInflightOrders: Get<BlockNumberFor<Self>>;
216

            
217
        /// Slot drift allowed for core buying
218
        #[pallet::constant]
219
        type BuyCoreSlotDrift: Get<Slot>;
220

            
221
        #[pallet::constant]
222
        type UniversalLocation: Get<InteriorLocation>;
223

            
224
        type RuntimeOrigin: Into<Result<pallet_xcm::Origin, <Self as Config>::RuntimeOrigin>>
225
            + From<<Self as frame_system::Config>::RuntimeOrigin>;
226

            
227
        /// The overarching call type
228
        type RuntimeCall: From<Call<Self>> + Encode + GetDispatchInfo;
229

            
230
        /// Outcome notifier implements functionality to enable reporting back the outcome
231
        type XCMNotifier: XCMNotifier<Self>;
232

            
233
        type LatestAuthorInfoFetcher: LatestAuthorInfoFetcher<Self::AccountId>;
234

            
235
        type SlotBeacon: SlotBeacon;
236

            
237
        /// A PublicKey can be converted into an `AccountId`. This is required in order to verify
238
        /// the collator signature
239
        type CollatorPublicKey: Member
240
            + Parameter
241
            + RuntimeAppPublic
242
            + AppCrypto
243
            + MaybeSerializeDeserialize
244
            + MaxEncodedLen;
245

            
246
        type WeightInfo: WeightInfo;
247
    }
248

            
249
    #[pallet::event]
250
    #[pallet::generate_deposit(pub(super) fn deposit_event)]
251
    pub enum Event<T: Config> {
252
        /// An XCM message to buy a core for this parathread has been sent to the relay chain.
253
        BuyCoreXcmSent {
254
            para_id: ParaId,
255
            transaction_status_query_id: QueryId,
256
        },
257
        /// We received response for xcm
258
        ReceivedBuyCoreXCMResult { para_id: ParaId, response: Response },
259

            
260
        /// We cleaned up expired pending blocks entries.
261
        CleanedUpExpiredPendingBlocksEntries { para_ids: Vec<ParaId> },
262

            
263
        /// We cleaned up expired in flight orders entries.
264
        CleanedUpExpiredInFlightOrderEntries { para_ids: Vec<ParaId> },
265
    }
266

            
267
    #[pallet::error]
268
    pub enum Error<T> {
269
        InvalidProof,
270
        ErrorValidatingXCM,
271
        ErrorDeliveringXCM,
272
        /// An order for this para id already exists
273
        OrderAlreadyExists,
274
        /// The para id is not a parathread
275
        NotAParathread,
276
        /// There are too many in-flight orders, buying cores will not work until some of those
277
        /// orders finish.
278
        InFlightLimitReached,
279
        /// There are no collators assigned to this parathread, so no point in buying a core
280
        NoAssignedCollators,
281
        /// This collator is not assigned to this parathread
282
        CollatorNotAssigned,
283
        /// The `XcmWeights` storage has not been set. This must have been set by root with the
284
        /// value of the relay chain xcm call weight and extrinsic weight
285
        XcmWeightStorageNotSet,
286
        /// Converting a multilocation into a relay relative multilocation failed
287
        ReanchorFailed,
288
        /// Inverting location from destination point of view failed
289
        LocationInversionFailed,
290
        /// Modifying XCM to report the result of XCM failed
291
        ReportNotifyingSetupFailed,
292
        /// Unexpected XCM response
293
        UnexpectedXCMResponse,
294
        /// Block production is pending for para id with successfully placed order
295
        BlockProductionPending,
296
        /// Block production is not allowed for current slot
297
        NotAllowedToProduceBlockRightNow,
298
        /// Collator signature nonce is incorrect
299
        IncorrectCollatorSignatureNonce,
300
        /// Collator signature is invalid
301
        InvalidCollatorSignature,
302
    }
303

            
304
    impl<T: Config> From<BuyingError<BlockNumberFor<T>>> for Error<T> {
305
10
        fn from(value: BuyingError<BlockNumberFor<T>>) -> Self {
306
10
            match value {
307
5
                BuyingError::OrderAlreadyExists { .. } => Error::<T>::OrderAlreadyExists,
308
2
                BuyingError::BlockProductionPending { .. } => Error::<T>::BlockProductionPending,
309
1
                BuyingError::NotAParathread => Error::<T>::NotAParathread,
310
                BuyingError::NotAllowedToProduceBlockRightNow { .. } => {
311
2
                    Error::<T>::NotAllowedToProduceBlockRightNow
312
                }
313
            }
314
10
        }
315
    }
316

            
317
    /// Set of parathreads that have already sent an XCM message to buy a core recently.
318
    /// Used to avoid 2 collators buying a core at the same time, because it is only possible to buy
319
    /// 1 core in 1 relay block for the same parathread.
320
    #[pallet::storage]
321
    pub type InFlightOrders<T: Config> =
322
        StorageMap<_, Twox128, ParaId, InFlightCoreBuyingOrder<BlockNumberFor<T>>, OptionQuery>;
323

            
324
    /// Number of pending blocks
325
    #[pallet::storage]
326
    pub type PendingBlocks<T: Config> =
327
        StorageMap<_, Twox128, ParaId, BlockNumberFor<T>, OptionQuery>;
328

            
329
    /// Mapping of QueryId to ParaId
330
    #[pallet::storage]
331
    pub type QueryIdToParaId<T: Config> = StorageMap<_, Twox128, QueryId, ParaId, OptionQuery>;
332

            
333
    /// This must be set by root with the value of the relay chain xcm call weight and extrinsic
334
    /// weight limit. This is a storage item because relay chain weights can change, so we need to
335
    /// be able to adjust them without doing a runtime upgrade.
336
    #[pallet::storage]
337
    pub type RelayXcmWeightConfig<T: Config> =
338
        StorageValue<_, RelayXcmWeightConfigInner<T>, OptionQuery>;
339

            
340
    /// Collator signature nonce for reply protection
341
    #[pallet::storage]
342
    pub type CollatorSignatureNonce<T: Config> = StorageMap<_, Twox128, ParaId, u64, ValueQuery>;
343

            
344
    #[derive(
345
        Encode,
346
        Decode,
347
        CloneNoBound,
348
        PartialEq,
349
        Eq,
350
        DebugNoBound,
351
        TypeInfo,
352
        MaxEncodedLen,
353
        DecodeWithMemTracking,
354
    )]
355
    #[scale_info(skip_type_params(T))]
356
    pub struct RelayXcmWeightConfigInner<T> {
357
        pub buy_execution_cost: u128,
358
        pub weight_at_most: Weight,
359
        pub _phantom: PhantomData<T>,
360
    }
361

            
362
    /// This must be set by root with the value of the relay chain xcm call weight and extrinsic
363
    /// weight limit. This is a storage item because relay chain weights can change, so we need to
364
    /// be able to adjust them without doing a runtime upgrade.
365
    #[pallet::storage]
366
    pub type RelayChain<T: Config> = StorageValue<_, T::RelayChain, ValueQuery>;
367

            
368
    #[pallet::call]
369
    impl<T: Config> Pallet<T> {
370
        /// Buy a core for this parathread id.
371
        /// Collators should call this to indicate that they intend to produce a block, but they
372
        /// cannot do it because this para id has no available cores.
373
        /// The purchase is automatic using XCM, and collators do not need to do anything.
374
        // Note that the collators that will be calling this function are parathread collators, not
375
        // tanssi collators. So we cannot force them to provide a complex proof, e.g. against relay
376
        // state.
377
        #[pallet::call_index(0)]
378
        #[pallet::weight(T::WeightInfo::buy_core())]
379
        pub fn buy_core(
380
            origin: OriginFor<T>,
381
            para_id: ParaId,
382
            // Below parameter are already validated during `validate_unsigned` cal
383
            proof: BuyCoreCollatorProof<T::CollatorPublicKey>,
384
14
        ) -> DispatchResult {
385
14
            ensure_none(origin)?;
386

            
387
14
            let current_nonce = CollatorSignatureNonce::<T>::get(para_id);
388
14
            CollatorSignatureNonce::<T>::set(para_id, current_nonce.saturating_add(1));
389

            
390
14
            Self::on_collator_instantaneous_core_requested(para_id, Some(proof.public_key))
391
        }
392

            
393
        /// Buy core for para id as root. Does not require any proof, useful in tests.
394
        #[pallet::call_index(1)]
395
        #[pallet::weight(T::WeightInfo::force_buy_core())]
396
30
        pub fn force_buy_core(origin: OriginFor<T>, para_id: ParaId) -> DispatchResult {
397
30
            ensure_root(origin)?;
398

            
399
29
            Self::on_collator_instantaneous_core_requested(para_id, None)
400
        }
401

            
402
        #[pallet::call_index(2)]
403
        #[pallet::weight(T::WeightInfo::set_relay_xcm_weight_config())]
404
        pub fn set_relay_xcm_weight_config(
405
            origin: OriginFor<T>,
406
            xcm_weights: Option<RelayXcmWeightConfigInner<T>>,
407
30
        ) -> DispatchResult {
408
30
            ensure_root(origin)?;
409

            
410
30
            RelayXcmWeightConfig::<T>::set(xcm_weights);
411

            
412
30
            Ok(())
413
        }
414

            
415
        #[pallet::call_index(3)]
416
        #[pallet::weight(T::WeightInfo::set_relay_chain())]
417
        pub fn set_relay_chain(
418
            origin: OriginFor<T>,
419
            relay_chain: Option<T::RelayChain>,
420
12
        ) -> DispatchResult {
421
12
            ensure_root(origin)?;
422

            
423
12
            if let Some(relay_chain) = relay_chain {
424
12
                RelayChain::<T>::put(relay_chain);
425
12
            } else {
426
                RelayChain::<T>::kill();
427
            }
428

            
429
12
            Ok(())
430
        }
431

            
432
        #[pallet::call_index(4)]
433
        #[pallet::weight(T::WeightInfo::query_response())]
434
        pub fn query_response(
435
            origin: OriginFor<T>,
436
            query_id: QueryId,
437
            response: Response,
438
106
        ) -> DispatchResult {
439
106
            let _responder = ensure_response(<T as Config>::RuntimeOrigin::from(origin))?;
440

            
441
106
            let maybe_para_id = QueryIdToParaId::<T>::get(query_id);
442

            
443
106
            let para_id = if let Some(para_id) = maybe_para_id {
444
106
                para_id
445
            } else {
446
                // Most probably entry was expired or removed in some other way. Let's return early.
447
                return Ok(());
448
            };
449

            
450
106
            QueryIdToParaId::<T>::remove(query_id);
451
106
            InFlightOrders::<T>::remove(para_id);
452

            
453
106
            match response {
454
44
                Response::DispatchResult(MaybeErrorCode::Success) => {
455
44
                    // Success. Add para id to pending block
456
44
                    let now = <frame_system::Pallet<T>>::block_number();
457
44
                    let ttl = T::PendingBlocksTtl::get();
458
44
                    PendingBlocks::<T>::insert(para_id, now.saturating_add(ttl));
459
44
                }
460
62
                Response::DispatchResult(_) => {
461
62
                    // We do not add paraid to pending block on failure
462
62
                }
463
                _ => {
464
                    // Unexpected.
465
                    return Err(Error::<T>::UnexpectedXCMResponse.into());
466
                }
467
            }
468

            
469
106
            Self::deposit_event(Event::ReceivedBuyCoreXCMResult { para_id, response });
470

            
471
106
            Ok(())
472
        }
473

            
474
        #[pallet::call_index(5)]
475
        #[pallet::weight(T::WeightInfo::clean_up_expired_in_flight_orders(expired_pending_blocks_para_id.len() as u32))]
476
        pub fn clean_up_expired_pending_blocks(
477
            origin: OriginFor<T>,
478
            expired_pending_blocks_para_id: Vec<ParaId>,
479
2
        ) -> DispatchResult {
480
2
            let _ = ensure_signed(origin)?;
481
2
            let now = frame_system::Pallet::<T>::block_number();
482
2
            let mut cleaned_up_para_ids = vec![];
483

            
484
4
            for para_id in expired_pending_blocks_para_id {
485
2
                let maybe_pending_block_ttl = PendingBlocks::<T>::get(para_id);
486
2
                if let Some(pending_block_ttl) = maybe_pending_block_ttl {
487
2
                    if pending_block_ttl < now {
488
1
                        PendingBlocks::<T>::remove(para_id);
489
1
                        cleaned_up_para_ids.push(para_id);
490
1
                    } else {
491
1
                        // Ignore if not expired
492
1
                    }
493
                }
494
            }
495

            
496
2
            Self::deposit_event(Event::CleanedUpExpiredPendingBlocksEntries {
497
2
                para_ids: cleaned_up_para_ids,
498
2
            });
499

            
500
2
            Ok(())
501
        }
502

            
503
        #[pallet::call_index(6)]
504
        #[pallet::weight(T::WeightInfo::clean_up_expired_in_flight_orders(expired_in_flight_orders.len() as u32))]
505
        pub fn clean_up_expired_in_flight_orders(
506
            origin: OriginFor<T>,
507
            expired_in_flight_orders: Vec<ParaId>,
508
2
        ) -> DispatchResult {
509
2
            let _ = ensure_signed(origin)?;
510
2
            let now = frame_system::Pallet::<T>::block_number();
511
2
            let mut cleaned_up_para_ids = vec![];
512

            
513
4
            for para_id in expired_in_flight_orders {
514
2
                let maybe_in_flight_order = InFlightOrders::<T>::get(para_id);
515
2
                if let Some(in_flight_order) = maybe_in_flight_order {
516
2
                    if in_flight_order.ttl < now {
517
1
                        InFlightOrders::<T>::remove(para_id);
518
1
                        QueryIdToParaId::<T>::remove(in_flight_order.query_id);
519
1
                        cleaned_up_para_ids.push(para_id);
520
1
                    } else {
521
1
                        // Ignore if not expired
522
1
                    }
523
                }
524
            }
525

            
526
2
            Self::deposit_event(Event::CleanedUpExpiredInFlightOrderEntries {
527
2
                para_ids: cleaned_up_para_ids,
528
2
            });
529

            
530
2
            Ok(())
531
        }
532
    }
533

            
534
    impl<T: Config> Pallet<T> {
535
        /// Returns the interior multilocation for this container chain para id. This is a relative
536
        /// multilocation that can be used in the `descend_origin` XCM opcode.
537
57
        pub fn interior_multilocation(para_id: ParaId) -> InteriorLocation {
538
57
            let container_chain_account = T::GetParathreadAccountId::convert(para_id);
539
57
            let account_junction = Junction::AccountId32 {
540
57
                id: container_chain_account,
541
57
                network: None,
542
57
            };
543

            
544
57
            [account_junction].into()
545
57
        }
546

            
547
        /// Returns a multilocation that can be used in the `deposit_asset` XCM opcode.
548
        /// The `interior_multilocation` can be obtained using `Self::interior_multilocation`.
549
57
        pub fn relay_relative_multilocation(
550
57
            interior_multilocation: InteriorLocation,
551
57
        ) -> Result<Location, Error<T>> {
552
57
            let relay_chain = Location::parent();
553
57
            let context: InteriorLocation = [Parachain(T::SelfParaId::get().into())].into();
554
57
            let mut reanchored: Location = interior_multilocation.into();
555
57
            reanchored
556
57
                .reanchor(&relay_chain, &context)
557
57
                .map_err(|_| Error::<T>::ReanchorFailed)?;
558

            
559
57
            Ok(reanchored)
560
57
        }
561

            
562
43
        pub fn is_core_buying_allowed(
563
43
            para_id: ParaId,
564
43
            _maybe_collator_public_key: Option<<T as Config>::CollatorPublicKey>,
565
43
        ) -> Result<(), BuyingError<BlockNumberFor<T>>> {
566
            // If an in flight order is pending (i.e we did not receive the notification yet) and our
567
            // record is not expired yet, we should not allow the collator to buy another core.
568
43
            let maybe_in_flight_order = InFlightOrders::<T>::get(para_id);
569
43
            if let Some(in_flight_order) = maybe_in_flight_order {
570
6
                if in_flight_order.ttl < <frame_system::Pallet<T>>::block_number() {
571
1
                    InFlightOrders::<T>::remove(para_id);
572
1
                } else {
573
5
                    return Err(BuyingError::OrderAlreadyExists {
574
5
                        ttl: in_flight_order.ttl,
575
5
                        current_block_number: <frame_system::Pallet<T>>::block_number(),
576
5
                    });
577
                }
578
37
            }
579

            
580
            // If a block production is pending and our record is not expired yet, we should not allow
581
            // the collator to buy another core yet.
582
38
            let maybe_pending_blocks_ttl = PendingBlocks::<T>::get(para_id);
583
38
            if let Some(pending_blocks_ttl) = maybe_pending_blocks_ttl {
584
3
                if pending_blocks_ttl < <frame_system::Pallet<T>>::block_number() {
585
1
                    PendingBlocks::<T>::remove(para_id);
586
1
                } else {
587
2
                    return Err(BuyingError::BlockProductionPending {
588
2
                        ttl: pending_blocks_ttl,
589
2
                        current_block_number: <frame_system::Pallet<T>>::block_number(),
590
2
                    });
591
                }
592
35
            }
593

            
594
            // Check that the para id is a parathread
595
36
            let parathread_params = T::GetParathreadParams::get_parathread_params(para_id)
596
36
                .ok_or(BuyingError::NotAParathread)?;
597

            
598
35
            let maybe_latest_author_info =
599
35
                T::LatestAuthorInfoFetcher::get_latest_author_info(para_id);
600
35
            if let Some(latest_author_info) = maybe_latest_author_info {
601
21
                let current_slot = T::SlotBeacon::slot();
602
21
                if !parathread_params.slot_frequency.should_parathread_buy_core(
603
21
                    Slot::from(u64::from(current_slot)),
604
21
                    T::BuyCoreSlotDrift::get(),
605
21
                    latest_author_info.latest_slot_number,
606
21
                ) {
607
                    // TODO: Take max slots to produce a block from config
608
2
                    return Err(BuyingError::NotAllowedToProduceBlockRightNow {
609
2
                        slot_frequency: parathread_params.slot_frequency,
610
2
                        max_slot_earlier_core_buying_permitted: Slot::from(2u64),
611
2
                        last_block_production_slot: latest_author_info.latest_slot_number,
612
2
                    });
613
19
                }
614
14
            }
615

            
616
33
            Ok(())
617
43
        }
618

            
619
        /// Send an XCM message to the relay chain to try to buy a core for this para_id.
620
43
        fn on_collator_instantaneous_core_requested(
621
43
            para_id: ParaId,
622
43
            maybe_collator_public_key: Option<<T as Config>::CollatorPublicKey>,
623
43
        ) -> DispatchResult {
624
43
            Self::is_core_buying_allowed(para_id, maybe_collator_public_key)
625
43
                .map_err(Into::<Error<T>>::into)?;
626

            
627
32
            let xcm_weights_storage =
628
33
                RelayXcmWeightConfig::<T>::get().ok_or(Error::<T>::XcmWeightStorageNotSet)?;
629

            
630
32
            let withdraw_amount = xcm_weights_storage.buy_execution_cost;
631

            
632
            // Use the account derived from the multilocation composed with DescendOrigin
633
            // Buy on-demand cores
634
            // Any failure should return everything to the derivative account
635

            
636
            // Don't use utility::as_derivative because that will make the tanssi sovereign account
637
            // pay for fees, instead use `DescendOrigin` to make the parathread tank account
638
            // pay for fees.
639
            // TODO: when coretime is implemented, use coretime instantaneous credits instead of
640
            // buying on-demand cores at the price defined by the relay
641
32
            let origin = OriginKind::SovereignAccount;
642
            // TODO: max_amount is the max price of a core that this parathread is willing to pay
643
            // It should be defined in a storage item somewhere, controllable by the container chain
644
            // manager.
645
32
            let max_amount =
646
32
                T::GetParathreadMaxCorePrice::get_max_core_price(para_id).unwrap_or(u128::MAX);
647
32
            let call =
648
32
                T::GetPurchaseCoreCall::get_encoded(RelayChain::<T>::get(), max_amount, para_id);
649
32
            let weight_at_most = xcm_weights_storage.weight_at_most;
650

            
651
            // Assumption: derived account already has DOT
652
            // The balance should be enough to cover the `Withdraw` needed to `BuyExecution`, plus
653
            // the price of the core, which can change based on demand.
654
32
            let relay_asset_total: Asset = (Here, withdraw_amount).into();
655
32
            let refund_asset_filter: AssetFilter = AssetFilter::Wild(WildAsset::AllCounted(1));
656

            
657
32
            let interior_multilocation = Self::interior_multilocation(para_id);
658
            // The parathread tank account is derived from the tanssi sovereign account and the
659
            // parathread para id.
660
32
            let derived_account =
661
32
                Self::relay_relative_multilocation(interior_multilocation.clone())?;
662

            
663
            // Need to use `builder_unsafe` because safe `builder` does not allow `descend_origin` as first instruction.
664
            // We use `descend_origin` instead of wrapping the transact call in `utility.as_derivative`
665
            // because with `descend_origin` the parathread tank account will pay for fees, while
666
            // `utility.as_derivative` will make the tanssi sovereign account pay for fees.
667

            
668
32
            let notify_call = <T as Config>::RuntimeCall::from(Call::<T>::query_response {
669
32
                query_id: 0,
670
32
                response: Default::default(),
671
32
            });
672
32
            let notify_call_weight = notify_call.get_dispatch_info().call_weight;
673

            
674
32
            let notify_query_ttl = <frame_system::Pallet<T>>::block_number()
675
32
                .saturating_add(T::CoreBuyingXCMQueryTtl::get());
676

            
677
            // Send XCM to relay chain
678
32
            let relay_chain = Location::parent();
679
32
            let query_id = T::XCMNotifier::new_notify_query(
680
32
                relay_chain.clone(),
681
32
                notify_call,
682
32
                notify_query_ttl,
683
32
                interior_multilocation.clone(),
684
            );
685

            
686
32
            let message: Xcm<()> = Xcm::builder_unsafe()
687
32
                .descend_origin(interior_multilocation.clone())
688
32
                .withdraw_asset(Assets::from(vec![relay_asset_total.clone()]))
689
32
                .buy_execution(relay_asset_total, Unlimited)
690
                // Both in case of error and in case of success, we want to refund the unused weight
691
32
                .set_appendix(
692
32
                    Xcm::builder_unsafe()
693
32
                        .report_transact_status(QueryResponseInfo {
694
                            // This location from the point of view of destination
695
32
                            destination: T::UniversalLocation::get()
696
32
                                .invert_target(&relay_chain)
697
32
                                .map_err(|e| {
698
                                    log::error!("invert_target: {:?}", e);
699

            
700
                                    Error::<T>::LocationInversionFailed
701
                                })?,
702
32
                            query_id,
703
32
                            max_weight: notify_call_weight,
704
                        })
705
32
                        .refund_surplus()
706
32
                        .deposit_asset(refund_asset_filter, derived_account)
707
32
                        .build(),
708
                )
709
32
                .transact(origin, weight_at_most, call)
710
32
                .build();
711

            
712
            // We intentionally do not charge any fees
713
32
            let (ticket, _price) =
714
32
                T::XcmSender::validate(&mut Some(relay_chain), &mut Some(message)).map_err(
715
                    |e| {
716
                        log::error!("XcmSender::validate: {:?}", e);
717
                        Error::<T>::ErrorValidatingXCM
718
                    },
719
                )?;
720
32
            T::XcmSender::deliver(ticket).map_err(|e| {
721
                log::error!("XcmSender::deliver: {:?}", e);
722
                Error::<T>::ErrorDeliveringXCM
723
            })?;
724
32
            Self::deposit_event(Event::BuyCoreXcmSent {
725
32
                para_id,
726
32
                transaction_status_query_id: query_id,
727
32
            });
728

            
729
32
            let in_flight_order_ttl =
730
32
                notify_query_ttl.saturating_add(T::AdditionalTtlForInflightOrders::get());
731
32
            InFlightOrders::<T>::insert(
732
32
                para_id,
733
32
                InFlightCoreBuyingOrder {
734
32
                    para_id,
735
32
                    query_id,
736
32
                    ttl: in_flight_order_ttl,
737
32
                },
738
            );
739

            
740
32
            QueryIdToParaId::<T>::insert(query_id, para_id);
741

            
742
32
            Ok(())
743
43
        }
744

            
745
63
        pub fn para_deregistered(para_id: ParaId) {
746
            // If para is deregistered we need to clean up in flight order, query id mapping
747
63
            if let Some(in_flight_order) = InFlightOrders::<T>::take(para_id) {
748
1
                InFlightOrders::<T>::remove(para_id);
749
1
                QueryIdToParaId::<T>::remove(in_flight_order.query_id);
750
62
            }
751

            
752
            // We need to clean the pending block entry if any
753
63
            PendingBlocks::<T>::remove(para_id);
754
63
        }
755
    }
756

            
757
    #[pallet::validate_unsigned]
758
    impl<T: Config> ValidateUnsigned for Pallet<T> {
759
        type Call = Call<T>;
760

            
761
20
        fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity {
762
20
            if let Call::buy_core { para_id, proof } = call {
763
20
                let block_number = <frame_system::Pallet<T>>::block_number();
764

            
765
20
                let current_nonce = CollatorSignatureNonce::<T>::get(para_id);
766
20
                if proof.nonce != current_nonce {
767
2
                    return InvalidTransaction::Call.into();
768
18
                }
769

            
770
18
                let is_valid_collator =
771
18
                    T::CheckCollatorValidity::is_valid_collator(*para_id, proof.public_key.clone());
772
18
                if !is_valid_collator {
773
3
                    return InvalidTransaction::Call.into();
774
15
                }
775

            
776
15
                if !proof.verify_signature(*para_id) {
777
2
                    return InvalidTransaction::Call.into();
778
13
                }
779

            
780
13
                ValidTransaction::with_tag_prefix("XcmCoreBuyer")
781
13
                    .priority(T::UnsignedPriority::get())
782
                    // TODO: tags
783
13
                    .and_provides((block_number, para_id))
784
                    //.and_provides((current_session, authority_id))
785
                    //.longevity(
786
                    //    TryInto::<u64>::try_into(
787
                    //       T::NextSessionRotation::average_session_length() / 2u32.into(),
788
                    //    )
789
                    //        .unwrap_or(64_u64),
790
                    //)
791
13
                    .longevity(64)
792
13
                    .propagate(true)
793
13
                    .build()
794
            } else {
795
                InvalidTransaction::Call.into()
796
            }
797
20
        }
798
    }
799
}
800

            
801
pub trait GetPurchaseCoreCall<RelayChain> {
802
    /// Get the encoded call to buy a core for this `para_id`, with this `max_amount`.
803
    /// Returns the encoded call and its estimated weight.
804
    fn get_encoded(relay_chain: RelayChain, max_amount: u128, para_id: ParaId) -> Vec<u8>;
805
}
806

            
807
pub trait CheckCollatorValidity<AccountId, PublicKey> {
808
    fn is_valid_collator(para_id: ParaId, public_key: PublicKey) -> bool;
809

            
810
    #[cfg(feature = "runtime-benchmarks")]
811
    fn set_valid_collator(para_id: ParaId, account_id: AccountId, public_key: PublicKey);
812
}
813

            
814
pub trait GetParathreadMaxCorePrice {
815
    fn get_max_core_price(para_id: ParaId) -> Option<u128>;
816
}
817

            
818
impl GetParathreadMaxCorePrice for () {
819
20
    fn get_max_core_price(_para_id: ParaId) -> Option<u128> {
820
20
        None
821
20
    }
822
}
823

            
824
pub trait GetParathreadParams {
825
    fn get_parathread_params(para_id: ParaId) -> Option<ParathreadParams>;
826

            
827
    #[cfg(feature = "runtime-benchmarks")]
828
    fn set_parathread_params(para_id: ParaId, parathread_params: Option<ParathreadParams>);
829
}
830

            
831
/// Use `into_account_truncating` to convert a `ParaId` into a `[u8; 32]`.
832
pub struct ParaIdIntoAccountTruncating;
833

            
834
impl Convert<ParaId, [u8; 32]> for ParaIdIntoAccountTruncating {
835
525
    fn convert(para_id: ParaId) -> [u8; 32] {
836
        // Derive a 32 byte account id for a parathread. Note that this is not the address of
837
        // the relay chain parathread tank, but that address is derived from this.
838
525
        let account: dp_core::AccountId = para_id.into_account_truncating();
839

            
840
525
        account.into()
841
525
    }
842
}