diff --git a/cumulus/parachains/runtimes/coretime/coretime-rococo/src/coretime.rs b/cumulus/parachains/runtimes/coretime/coretime-rococo/src/coretime.rs index a837b8d25dcf7a1f5b03e598447f8e34c0e0e49c..54cfc64930b80fb7b5a26e156049b278bd57883e 100644 --- a/cumulus/parachains/runtimes/coretime/coretime-rococo/src/coretime.rs +++ b/cumulus/parachains/runtimes/coretime/coretime-rococo/src/coretime.rs @@ -111,6 +111,7 @@ enum CoretimeProviderCalls { parameter_types! { pub const BrokerPalletId: PalletId = PalletId(*b"py/broke"); + pub const MinimumCreditPurchase: Balance = UNITS / 10; pub RevenueAccumulationAccount: AccountId = BrokerPalletId::get().into_sub_account_truncating(b"burnstash"); } @@ -319,4 +320,5 @@ impl pallet_broker::Config for Runtime { type SovereignAccountOf = SovereignAccountOf; type MaxAutoRenewals = ConstU32<100>; type PriceAdapter = pallet_broker::CenterTargetPrice<Balance>; + type MinimumCreditPurchase = MinimumCreditPurchase; } diff --git a/cumulus/parachains/runtimes/coretime/coretime-westend/src/coretime.rs b/cumulus/parachains/runtimes/coretime/coretime-westend/src/coretime.rs index 805861b1f8bdb90acd967e93bdaf198c892ee64c..9aa9e699b65da0689c2f0e14236371e254dcfcb8 100644 --- a/cumulus/parachains/runtimes/coretime/coretime-westend/src/coretime.rs +++ b/cumulus/parachains/runtimes/coretime/coretime-westend/src/coretime.rs @@ -111,6 +111,7 @@ enum CoretimeProviderCalls { parameter_types! { pub const BrokerPalletId: PalletId = PalletId(*b"py/broke"); + pub const MinimumCreditPurchase: Balance = UNITS / 10; pub RevenueAccumulationAccount: AccountId = BrokerPalletId::get().into_sub_account_truncating(b"burnstash"); } @@ -332,4 +333,5 @@ impl pallet_broker::Config for Runtime { type SovereignAccountOf = SovereignAccountOf; type MaxAutoRenewals = ConstU32<20>; type PriceAdapter = pallet_broker::CenterTargetPrice<Balance>; + type MinimumCreditPurchase = MinimumCreditPurchase; } diff --git a/polkadot/runtime/parachains/src/assigner_coretime/tests.rs b/polkadot/runtime/parachains/src/assigner_coretime/tests.rs index ab011bfc4ae126050f39e9b0d3fffaab04e3c8e4..ed56170cbcdec14657780de06a7369be6450543f 100644 --- a/polkadot/runtime/parachains/src/assigner_coretime/tests.rs +++ b/polkadot/runtime/parachains/src/assigner_coretime/tests.rs @@ -20,13 +20,13 @@ use crate::{ assigner_coretime::{mock_helpers::GenesisConfigBuilder, pallet::Error, Schedule}, initializer::SessionChangeNotification, mock::{ - new_test_ext, Balances, CoretimeAssigner, OnDemand, Paras, ParasShared, RuntimeOrigin, - Scheduler, System, Test, + new_test_ext, CoretimeAssigner, OnDemand, Paras, ParasShared, RuntimeOrigin, Scheduler, + System, Test, }, paras::{ParaGenesisArgs, ParaKind}, scheduler::common::Assignment, }; -use frame_support::{assert_noop, assert_ok, pallet_prelude::*, traits::Currency}; +use frame_support::{assert_noop, assert_ok, pallet_prelude::*}; use pallet_broker::TaskId; use polkadot_primitives::{BlockNumber, Id as ParaId, SessionIndex, ValidationCode}; @@ -494,9 +494,9 @@ fn pop_assignment_for_core_works() { // Initialize the parathread, wait for it to be ready, then add an // on demand order to later pop with our Coretime assigner. schedule_blank_para(para_id, ParaKind::Parathread); - Balances::make_free_balance_be(&alice, amt); + on_demand::Credits::<Test>::insert(&alice, amt); run_to_block(1, |n| if n == 1 { Some(Default::default()) } else { None }); - assert_ok!(OnDemand::place_order_allow_death(RuntimeOrigin::signed(alice), amt, para_id)); + assert_ok!(OnDemand::place_order_with_credits(RuntimeOrigin::signed(alice), amt, para_id)); // Case 1: Assignment idle assert_ok!(CoretimeAssigner::assign_core( diff --git a/polkadot/runtime/parachains/src/coretime/benchmarking.rs b/polkadot/runtime/parachains/src/coretime/benchmarking.rs index 49e3d8a88c0158f09aa3a4d8e98a969ca907be44..aaa4a4f9ee9a921acd33d95be1596ab333039cb2 100644 --- a/polkadot/runtime/parachains/src/coretime/benchmarking.rs +++ b/polkadot/runtime/parachains/src/coretime/benchmarking.rs @@ -96,4 +96,14 @@ mod benchmarks { Some(BlockNumberFor::<T>::from(20u32)), ) } + + #[benchmark] + fn credit_account() { + // Setup + let root_origin = <T as frame_system::Config>::RuntimeOrigin::root(); + let who: T::AccountId = whitelisted_caller(); + + #[extrinsic_call] + _(root_origin as <T as frame_system::Config>::RuntimeOrigin, who, 1_000_000u32.into()) + } } diff --git a/polkadot/runtime/parachains/src/coretime/mod.rs b/polkadot/runtime/parachains/src/coretime/mod.rs index 5656e92b90be064d45f52481460e5fce36cc591d..e961d5fa76a8b61e653e4840870d5cf99a4541c5 100644 --- a/polkadot/runtime/parachains/src/coretime/mod.rs +++ b/polkadot/runtime/parachains/src/coretime/mod.rs @@ -48,7 +48,7 @@ const LOG_TARGET: &str = "runtime::parachains::coretime"; pub trait WeightInfo { fn request_core_count() -> Weight; fn request_revenue_at() -> Weight; - //fn credit_account() -> Weight; + fn credit_account() -> Weight; fn assign_core(s: u32) -> Weight; } @@ -62,19 +62,18 @@ impl WeightInfo for TestWeightInfo { fn request_revenue_at() -> Weight { Weight::MAX } - // TODO: Add real benchmarking functionality for each of these to - // benchmarking.rs, then uncomment here and in trait definition. - //fn credit_account() -> Weight { - // Weight::MAX - //} + fn credit_account() -> Weight { + Weight::MAX + } fn assign_core(_s: u32) -> Weight { Weight::MAX } } /// Shorthand for the Balance type the runtime is using. -pub type BalanceOf<T> = - <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance; +pub type BalanceOf<T> = <<T as on_demand::Config>::Currency as Currency< + <T as frame_system::Config>::AccountId, +>>::Balance; /// Broker pallet index on the coretime chain. Used to /// @@ -120,8 +119,6 @@ pub mod pallet { type RuntimeOrigin: From<<Self as frame_system::Config>::RuntimeOrigin> + Into<result::Result<Origin, <Self as Config>::RuntimeOrigin>>; type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; - /// The runtime's definition of a Currency. - type Currency: Currency<Self::AccountId>; /// The ParaId of the coretime chain. #[pallet::constant] type BrokerId: Get<u32>; @@ -195,18 +192,19 @@ pub mod pallet { Self::notify_revenue(when) } - //// TODO Impl me! - ////#[pallet::weight(<T as Config>::WeightInfo::credit_account())] - //#[pallet::call_index(3)] - //pub fn credit_account( - // origin: OriginFor<T>, - // _who: T::AccountId, - // _amount: BalanceOf<T>, - //) -> DispatchResult { - // // Ignore requests not coming from the coretime chain or root. - // Self::ensure_root_or_para(origin, <T as Config>::BrokerId::get().into())?; - // Ok(()) - //} + #[pallet::weight(<T as Config>::WeightInfo::credit_account())] + #[pallet::call_index(3)] + pub fn credit_account( + origin: OriginFor<T>, + who: T::AccountId, + amount: BalanceOf<T>, + ) -> DispatchResult { + // Ignore requests not coming from the coretime chain or root. + Self::ensure_root_or_para(origin, <T as Config>::BrokerId::get().into())?; + + on_demand::Pallet::<T>::credit_account(who, amount.saturated_into()); + Ok(()) + } /// Receive instructions from the `ExternalBrokerOrigin`, detailing how a specific core is /// to be used. diff --git a/polkadot/runtime/parachains/src/mock.rs b/polkadot/runtime/parachains/src/mock.rs index ee1990a7b618ab9ad8a3a6261a55b2e1163d3080..fdf4898eaaaa298089ea3250495149f8840b6b38 100644 --- a/polkadot/runtime/parachains/src/mock.rs +++ b/polkadot/runtime/parachains/src/mock.rs @@ -434,7 +434,6 @@ impl Get<InteriorLocation> for BrokerPot { impl coretime::Config for Test { type RuntimeOrigin = RuntimeOrigin; type RuntimeEvent = RuntimeEvent; - type Currency = pallet_balances::Pallet<Test>; type BrokerId = BrokerId; type WeightInfo = crate::coretime::TestWeightInfo; type SendXcm = DummyXcmSender; diff --git a/polkadot/runtime/parachains/src/on_demand/benchmarking.rs b/polkadot/runtime/parachains/src/on_demand/benchmarking.rs index d494a77a5c4dbeee27e510e4df35f1f78741b885..4a996848bb029f11ff593aa88487c952637e022a 100644 --- a/polkadot/runtime/parachains/src/on_demand/benchmarking.rs +++ b/polkadot/runtime/parachains/src/on_demand/benchmarking.rs @@ -91,6 +91,20 @@ mod benchmarks { _(RawOrigin::Signed(caller.into()), BalanceOf::<T>::max_value(), para_id) } + #[benchmark] + fn place_order_with_credits(s: Linear<1, MAX_FILL_BENCH>) { + // Setup + let caller: T::AccountId = whitelisted_caller(); + let para_id = ParaId::from(111u32); + init_parathread::<T>(para_id); + Credits::<T>::insert(&caller, BalanceOf::<T>::max_value()); + + Pallet::<T>::populate_queue(para_id, s); + + #[extrinsic_call] + _(RawOrigin::Signed(caller.into()), BalanceOf::<T>::max_value(), para_id) + } + impl_benchmark_test_suite!( Pallet, crate::mock::new_test_ext( diff --git a/polkadot/runtime/parachains/src/on_demand/mod.rs b/polkadot/runtime/parachains/src/on_demand/mod.rs index 66400eb00fd9d7a64ed2ffd660ac0e0f7c3e9027..c8ff4b1ae4a5d96b3b6b041ddbced04ae1b779cc 100644 --- a/polkadot/runtime/parachains/src/on_demand/mod.rs +++ b/polkadot/runtime/parachains/src/on_demand/mod.rs @@ -73,6 +73,7 @@ pub use pallet::*; pub trait WeightInfo { fn place_order_allow_death(s: u32) -> Weight; fn place_order_keep_alive(s: u32) -> Weight; + fn place_order_with_credits(s: u32) -> Weight; } /// A weight info that is only suitable for testing. @@ -86,6 +87,19 @@ impl WeightInfo for TestWeightInfo { fn place_order_keep_alive(_: u32) -> Weight { Weight::MAX } + + fn place_order_with_credits(_: u32) -> Weight { + Weight::MAX + } +} + +/// Defines how the account wants to pay for on-demand. +#[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Clone, Eq)] +enum PaymentType { + /// Use credits to purchase on-demand coretime. + Credits, + /// Use account's free balance to purchase on-demand coretime. + Balance, } #[frame_support::pallet] @@ -169,6 +183,11 @@ pub mod pallet { pub type Revenue<T: Config> = StorageValue<_, BoundedVec<BalanceOf<T>, T::MaxHistoricalRevenue>, ValueQuery>; + /// Keeps track of credits owned by each account. + #[pallet::storage] + pub type Credits<T: Config> = + StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf<T>, ValueQuery>; + #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event<T: Config> { @@ -176,6 +195,8 @@ pub mod pallet { OnDemandOrderPlaced { para_id: ParaId, spot_price: BalanceOf<T>, ordered_by: T::AccountId }, /// The value of the spot price has likely changed SpotPriceSet { spot_price: BalanceOf<T> }, + /// An account was given credits. + AccountCredited { who: T::AccountId, amount: BalanceOf<T> }, } #[pallet::error] @@ -185,6 +206,8 @@ pub mod pallet { /// The current spot price is higher than the max amount specified in the `place_order` /// call, making it invalid. SpotPriceHigherThanMaxAmount, + /// The account doesn't have enough credits to purchase on-demand coretime. + InsufficientCredits, } #[pallet::hooks] @@ -235,13 +258,21 @@ pub mod pallet { /// - `OnDemandOrderPlaced` #[pallet::call_index(0)] #[pallet::weight(<T as Config>::WeightInfo::place_order_allow_death(QueueStatus::<T>::get().size()))] + #[allow(deprecated)] + #[deprecated(note = "This will be removed in favor of using `place_order_with_credits`")] pub fn place_order_allow_death( origin: OriginFor<T>, max_amount: BalanceOf<T>, para_id: ParaId, ) -> DispatchResult { let sender = ensure_signed(origin)?; - Pallet::<T>::do_place_order(sender, max_amount, para_id, AllowDeath) + Pallet::<T>::do_place_order( + sender, + max_amount, + para_id, + AllowDeath, + PaymentType::Balance, + ) } /// Same as the [`place_order_allow_death`](Self::place_order_allow_death) call , but with a @@ -261,13 +292,55 @@ pub mod pallet { /// - `OnDemandOrderPlaced` #[pallet::call_index(1)] #[pallet::weight(<T as Config>::WeightInfo::place_order_keep_alive(QueueStatus::<T>::get().size()))] + #[allow(deprecated)] + #[deprecated(note = "This will be removed in favor of using `place_order_with_credits`")] pub fn place_order_keep_alive( origin: OriginFor<T>, max_amount: BalanceOf<T>, para_id: ParaId, ) -> DispatchResult { let sender = ensure_signed(origin)?; - Pallet::<T>::do_place_order(sender, max_amount, para_id, KeepAlive) + Pallet::<T>::do_place_order( + sender, + max_amount, + para_id, + KeepAlive, + PaymentType::Balance, + ) + } + + /// Create a single on demand core order with credits. + /// Will charge the owner's on-demand credit account the spot price for the current block. + /// + /// Parameters: + /// - `origin`: The sender of the call, on-demand credits will be withdrawn from this + /// account. + /// - `max_amount`: The maximum number of credits to spend from the origin to place an + /// order. + /// - `para_id`: A `ParaId` the origin wants to provide blockspace for. + /// + /// Errors: + /// - `InsufficientCredits` + /// - `QueueFull` + /// - `SpotPriceHigherThanMaxAmount` + /// + /// Events: + /// - `OnDemandOrderPlaced` + #[pallet::call_index(2)] + #[pallet::weight(<T as Config>::WeightInfo::place_order_with_credits(QueueStatus::<T>::get().size()))] + pub fn place_order_with_credits( + origin: OriginFor<T>, + max_amount: BalanceOf<T>, + para_id: ParaId, + ) -> DispatchResult { + let sender = ensure_signed(origin)?; + Pallet::<T>::do_place_order( + sender, + max_amount, + para_id, + KeepAlive, + PaymentType::Credits, + ) } } } @@ -349,6 +422,18 @@ where }); } + /// Adds credits to the specified account. + /// + /// Parameters: + /// - `who`: Credit receiver. + /// - `amount`: The amount of new credits the account will receive. + pub fn credit_account(who: T::AccountId, amount: BalanceOf<T>) { + Credits::<T>::mutate(who.clone(), |credits| { + *credits = credits.saturating_add(amount); + }); + Pallet::<T>::deposit_event(Event::<T>::AccountCredited { who, amount }); + } + /// Helper function for `place_order_*` calls. Used to differentiate between placing orders /// with a keep alive check or to allow the account to be reaped. The amount charged is /// stored to the pallet account to be later paid out as revenue. @@ -358,6 +443,7 @@ where /// - `max_amount`: The maximum balance to withdraw from the origin to place an order. /// - `para_id`: A `ParaId` the origin wants to provide blockspace for. /// - `existence_requirement`: Whether or not to ensure that the account will not be reaped. + /// - `payment_type`: Defines how the user wants to pay for on-demand. /// /// Errors: /// - `InsufficientBalance`: from the Currency implementation @@ -371,6 +457,7 @@ where max_amount: BalanceOf<T>, para_id: ParaId, existence_requirement: ExistenceRequirement, + payment_type: PaymentType, ) -> DispatchResult { let config = configuration::ActiveConfig::<T>::get(); @@ -391,22 +478,39 @@ where Error::<T>::QueueFull ); - // Charge the sending account the spot price. The amount will be teleported to the - // broker chain once it requests revenue information. - let amt = T::Currency::withdraw( - &sender, - spot_price, - WithdrawReasons::FEE, - existence_requirement, - )?; - - // Consume the negative imbalance and deposit it into the pallet account. Make sure the - // account preserves even without the existential deposit. - let pot = Self::account_id(); - if !System::<T>::account_exists(&pot) { - System::<T>::inc_providers(&pot); + match payment_type { + PaymentType::Balance => { + // Charge the sending account the spot price. The amount will be teleported to + // the broker chain once it requests revenue information. + let amt = T::Currency::withdraw( + &sender, + spot_price, + WithdrawReasons::FEE, + existence_requirement, + )?; + + // Consume the negative imbalance and deposit it into the pallet account. Make + // sure the account preserves even without the existential deposit. + let pot = Self::account_id(); + if !System::<T>::account_exists(&pot) { + System::<T>::inc_providers(&pot); + } + T::Currency::resolve_creating(&pot, amt); + }, + PaymentType::Credits => { + let credits = Credits::<T>::get(&sender); + + // Charge the sending account the spot price in credits. + let new_credits_value = + credits.checked_sub(&spot_price).ok_or(Error::<T>::InsufficientCredits)?; + + if new_credits_value.is_zero() { + Credits::<T>::remove(&sender); + } else { + Credits::<T>::insert(&sender, new_credits_value); + } + }, } - T::Currency::resolve_creating(&pot, amt); // Add the amount to the current block's (index 0) revenue information. Revenue::<T>::mutate(|bounded_revenue| { @@ -619,7 +723,7 @@ where /// Increases the affinity of a `ParaId` to a specified `CoreIndex`. /// Adds to the count of the `CoreAffinityCount` if an entry is found and the core_index - /// matches. A non-existent entry will be initialized with a count of 1 and uses the supplied + /// matches. A non-existent entry will be initialized with a count of 1 and uses the supplied /// `CoreIndex`. fn increase_affinity(para_id: ParaId, core_index: CoreIndex) { ParaIdAffinity::<T>::mutate(para_id, |maybe_affinity| match maybe_affinity { diff --git a/polkadot/runtime/parachains/src/on_demand/tests.rs b/polkadot/runtime/parachains/src/on_demand/tests.rs index 7da16942c7ad6989a2188fe749c947141f073ee0..a435598d8f5559c736973d973a7876ed41f25cbf 100644 --- a/polkadot/runtime/parachains/src/on_demand/tests.rs +++ b/polkadot/runtime/parachains/src/on_demand/tests.rs @@ -98,6 +98,7 @@ fn place_order_run_to_blocknumber(para_id: ParaId, blocknumber: Option<BlockNumb if let Some(bn) = blocknumber { run_to_block(bn, |n| if n == bn { Some(Default::default()) } else { None }); } + #[allow(deprecated)] OnDemand::place_order_allow_death(RuntimeOrigin::signed(alice), amt, para_id).unwrap() } @@ -266,6 +267,7 @@ fn spot_traffic_decreases_between_idle_blocks() { } #[test] +#[allow(deprecated)] fn place_order_works() { let alice = 1u64; let amt = 10_000_000u128; @@ -308,6 +310,7 @@ fn place_order_works() { } #[test] +#[allow(deprecated)] fn place_order_keep_alive_keeps_alive() { let alice = 1u64; let amt = 1u128; // The same as crate::mock's EXISTENTIAL_DEPOSIT @@ -315,6 +318,8 @@ fn place_order_keep_alive_keeps_alive() { let para_id = ParaId::from(111); new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| { + let config = configuration::ActiveConfig::<Test>::get(); + // Initialize the parathread and wait for it to be ready. schedule_blank_para(para_id, ParaKind::Parathread); Balances::make_free_balance_be(&alice, amt); @@ -327,6 +332,71 @@ fn place_order_keep_alive_keeps_alive() { OnDemand::place_order_keep_alive(RuntimeOrigin::signed(alice), max_amt, para_id), BalancesError::<Test, _>::InsufficientBalance ); + + Balances::make_free_balance_be(&alice, max_amt); + assert_ok!(OnDemand::place_order_keep_alive( + RuntimeOrigin::signed(alice), + max_amt, + para_id + ),); + + let queue_status = QueueStatus::<Test>::get(); + let spot_price = queue_status.traffic.saturating_mul_int( + config.scheduler_params.on_demand_base_fee.saturated_into::<BalanceOf<Test>>(), + ); + assert_eq!(Balances::free_balance(&alice), max_amt.saturating_sub(spot_price)); + assert_eq!( + FreeEntries::<Test>::get().pop(), + Some(EnqueuedOrder::new(QueueIndex(0), para_id)) + ); + }); +} + +#[test] +fn place_order_with_credits() { + let alice = 1u64; + let initial_credit = 10_000_000u128; + let para_id = ParaId::from(111); + + new_test_ext(GenesisConfigBuilder::default().build()).execute_with(|| { + let config = configuration::ActiveConfig::<Test>::get(); + + // Initialize the parathread and wait for it to be ready. + schedule_blank_para(para_id, ParaKind::Parathread); + OnDemand::credit_account(alice, initial_credit); + assert_eq!(Credits::<Test>::get(alice), initial_credit); + + assert!(!Paras::is_parathread(para_id)); + run_to_block(100, |n| if n == 100 { Some(Default::default()) } else { None }); + assert!(Paras::is_parathread(para_id)); + + let queue_status = QueueStatus::<Test>::get(); + let spot_price = queue_status.traffic.saturating_mul_int( + config.scheduler_params.on_demand_base_fee.saturated_into::<BalanceOf<Test>>(), + ); + + // Create an order and pay for it with credits. + assert_ok!(OnDemand::place_order_with_credits( + RuntimeOrigin::signed(alice), + initial_credit, + para_id + )); + assert_eq!(Credits::<Test>::get(alice), initial_credit.saturating_sub(spot_price)); + assert_eq!( + FreeEntries::<Test>::get().pop(), + Some(EnqueuedOrder::new(QueueIndex(0), para_id)) + ); + + // Insufficient credits: + Credits::<Test>::insert(alice, 1u128); + assert_noop!( + OnDemand::place_order_with_credits( + RuntimeOrigin::signed(alice), + 1_000_000u128, + para_id + ), + Error::<Test>::InsufficientCredits + ); }); } diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs index f6729dd976257b506976264633387a0de944eb76..6195a9e356d47c304dce7441c393d174f6c2514c 100644 --- a/polkadot/runtime/rococo/src/lib.rs +++ b/polkadot/runtime/rococo/src/lib.rs @@ -1130,7 +1130,6 @@ impl Get<InteriorLocation> for BrokerPot { impl coretime::Config for Runtime { type RuntimeOrigin = RuntimeOrigin; type RuntimeEvent = RuntimeEvent; - type Currency = Balances; type BrokerId = BrokerId; type BrokerPotLocation = BrokerPot; type WeightInfo = weights::polkadot_runtime_parachains_coretime::WeightInfo<Runtime>; diff --git a/polkadot/runtime/rococo/src/weights/polkadot_runtime_parachains_coretime.rs b/polkadot/runtime/rococo/src/weights/polkadot_runtime_parachains_coretime.rs index b2329c098cead5d2780b6ac44a1d6b5963efcaf8..94dc7a4e0750862c27e0a7dd64a47280364825ac 100644 --- a/polkadot/runtime/rococo/src/weights/polkadot_runtime_parachains_coretime.rs +++ b/polkadot/runtime/rococo/src/weights/polkadot_runtime_parachains_coretime.rs @@ -86,6 +86,22 @@ impl<T: frame_system::Config> polkadot_runtime_parachains::coretime::WeightInfo .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().writes(1)) } + /// Storage: `Configuration::PendingConfigs` (r:1 w:1) + /// Proof: `Configuration::PendingConfigs` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Configuration::BypassConsistencyCheck` (r:1 w:0) + /// Proof: `Configuration::BypassConsistencyCheck` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParasShared::CurrentSessionIndex` (r:1 w:0) + /// Proof: `ParasShared::CurrentSessionIndex` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn credit_account() -> Weight { + // Proof Size summary in bytes: + // Measured: `151` + // Estimated: `1636` + // Minimum execution time: 7_519_000 picoseconds. + Weight::from_parts(7_803_000, 0) + .saturating_add(Weight::from_parts(0, 1636)) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().writes(1)) + } /// Storage: `CoretimeAssignmentProvider::CoreDescriptors` (r:1 w:1) /// Proof: `CoretimeAssignmentProvider::CoreDescriptors` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `CoretimeAssignmentProvider::CoreSchedules` (r:0 w:1) diff --git a/polkadot/runtime/rococo/src/weights/polkadot_runtime_parachains_on_demand.rs b/polkadot/runtime/rococo/src/weights/polkadot_runtime_parachains_on_demand.rs index 1dd62d129f9a0c5e06a67e5b949ab21decc0d260..f251ad5f6b86b9d304c84b687b7190e0881510ff 100644 --- a/polkadot/runtime/rococo/src/weights/polkadot_runtime_parachains_on_demand.rs +++ b/polkadot/runtime/rococo/src/weights/polkadot_runtime_parachains_on_demand.rs @@ -102,4 +102,28 @@ impl<T: frame_system::Config> polkadot_runtime_parachains::on_demand::WeightInfo .saturating_add(T::DbWeight::get().writes(3)) .saturating_add(Weight::from_parts(0, 8).saturating_mul(s.into())) } + /// Storage: `OnDemandAssignmentProvider::QueueStatus` (r:1 w:1) + /// Proof: `OnDemandAssignmentProvider::QueueStatus` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `OnDemandAssignmentProvider::Revenue` (r:1 w:1) + /// Proof: `OnDemandAssignmentProvider::Revenue` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `OnDemandAssignmentProvider::ParaIdAffinity` (r:1 w:0) + /// Proof: `OnDemandAssignmentProvider::ParaIdAffinity` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `OnDemandAssignmentProvider::FreeEntries` (r:1 w:1) + /// Proof: `OnDemandAssignmentProvider::FreeEntries` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// The range of component `s` is `[1, 9999]`. + fn place_order_with_credits(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `270 + s * (8 ±0)` + // Estimated: `3733 + s * (8 ±0)` + // Minimum execution time: 28_422_000 picoseconds. + Weight::from_parts(28_146_882, 0) + .saturating_add(Weight::from_parts(0, 3733)) + // Standard Error: 140 + .saturating_add(Weight::from_parts(21_283, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(3)) + .saturating_add(Weight::from_parts(0, 8).saturating_mul(s.into())) + } } diff --git a/polkadot/runtime/test-runtime/src/lib.rs b/polkadot/runtime/test-runtime/src/lib.rs index 1a19b637b798afbd03ac2cae5694407fdf05edb2..f592dc2b61df05db85ec98c22de6976ea0fa96d8 100644 --- a/polkadot/runtime/test-runtime/src/lib.rs +++ b/polkadot/runtime/test-runtime/src/lib.rs @@ -659,7 +659,6 @@ impl SendXcm for DummyXcmSender { impl coretime::Config for Runtime { type RuntimeOrigin = RuntimeOrigin; type RuntimeEvent = RuntimeEvent; - type Currency = pallet_balances::Pallet<Runtime>; type BrokerId = BrokerId; type WeightInfo = crate::coretime::TestWeightInfo; type SendXcm = DummyXcmSender; diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index 7fd2ac53530ac982040bacc6e53bd4ded6fb1748..11788c0193e49571c3ecee90aa5b0401fb241924 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -1356,7 +1356,6 @@ impl Get<InteriorLocation> for BrokerPot { impl coretime::Config for Runtime { type RuntimeOrigin = RuntimeOrigin; type RuntimeEvent = RuntimeEvent; - type Currency = Balances; type BrokerId = BrokerId; type BrokerPotLocation = BrokerPot; type WeightInfo = weights::polkadot_runtime_parachains_coretime::WeightInfo<Runtime>; diff --git a/polkadot/runtime/westend/src/weights/polkadot_runtime_parachains_coretime.rs b/polkadot/runtime/westend/src/weights/polkadot_runtime_parachains_coretime.rs index 9df382875f5f12ddee62751eeca5feb16dfece41..a36fefb704deb225cc7dc02b00533f8541444263 100644 --- a/polkadot/runtime/westend/src/weights/polkadot_runtime_parachains_coretime.rs +++ b/polkadot/runtime/westend/src/weights/polkadot_runtime_parachains_coretime.rs @@ -86,6 +86,22 @@ impl<T: frame_system::Config> polkadot_runtime_parachains::coretime::WeightInfo .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().writes(1)) } + /// Storage: `Configuration::PendingConfigs` (r:1 w:1) + /// Proof: `Configuration::PendingConfigs` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Configuration::BypassConsistencyCheck` (r:1 w:0) + /// Proof: `Configuration::BypassConsistencyCheck` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParasShared::CurrentSessionIndex` (r:1 w:0) + /// Proof: `ParasShared::CurrentSessionIndex` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn credit_account() -> Weight { + // Proof Size summary in bytes: + // Measured: `151` + // Estimated: `1636` + // Minimum execution time: 7_519_000 picoseconds. + Weight::from_parts(7_803_000, 0) + .saturating_add(Weight::from_parts(0, 1636)) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().writes(1)) + } /// Storage: `CoretimeAssignmentProvider::CoreDescriptors` (r:1 w:1) /// Proof: `CoretimeAssignmentProvider::CoreDescriptors` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `CoretimeAssignmentProvider::CoreSchedules` (r:0 w:1) diff --git a/polkadot/runtime/westend/src/weights/polkadot_runtime_parachains_on_demand.rs b/polkadot/runtime/westend/src/weights/polkadot_runtime_parachains_on_demand.rs index fc7efa6edfcf361f51892ef809721d3c34e915dd..2e84319d0b628facd5b3383908214017a07a2603 100644 --- a/polkadot/runtime/westend/src/weights/polkadot_runtime_parachains_on_demand.rs +++ b/polkadot/runtime/westend/src/weights/polkadot_runtime_parachains_on_demand.rs @@ -96,4 +96,28 @@ impl<T: frame_system::Config> polkadot_runtime_parachains::on_demand::WeightInfo .saturating_add(T::DbWeight::get().writes(3)) .saturating_add(Weight::from_parts(0, 8).saturating_mul(s.into())) } + /// Storage: `OnDemandAssignmentProvider::QueueStatus` (r:1 w:1) + /// Proof: `OnDemandAssignmentProvider::QueueStatus` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `OnDemandAssignmentProvider::Revenue` (r:1 w:1) + /// Proof: `OnDemandAssignmentProvider::Revenue` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `OnDemandAssignmentProvider::ParaIdAffinity` (r:1 w:0) + /// Proof: `OnDemandAssignmentProvider::ParaIdAffinity` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `OnDemandAssignmentProvider::FreeEntries` (r:1 w:1) + /// Proof: `OnDemandAssignmentProvider::FreeEntries` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// The range of component `s` is `[1, 9999]`. + fn place_order_with_credits(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `270 + s * (8 ±0)` + // Estimated: `3733 + s * (8 ±0)` + // Minimum execution time: 28_422_000 picoseconds. + Weight::from_parts(28_146_882, 0) + .saturating_add(Weight::from_parts(0, 3733)) + // Standard Error: 140 + .saturating_add(Weight::from_parts(21_283, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(3)) + .saturating_add(Weight::from_parts(0, 8).saturating_mul(s.into())) + } } diff --git a/polkadot/zombienet-sdk-tests/tests/smoke/coretime_revenue.rs b/polkadot/zombienet-sdk-tests/tests/smoke/coretime_revenue.rs index 59a71a83e01ecf91bbad9b5358caa0bc08f0cd0e..daa65c81d8003e01574672d86dd825ff645bffb7 100644 --- a/polkadot/zombienet-sdk-tests/tests/smoke/coretime_revenue.rs +++ b/polkadot/zombienet-sdk-tests/tests/smoke/coretime_revenue.rs @@ -14,6 +14,9 @@ use anyhow::anyhow; #[subxt::subxt(runtime_metadata_path = "metadata-files/coretime-rococo-local.scale")] mod coretime_rococo {} +#[subxt::subxt(runtime_metadata_path = "metadata-files/rococo-local.scale")] +mod rococo {} + use crate::helpers::rococo::{ self as rococo_api, runtime_types::{ @@ -43,6 +46,7 @@ use coretime_rococo::{ sp_arithmetic::per_things::Perbill, }, }; +use rococo::on_demand_assignment_provider::events as on_demand_events; type CoretimeRuntimeCall = coretime_api::runtime_types::coretime_rococo_runtime::RuntimeCall; type CoretimeUtilityCall = coretime_api::runtime_types::pallet_utility::pallet::Call; @@ -87,7 +91,7 @@ async fn assert_total_issuance( assert_eq!(ti, actual_ti); } -type ParaEvents<C> = Arc<RwLock<Vec<(u64, subxt::events::EventDetails<C>)>>>; +type EventOf<C> = Arc<RwLock<Vec<(u64, subxt::events::EventDetails<C>)>>>; macro_rules! trace_event { ($event:ident : $mod:ident => $($ev:ident),*) => { @@ -101,7 +105,7 @@ macro_rules! trace_event { }; } -async fn para_watcher<C: subxt::Config + Clone>(api: OnlineClient<C>, events: ParaEvents<C>) +async fn para_watcher<C: subxt::Config + Clone>(api: OnlineClient<C>, events: EventOf<C>) where <C::Header as subxt::config::Header>::Number: Display, { @@ -129,8 +133,35 @@ where } } -async fn wait_for_para_event<C: subxt::Config + Clone, E: StaticEvent, P: Fn(&E) -> bool + Copy>( - events: ParaEvents<C>, +async fn relay_watcher<C: subxt::Config + Clone>(api: OnlineClient<C>, events: EventOf<C>) +where + <C::Header as subxt::config::Header>::Number: Display, +{ + let mut blocks_sub = api.blocks().subscribe_finalized().await.unwrap(); + + log::debug!("Starting parachain watcher"); + while let Some(block) = blocks_sub.next().await { + let block = block.unwrap(); + log::debug!("Finalized parachain block {}", block.number()); + + for event in block.events().await.unwrap().iter() { + let event = event.unwrap(); + log::debug!("Got event: {} :: {}", event.pallet_name(), event.variant_name()); + { + events.write().await.push((block.number().into(), event.clone())); + } + + if event.pallet_name() == "OnDemandAssignmentProvider" { + trace_event!(event: on_demand_events => + AccountCredited, SpotPriceSet, OnDemandOrderPlaced + ); + } + } + } +} + +async fn wait_for_event<C: subxt::Config + Clone, E: StaticEvent, P: Fn(&E) -> bool + Copy>( + events: EventOf<C>, pallet: &'static str, variant: &'static str, predicate: P, @@ -230,14 +261,22 @@ async fn coretime_revenue_test() -> Result<(), anyhow::Error> { let bob = dev::bob(); - let para_events: ParaEvents<PolkadotConfig> = Arc::new(RwLock::new(Vec::new())); + let para_events: EventOf<PolkadotConfig> = Arc::new(RwLock::new(Vec::new())); let p_api = para_node.wait_client().await?; let p_events = para_events.clone(); - let _subscriber = tokio::spawn(async move { + let _subscriber1 = tokio::spawn(async move { para_watcher(p_api, p_events).await; }); + let relay_events: EventOf<PolkadotConfig> = Arc::new(RwLock::new(Vec::new())); + let r_api = relay_node.wait_client().await?; + let r_events = relay_events.clone(); + + let _subscriber2 = tokio::spawn(async move { + relay_watcher(r_api, r_events).await; + }); + let api: OnlineClient<PolkadotConfig> = para_node.wait_client().await?; let _s1 = tokio::spawn(async move { ti_watcher(api, "PARA").await; @@ -276,7 +315,7 @@ async fn coretime_revenue_test() -> Result<(), anyhow::Error> { ) .await?; - wait_for_para_event( + wait_for_event( para_events.clone(), "Balances", "Minted", @@ -328,16 +367,16 @@ async fn coretime_revenue_test() -> Result<(), anyhow::Error> { log::info!("Waiting for a full-length sale to begin"); - // Skip the first sale completeley as it may be a short one. Also, `request_code_count` requires + // Skip the first sale completeley as it may be a short one. Also, `request_core_count` requires // two session boundaries to propagate. Given that the `fast-runtime` session is 10 blocks and // the timeslice is 20 blocks, we should be just in time. let _: coretime_api::broker::events::SaleInitialized = - wait_for_para_event(para_events.clone(), "Broker", "SaleInitialized", |_| true).await; + wait_for_event(para_events.clone(), "Broker", "SaleInitialized", |_| true).await; log::info!("Skipped short sale"); let sale: coretime_api::broker::events::SaleInitialized = - wait_for_para_event(para_events.clone(), "Broker", "SaleInitialized", |_| true).await; + wait_for_event(para_events.clone(), "Broker", "SaleInitialized", |_| true).await; log::info!("{:?}", sale); // Alice buys a region @@ -349,7 +388,7 @@ async fn coretime_revenue_test() -> Result<(), anyhow::Error> { .sign_and_submit_default(&coretime_api::tx().broker().purchase(1_000_000_000), &alice) .await?; - let purchase = wait_for_para_event( + let purchase = wait_for_event( para_events.clone(), "Broker", "Purchased", @@ -381,19 +420,17 @@ async fn coretime_revenue_test() -> Result<(), anyhow::Error> { ) .await?; - let pooled = wait_for_para_event( - para_events.clone(), - "Broker", - "Pooled", - |e: &broker_events::Pooled| e.region_id.begin == region_begin, - ) - .await; + let pooled = + wait_for_event(para_events.clone(), "Broker", "Pooled", |e: &broker_events::Pooled| { + e.region_id.begin == region_begin + }) + .await; // Wait until the beginning of the timeslice where the region belongs to log::info!("Waiting for the region to begin"); - let hist = wait_for_para_event( + let hist = wait_for_event( para_events.clone(), "Broker", "HistoryInitialized", @@ -443,7 +480,7 @@ async fn coretime_revenue_test() -> Result<(), anyhow::Error> { log::info!("Waiting for Alice's revenue to be ready to claim"); - let claims_ready = wait_for_para_event( + let claims_ready = wait_for_event( para_events.clone(), "Broker", "ClaimsReady", @@ -460,6 +497,54 @@ async fn coretime_revenue_test() -> Result<(), anyhow::Error> { assert_total_issuance(relay_client.clone(), para_client.clone(), total_issuance).await; + // Try purchasing on-demand with credits: + + log::info!("Bob is going to buy on-demand credits for alice"); + + let r = para_client + .tx() + .sign_and_submit_then_watch_default( + &coretime_api::tx().broker().purchase_credit(100_000_000, alice_acc.clone()), + &bob, + ) + .await? + .wait_for_finalized_success() + .await?; + + assert!(r.find_first::<coretime_api::broker::events::CreditPurchased>()?.is_some()); + + let _account_credited = wait_for_event( + relay_events.clone(), + "OnDemandAssignmentProvider", + "AccountCredited", + |e: &on_demand_events::AccountCredited| e.who == alice_acc && e.amount == 100_000_000, + ) + .await; + + // Once the account is credit we can place an on-demand order using credits + log::info!("Alice is going to place an on-demand order using credits"); + + let r = relay_client + .tx() + .sign_and_submit_then_watch_default( + &rococo_api::tx() + .on_demand_assignment_provider() + .place_order_with_credits(100_000_000, primitives::Id(100)), + &alice, + ) + .await? + .wait_for_finalized_success() + .await?; + + let order = r + .find_first::<rococo_api::on_demand_assignment_provider::events::OnDemandOrderPlaced>()? + .unwrap(); + + assert_eq!(order.spot_price, ON_DEMAND_BASE_FEE); + + // NOTE: Purchasing on-demand with credits doesn't affect the total issuance, as the credits are + // purchased on the PC. Therefore we don't check for total issuance changes. + // Alice claims her revenue log::info!("Alice is going to claim her revenue"); @@ -472,7 +557,7 @@ async fn coretime_revenue_test() -> Result<(), anyhow::Error> { ) .await?; - let claim_paid = wait_for_para_event( + let claim_paid = wait_for_event( para_events.clone(), "Broker", "RevenueClaimPaid", @@ -490,16 +575,18 @@ async fn coretime_revenue_test() -> Result<(), anyhow::Error> { // between. let _: coretime_api::broker::events::SaleInitialized = - wait_for_para_event(para_events.clone(), "Broker", "SaleInitialized", |_| true).await; + wait_for_event(para_events.clone(), "Broker", "SaleInitialized", |_| true).await; total_issuance.0 -= ON_DEMAND_BASE_FEE / 2; total_issuance.1 -= ON_DEMAND_BASE_FEE / 2; let _: coretime_api::broker::events::SaleInitialized = - wait_for_para_event(para_events.clone(), "Broker", "SaleInitialized", |_| true).await; + wait_for_event(para_events.clone(), "Broker", "SaleInitialized", |_| true).await; assert_total_issuance(relay_client.clone(), para_client.clone(), total_issuance).await; + assert_eq!(order.spot_price, ON_DEMAND_BASE_FEE); + log::info!("Test finished successfully"); Ok(()) diff --git a/prdoc/pr_5990.prdoc b/prdoc/pr_5990.prdoc new file mode 100644 index 0000000000000000000000000000000000000000..ee13ad634dcf514b67d4fe80399e18cd033f8c10 --- /dev/null +++ b/prdoc/pr_5990.prdoc @@ -0,0 +1,30 @@ +# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0 +# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json + +title: On-demand credits + +doc: + - audience: [ Runtime User, Runtime Dev ] + description: | + The PR implements functionality on the relay chain for purchasing on-demand + Coretime using credits. This means on-demand Coretime should no longer be + purchased with the relay chain balance but rather with credits acquired + on the Coretime chain. The extrinsic to use for purchasing Coretime is + `place_order_with_credits`. It is worth noting that the PR also introduces + a minimum credit purchase requirement to prevent potential attacks. + +crates: + - name: pallet-broker + bump: major + - name: polkadot-runtime-parachains + bump: major + - name: rococo-runtime + bump: patch + - name: westend-runtime + bump: patch + - name: polkadot-test-runtime + bump: patch + - name: coretime-rococo-runtime + bump: major + - name: coretime-westend-runtime + bump: major diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 795e80e6746cb789c1c0dc524eba132b225c0cbc..6e847a7dc655557e445a17644e08018ac10aa267 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -2316,6 +2316,7 @@ impl pallet_migrations::Config for Runtime { parameter_types! { pub const BrokerPalletId: PalletId = PalletId(*b"py/broke"); + pub const MinimumCreditPurchase: Balance = 100 * MILLICENTS; } pub struct IntoAuthor; @@ -2368,6 +2369,7 @@ impl pallet_broker::Config for Runtime { type SovereignAccountOf = SovereignAccountOf; type MaxAutoRenewals = ConstU32<10>; type PriceAdapter = pallet_broker::CenterTargetPrice<Balance>; + type MinimumCreditPurchase = MinimumCreditPurchase; } parameter_types! { diff --git a/substrate/frame/broker/src/benchmarking.rs b/substrate/frame/broker/src/benchmarking.rs index 516518740f7d03db946e8aec1f33595f8b894923..49003afcdd8bc357f0e4f1fe469167b91561ad69 100644 --- a/substrate/frame/broker/src/benchmarking.rs +++ b/substrate/frame/broker/src/benchmarking.rs @@ -543,26 +543,22 @@ mod benches { let caller: T::AccountId = whitelisted_caller(); T::Currency::set_balance( &caller.clone(), - T::Currency::minimum_balance().saturating_add(30_000_000u32.into()), + T::Currency::minimum_balance().saturating_add(T::MinimumCreditPurchase::get()), ); T::Currency::set_balance(&Broker::<T>::account_id(), T::Currency::minimum_balance()); - let region = Broker::<T>::do_purchase(caller.clone(), 10_000_000u32.into()) - .expect("Offer not high enough for configuration."); - - let recipient: T::AccountId = account("recipient", 0, SEED); - - Broker::<T>::do_pool(region, None, recipient, Final) - .map_err(|_| BenchmarkError::Weightless)?; - let beneficiary: RelayAccountIdOf<T> = account("beneficiary", 0, SEED); #[extrinsic_call] - _(RawOrigin::Signed(caller.clone()), 20_000_000u32.into(), beneficiary.clone()); + _(RawOrigin::Signed(caller.clone()), T::MinimumCreditPurchase::get(), beneficiary.clone()); assert_last_event::<T>( - Event::CreditPurchased { who: caller, beneficiary, amount: 20_000_000u32.into() } - .into(), + Event::CreditPurchased { + who: caller, + beneficiary, + amount: T::MinimumCreditPurchase::get(), + } + .into(), ); Ok(()) diff --git a/substrate/frame/broker/src/dispatchable_impls.rs b/substrate/frame/broker/src/dispatchable_impls.rs index 489be12bdd1545d5deb731ca2b29ee9549f8fd1d..77bbf0878b4fa0f3a454d08d419c9fcc9f33fab5 100644 --- a/substrate/frame/broker/src/dispatchable_impls.rs +++ b/substrate/frame/broker/src/dispatchable_impls.rs @@ -426,6 +426,7 @@ impl<T: Config> Pallet<T> { amount: BalanceOf<T>, beneficiary: RelayAccountIdOf<T>, ) -> DispatchResult { + ensure!(amount >= T::MinimumCreditPurchase::get(), Error::<T>::CreditPurchaseTooSmall); T::Currency::transfer(&who, &Self::account_id(), amount, Expendable)?; let rc_amount = T::ConvertBalance::convert(amount); T::Coretime::credit_account(beneficiary.clone(), rc_amount); diff --git a/substrate/frame/broker/src/lib.rs b/substrate/frame/broker/src/lib.rs index 01368fd6404da182f1054a9b29c80410afd96e5d..f605815a421cac6668b7ce9052a3f8353db100e7 100644 --- a/substrate/frame/broker/src/lib.rs +++ b/substrate/frame/broker/src/lib.rs @@ -121,8 +121,15 @@ pub mod pallet { #[pallet::constant] type MaxReservedCores: Get<u32>; + /// Given that we are performing all auto-renewals in a single block, it has to be limited. #[pallet::constant] type MaxAutoRenewals: Get<u32>; + + /// The smallest amount of credits a user can purchase. + /// + /// Needed to prevent spam attacks. + #[pallet::constant] + type MinimumCreditPurchase: Get<BalanceOf<Self>>; } /// The current configuration of this pallet. @@ -544,6 +551,9 @@ pub mod pallet { SovereignAccountNotFound, /// Attempted to disable auto-renewal for a core that didn't have it enabled. AutoRenewalNotEnabled, + /// Needed to prevent spam attacks.The amount of credits the user attempted to purchase is + /// below `T::MinimumCreditPurchase`. + CreditPurchaseTooSmall, } #[derive(frame_support::DefaultNoBound)] diff --git a/substrate/frame/broker/src/mock.rs b/substrate/frame/broker/src/mock.rs index 42377eefdb22ef60c2225ffd8a9b889d40b7c8d4..40233a22edfc9777ea12824fae20eb17974e3546 100644 --- a/substrate/frame/broker/src/mock.rs +++ b/substrate/frame/broker/src/mock.rs @@ -177,6 +177,7 @@ impl OnUnbalanced<Credit<u64, <Test as Config>::Currency>> for IntoZero { ord_parameter_types! { pub const One: u64 = 1; + pub const MinimumCreditPurchase: u64 = 50; } type EnsureOneOrRoot = EitherOfDiverse<EnsureRoot<u64>, EnsureSignedBy<One, u64>>; @@ -203,6 +204,7 @@ impl crate::Config for Test { type SovereignAccountOf = SovereignAccountOf; type MaxAutoRenewals = ConstU32<3>; type PriceAdapter = CenterTargetPrice<BalanceOf<Self>>; + type MinimumCreditPurchase = MinimumCreditPurchase; } pub fn advance_to(b: u64) { diff --git a/substrate/frame/broker/src/tests.rs b/substrate/frame/broker/src/tests.rs index a130a2050d9a1aba164e586890a50e98c714cc02..984650aac08edeadf3c9eaead83b56d991733865 100644 --- a/substrate/frame/broker/src/tests.rs +++ b/substrate/frame/broker/src/tests.rs @@ -113,7 +113,7 @@ fn drop_history_works() { TestExt::new() .contribution_timeout(4) .endow(1, 1000) - .endow(2, 30) + .endow(2, 50) .execute_with(|| { assert_ok!(Broker::do_start_sales(100, 1)); advance_to(2); @@ -121,7 +121,7 @@ fn drop_history_works() { // Place region in pool. Active in pool timeslices 4, 5, 6 = rcblocks 8, 10, 12; we // expect to make/receive revenue reports on blocks 10, 12, 14. assert_ok!(Broker::do_pool(region, Some(1), 1, Final)); - assert_ok!(Broker::do_purchase_credit(2, 30, 2)); + assert_ok!(Broker::do_purchase_credit(2, 50, 2)); advance_to(6); // In the stable state with no pending payouts, we expect to see 3 items in // InstaPoolHistory here since there is a latency of 1 timeslice (for generating the @@ -694,6 +694,24 @@ fn purchase_works() { }); } +#[test] +fn purchase_credit_works() { + TestExt::new().endow(1, 50).execute_with(|| { + assert_ok!(Broker::do_start_sales(100, 1)); + advance_to(2); + + let credits = CoretimeCredit::get(); + assert_eq!(credits.get(&1), None); + + assert_noop!(Broker::do_purchase_credit(1, 10, 1), Error::<Test>::CreditPurchaseTooSmall); + assert_noop!(Broker::do_purchase_credit(1, 100, 1), TokenError::FundsUnavailable); + + assert_ok!(Broker::do_purchase_credit(1, 50, 1)); + let credits = CoretimeCredit::get(); + assert_eq!(credits.get(&1), Some(&50)); + }); +} + #[test] fn partition_works() { TestExt::new().endow(1, 1000).execute_with(|| {