diff --git a/pallets/collator-assignment/src/benchmarking.rs b/pallets/collator-assignment/src/benchmarking.rs index c82d149d6..10b67e07e 100644 --- a/pallets/collator-assignment/src/benchmarking.rs +++ b/pallets/collator-assignment/src/benchmarking.rs @@ -68,7 +68,7 @@ mod benchmarks { let container_chains: Vec<_> = (0..y).map(|i| ParaId::from(2000 + i)).collect(); let session_index = 0u32.into(); T::ContainerChains::set_session_container_chains(session_index, &container_chains); - T::RemoveParaIdsWithNoCredits::make_valid_para_ids(&container_chains); + T::ParaIdAssignmentHooks::make_valid_para_ids(&container_chains); T::HostConfiguration::set_host_configuration(session_index); // Assign random collators to test worst case: when collators need to be checked against existing collators diff --git a/pallets/collator-assignment/src/lib.rs b/pallets/collator-assignment/src/lib.rs index fa63c6757..227b5e7b3 100644 --- a/pallets/collator-assignment/src/lib.rs +++ b/pallets/collator-assignment/src/lib.rs @@ -54,9 +54,9 @@ use { }, sp_std::{collections::btree_set::BTreeSet, fmt::Debug, prelude::*, vec}, tp_traits::{ - CollatorAssignmentHook, CollatorAssignmentTip, GetContainerChainAuthor, - GetHostConfiguration, GetSessionContainerChains, ParaId, RemoveInvulnerables, - RemoveParaIdsWithNoCredits, ShouldRotateAllCollators, Slot, + CollatorAssignmentTip, GetContainerChainAuthor, GetHostConfiguration, + GetSessionContainerChains, ParaId, ParaIdAssignmentHooks, RemoveInvulnerables, + ShouldRotateAllCollators, Slot, }, }; pub use {dp_collator_assignment::AssignedCollators, pallet::*}; @@ -105,8 +105,7 @@ pub mod pallet { type ShouldRotateAllCollators: ShouldRotateAllCollators; type GetRandomnessForNextBlock: GetRandomnessForNextBlock>; type RemoveInvulnerables: RemoveInvulnerables; - type RemoveParaIdsWithNoCredits: RemoveParaIdsWithNoCredits; - type CollatorAssignmentHook: CollatorAssignmentHook>; + type ParaIdAssignmentHooks: ParaIdAssignmentHooks, Self::AccountId>; type Currency: Currency; type CollatorAssignmentTip: CollatorAssignmentTip>; type ForceEmptyOrchestrator: Get; @@ -309,16 +308,13 @@ pub mod pallet { old_assigned.container_chains.keys().cloned().collect(); // Remove the containerChains that do not have enough credits for block production - T::RemoveParaIdsWithNoCredits::remove_para_ids_with_no_credits( + T::ParaIdAssignmentHooks::pre_assignment( &mut container_chain_ids, &old_assigned_para_ids, ); // TODO: parathreads should be treated a bit differently, they don't need to have the same amount of credits // as parathreads because they will not be producing blocks on every slot. - T::RemoveParaIdsWithNoCredits::remove_para_ids_with_no_credits( - &mut parathreads, - &old_assigned_para_ids, - ); + T::ParaIdAssignmentHooks::pre_assignment(&mut parathreads, &old_assigned_para_ids); let mut shuffle_collators = None; // If the random_seed is all zeros, we don't shuffle the list of collators nor the list @@ -467,51 +463,11 @@ pub mod pallet { // TODO: this probably is asking for a refactor // only apply the onCollatorAssignedHook if sufficient collators - for para_id in &container_chain_ids { - if !new_assigned - .container_chains - .get(para_id) - .unwrap_or(&vec![]) - .is_empty() - { - if let Err(e) = T::CollatorAssignmentHook::on_collators_assigned( - *para_id, - maybe_tip.as_ref(), - false, - ) { - // On error remove para from assignment - log::warn!( - "CollatorAssignmentHook error! Removing para {} from assignment: {:?}", - u32::from(*para_id), - e - ); - new_assigned.container_chains.remove(para_id); - } - } - } - - for para_id in ¶threads { - if !new_assigned - .container_chains - .get(para_id) - .unwrap_or(&vec![]) - .is_empty() - { - if let Err(e) = T::CollatorAssignmentHook::on_collators_assigned( - *para_id, - maybe_tip.as_ref(), - true, - ) { - // On error remove para from assignment - log::warn!( - "CollatorAssignmentHook error! Removing para {} from assignment: {:?}", - u32::from(*para_id), - e - ); - new_assigned.container_chains.remove(para_id); - } - } - } + T::ParaIdAssignmentHooks::post_assignment( + &old_assigned_para_ids, + &mut new_assigned.container_chains, + &maybe_tip, + ); Self::store_collator_fullness( &new_assigned, diff --git a/pallets/collator-assignment/src/mock.rs b/pallets/collator-assignment/src/mock.rs index 9059b4443..f59e3ae4c 100644 --- a/pallets/collator-assignment/src/mock.rs +++ b/pallets/collator-assignment/src/mock.rs @@ -33,8 +33,8 @@ use { }, sp_std::collections::{btree_map::BTreeMap, btree_set::BTreeSet}, tp_traits::{ - CollatorAssignmentHook, CollatorAssignmentTip, ParaId, ParathreadParams, - RemoveInvulnerables, RemoveParaIdsWithNoCredits, SessionContainerChains, + CollatorAssignmentTip, ParaId, ParaIdAssignmentHooks, ParathreadParams, + RemoveInvulnerables, SessionContainerChains, }, tracing_subscriber::{layer::SubscriberExt, FmtSubscriber}, }; @@ -310,23 +310,6 @@ impl CollatorAssignmentTip for MockCollatorAssignmentTip { } } } -pub struct MockCollatorAssignmentHook; - -impl CollatorAssignmentHook for MockCollatorAssignmentHook { - fn on_collators_assigned( - para_id: ParaId, - _maybe_tip: Option<&u32>, - _is_parathread: bool, - ) -> Result { - // Only fail for para 1001 - if MockData::mock().assignment_hook_errors && para_id == 1001.into() { - // The error doesn't matter - Err(sp_runtime::DispatchError::Unavailable) - } else { - Ok(Weight::default()) - } - } -} pub struct GetCoreAllocationConfigurationImpl; @@ -352,8 +335,7 @@ impl pallet_collator_assignment::Config for Test { RotateCollatorsEveryNSessions; type GetRandomnessForNextBlock = MockGetRandomnessForNextBlock; type RemoveInvulnerables = RemoveAccountIdsAbove100; - type RemoveParaIdsWithNoCredits = RemoveParaIdsAbove5000; - type CollatorAssignmentHook = MockCollatorAssignmentHook; + type ParaIdAssignmentHooks = MockParaIdAssignmentHooksImpl; type CollatorAssignmentTip = MockCollatorAssignmentTip; type ForceEmptyOrchestrator = ConstBool; type Currency = (); @@ -414,16 +396,22 @@ impl RemoveInvulnerables for RemoveAccountIdsAbove100 { } /// Any ParaId >= 5000 will be considered to not have enough credits -pub struct RemoveParaIdsAbove5000; +pub struct MockParaIdAssignmentHooksImpl; -impl RemoveParaIdsWithNoCredits for RemoveParaIdsAbove5000 { - fn remove_para_ids_with_no_credits( - para_ids: &mut Vec, - _currently_assigned: &BTreeSet, - ) { +impl ParaIdAssignmentHooks for MockParaIdAssignmentHooksImpl { + fn pre_assignment(para_ids: &mut Vec, _old_assigned: &BTreeSet) { para_ids.retain(|para_id| *para_id <= ParaId::from(5000)); } + fn post_assignment( + _current_assigned: &BTreeSet, + new_assigned: &mut BTreeMap>, + _maybe_tip: &Option, + ) -> Weight { + new_assigned.retain(|para_id, _| *para_id <= ParaId::from(5000)); + Weight::zero() + } + #[cfg(feature = "runtime-benchmarks")] fn make_valid_para_ids(_para_ids: &[ParaId]) {} } diff --git a/pallets/collator-assignment/src/tests.rs b/pallets/collator-assignment/src/tests.rs index 8010ce5ad..e7dd4aa2f 100644 --- a/pallets/collator-assignment/src/tests.rs +++ b/pallets/collator-assignment/src/tests.rs @@ -1344,64 +1344,6 @@ fn assign_collators_prioritizing_tip() { }); } -#[test] -fn on_collators_assigned_hook_failure_removes_para_from_assignment() { - new_test_ext().execute_with(|| { - run_to_block(1); - - MockData::mutate(|m| { - m.collators_per_container = 2; - m.collators_per_parathread = 2; - m.min_orchestrator_chain_collators = 5; - m.max_orchestrator_chain_collators = 5; - - m.collators = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; - m.container_chains = vec![1001, 1002, 1003, 1004]; - m.assignment_hook_errors = false; - }); - run_to_block(11); - - assert_eq!( - assigned_collators(), - BTreeMap::from_iter(vec![ - (1, 1000), - (2, 1000), - (3, 1000), - (4, 1000), - (5, 1000), - (6, 1001), - (7, 1001), - (8, 1002), - (9, 1002), - (10, 1003), - (11, 1003), - ]), - ); - - // Para 1001 will fail on_assignment_hook - MockData::mutate(|m| { - m.assignment_hook_errors = true; - }); - - run_to_block(21); - - assert_eq!( - assigned_collators(), - BTreeMap::from_iter(vec![ - (1, 1000), - (2, 1000), - (3, 1000), - (4, 1000), - (5, 1000), - (8, 1002), - (9, 1002), - (10, 1003), - (11, 1003), - ]), - ); - }); -} - #[test] fn assign_collators_truncates_before_shuffling() { // Check that if there are more collators than needed, we only assign the first collators diff --git a/pallets/services-payment/src/lib.rs b/pallets/services-payment/src/lib.rs index 0c4cb4333..65da9e6c7 100644 --- a/pallets/services-payment/src/lib.rs +++ b/pallets/services-payment/src/lib.rs @@ -422,6 +422,27 @@ pub mod pallet { }); } + pub fn charge_tip(para_id: &ParaId, tip: &BalanceOf) -> Result<(), DispatchError> { + // Only charge the tip to the paras that had a max tip set + // (aka were willing to tip for being assigned a collator) + if MaxTip::::get(para_id).is_some() { + let tip_imbalance = T::Currency::withdraw( + &Self::parachain_tank(*para_id), + *tip, + WithdrawReasons::TIP, + ExistenceRequirement::KeepAlive, + )?; + + Self::deposit_event(Event::::CollatorAssignmentTipCollected { + para_id: *para_id, + payer: Self::parachain_tank(*para_id), + tip: *tip, + }); + T::OnChargeForCollatorAssignmentTip::on_unbalanced(tip_imbalance); + } + Ok(()) + } + pub fn free_block_production_credits(para_id: ParaId) -> Option> { BlockProductionCredits::::get(para_id) } diff --git a/primitives/traits/src/lib.rs b/primitives/traits/src/lib.rs index 41d1189fa..d7c1269d0 100644 --- a/primitives/traits/src/lib.rs +++ b/primitives/traits/src/lib.rs @@ -45,7 +45,7 @@ use { traits::{CheckedAdd, CheckedMul}, ArithmeticError, DispatchResult, Perbill, RuntimeDebug, }, - sp_std::{collections::btree_set::BTreeSet, vec::Vec}, + sp_std::{collections::btree_map::BTreeMap, collections::btree_set::BTreeSet, vec::Vec}, }; // Separate import as rustfmt wrongly change it to `sp_std::vec::self`, which is the module instead @@ -272,24 +272,30 @@ impl RemoveInvulnerables for () { /// Helper trait for pallet_collator_assignment to be able to not assign collators to container chains with no credits /// in pallet_services_payment -pub trait RemoveParaIdsWithNoCredits { +pub trait ParaIdAssignmentHooks { /// Remove para ids with not enough credits. The resulting order will affect priority: the first para id in the list /// will be the first one to get collators. - fn remove_para_ids_with_no_credits( - para_ids: &mut Vec, - currently_assigned: &BTreeSet, - ); + fn pre_assignment(para_ids: &mut Vec, old_assigned: &BTreeSet); + fn post_assignment( + current_assigned: &BTreeSet, + new_assigned: &mut BTreeMap>, + maybe_tip: &Option, + ) -> Weight; /// Make those para ids valid by giving them enough credits, for benchmarking. #[cfg(feature = "runtime-benchmarks")] fn make_valid_para_ids(para_ids: &[ParaId]); } -impl RemoveParaIdsWithNoCredits for () { - fn remove_para_ids_with_no_credits( - _para_ids: &mut Vec, - _currently_assigned: &BTreeSet, - ) { +impl ParaIdAssignmentHooks for () { + fn pre_assignment(_para_ids: &mut Vec, _currently_assigned: &BTreeSet) {} + + fn post_assignment( + _current_assigned: &BTreeSet, + _new_assigned: &mut BTreeMap>, + _maybe_tip: &Option, + ) -> Weight { + Default::default() } #[cfg(feature = "runtime-benchmarks")] diff --git a/runtime/dancebox/src/lib.rs b/runtime/dancebox/src/lib.rs index ea045d1d6..d549d3f18 100644 --- a/runtime/dancebox/src/lib.rs +++ b/runtime/dancebox/src/lib.rs @@ -24,18 +24,22 @@ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); pub mod xcm_config; +use frame_support::storage::{with_storage_layer, with_transaction}; +use frame_support::traits::{ExistenceRequirement, WithdrawReasons}; use polkadot_runtime_common::SlowAdjustingFeeUpdate; #[cfg(feature = "std")] use sp_version::NativeVersion; #[cfg(any(feature = "std", test))] pub use sp_runtime::BuildStorage; +use sp_runtime::{DispatchError, TransactionOutcome}; pub mod weights; #[cfg(test)] mod tests; +use pallet_services_payment::BalanceOf; use { cumulus_pallet_parachain_system::{ RelayChainStateProof, RelayNumberMonotonicallyIncreases, RelaychainDataProvider, @@ -103,6 +107,7 @@ use { transaction_validity::{TransactionSource, TransactionValidity}, AccountId32, ApplyExtrinsicResult, }, + sp_std::collections::btree_map::BTreeMap, sp_std::{collections::btree_set::BTreeSet, marker::PhantomData, prelude::*}, sp_version::RuntimeVersion, staging_xcm::{ @@ -110,8 +115,8 @@ use { }, tp_traits::{ apply, derive_storage_traits, GetContainerChainAuthor, GetHostConfiguration, - GetSessionContainerChains, MaybeSelfChainBlockAuthor, RelayStorageRootProvider, - RemoveInvulnerables, RemoveParaIdsWithNoCredits, SlotFrequency, + GetSessionContainerChains, MaybeSelfChainBlockAuthor, ParaIdAssignmentHooks, + RelayStorageRootProvider, RemoveInvulnerables, SlotFrequency, }, tp_xcm_core_buyer::BuyCoreCollatorProof, xcm_runtime_apis::{ @@ -807,56 +812,126 @@ impl RemoveInvulnerables for RemoveInvulnerablesImpl { } } -pub struct RemoveParaIdsWithNoCreditsImpl; +pub struct ParaIdAssignmentHooksImpl; -impl RemoveParaIdsWithNoCredits for RemoveParaIdsWithNoCreditsImpl { - fn remove_para_ids_with_no_credits( - para_ids: &mut Vec, +impl ParaIdAssignmentHooksImpl { + fn charge_para_ids_internal( + blocks_per_session: tp_traits::BlockNumber, + para_id: ParaId, currently_assigned: &BTreeSet, - ) { - let blocks_per_session = Period::get(); - - para_ids.retain(|para_id| { - // If the para has been assigned collators for this session it must have enough block credits - // for the current and the next session. - let block_credits_needed = if currently_assigned.contains(para_id) { - blocks_per_session * 2 + maybe_tip: &Option>, + ) -> Result { + use frame_support::traits::Currency; + type ServicePaymentCurrency = ::Currency; + + // Check if the container chain has enough credits for a session assignments + let maybe_assignment_imbalance = + if pallet_services_payment::Pallet::::burn_collator_assignment_free_credit_for_para(¶_id).is_err() { + let (amount_to_charge, _weight) = + ::ProvideCollatorAssignmentCost::collator_assignment_cost(¶_id); + Some(>::withdraw( + &pallet_services_payment::Pallet::::parachain_tank(para_id), + amount_to_charge, + WithdrawReasons::FEE, + ExistenceRequirement::KeepAlive, + )?) } else { - blocks_per_session + None }; - // Check if the container chain has enough credits for producing blocks - let free_block_credits = pallet_services_payment::BlockProductionCredits::::get(para_id) - .unwrap_or_default(); - - // Check if the container chain has enough credits for a session assignments - let free_session_credits = pallet_services_payment::CollatorAssignmentCredits::::get(para_id) - .unwrap_or_default(); - - // If para's max tip is set it should have enough to pay for one assignment with tip - let max_tip = pallet_services_payment::MaxTip::::get(para_id).unwrap_or_default() ; - - // Return if we can survive with free credits - if free_block_credits >= block_credits_needed && free_session_credits >= 1 { - // Max tip should always be checked, as it can be withdrawn even if free credits were used - return Balances::can_withdraw(&pallet_services_payment::Pallet::::parachain_tank(*para_id), max_tip).into_result(true).is_ok() + if let Some(tip) = maybe_tip { + if let Err(e) = pallet_services_payment::Pallet::::charge_tip(¶_id, tip) { + // Return assignment imbalance to tank on error + if let Some(assignment_imbalance) = maybe_assignment_imbalance { + ::Currency::resolve_creating( + &pallet_services_payment::Pallet::::parachain_tank(para_id), + assignment_imbalance, + ); + } + return Err(e); } + } - let remaining_block_credits = block_credits_needed.saturating_sub(free_block_credits); - let remaining_session_credits = 1u32.saturating_sub(free_session_credits); + if let Some(assignment_imbalance) = maybe_assignment_imbalance { + ::OnChargeForCollatorAssignment::on_unbalanced(assignment_imbalance); + } - let (block_production_costs, _) = ::ProvideBlockProductionCost::block_cost(para_id); - let (collator_assignment_costs, _) = ::ProvideCollatorAssignmentCost::collator_assignment_cost(para_id); - // let's check if we can withdraw - let remaining_block_credits_to_pay = u128::from(remaining_block_credits).saturating_mul(block_production_costs); - let remaining_session_credits_to_pay = u128::from(remaining_session_credits).saturating_mul(collator_assignment_costs); + // If the para has been assigned collators for this session it must have enough block credits + // for the current and the next session. + let block_credits_needed = if currently_assigned.contains(¶_id) { + blocks_per_session * 2 + } else { + blocks_per_session + }; + // Check if the container chain has enough credits for producing blocks + let free_block_credits = + pallet_services_payment::BlockProductionCredits::::get(para_id) + .unwrap_or_default(); + let remaining_block_credits = block_credits_needed.saturating_sub(free_block_credits); + let (block_production_costs, _) = + ::ProvideBlockProductionCost::block_cost( + ¶_id, + ); + // Check if we can withdraw + let remaining_block_credits_to_pay = + u128::from(remaining_block_credits).saturating_mul(block_production_costs); + let remaining_to_pay = remaining_block_credits_to_pay; + // This should take into account whether we tank goes below ED + // The true refers to keepAlive + Balances::can_withdraw( + &pallet_services_payment::Pallet::::parachain_tank(para_id), + remaining_to_pay, + ) + .into_result(true)?; + // TODO: Have proper weight + Ok(Weight::zero()) + } +} - let remaining_to_pay = remaining_block_credits_to_pay.saturating_add(remaining_session_credits_to_pay).saturating_add(max_tip); +impl ParaIdAssignmentHooks, AC> for ParaIdAssignmentHooksImpl { + fn pre_assignment(para_ids: &mut Vec, currently_assigned: &BTreeSet) { + let blocks_per_session = Period::get(); + para_ids.retain(|para_id| { + with_transaction(|| { + let max_tip = + pallet_services_payment::MaxTip::::get(para_id).unwrap_or_default(); + TransactionOutcome::Rollback(Self::charge_para_ids_internal( + blocks_per_session, + *para_id, + currently_assigned, + &Some(max_tip), + )) + }) + .is_ok() + }); + } - // This should take into account whether we tank goes below ED - // The true refers to keepAlive - Balances::can_withdraw(&pallet_services_payment::Pallet::::parachain_tank(*para_id), remaining_to_pay).into_result(true).is_ok() + fn post_assignment( + current_assigned: &BTreeSet, + new_assigned: &mut BTreeMap>, + maybe_tip: &Option>, + ) -> Weight { + let blocks_per_session = Period::get(); + let mut total_weight = Weight::zero(); + new_assigned.retain(|¶_id, collators| { + // Short-circuit in case collators are empty + if collators.is_empty() { + return true; + } + with_storage_layer(|| { + Self::charge_para_ids_internal( + blocks_per_session, + para_id, + current_assigned, + maybe_tip, + ) + }) + .inspect(|weight| { + total_weight += *weight; + }) + .is_ok() }); + total_weight } /// Make those para ids valid by giving them enough credits, for benchmarking. @@ -894,8 +969,7 @@ impl pallet_collator_assignment::Config for Runtime { RotateCollatorsEveryNSessions; type GetRandomnessForNextBlock = BabeGetRandomnessForNextBlock; type RemoveInvulnerables = RemoveInvulnerablesImpl; - type RemoveParaIdsWithNoCredits = RemoveParaIdsWithNoCreditsImpl; - type CollatorAssignmentHook = ServicesPayment; + type ParaIdAssignmentHooks = ParaIdAssignmentHooksImpl; type CollatorAssignmentTip = ServicesPayment; type Currency = Balances; type ForceEmptyOrchestrator = ConstBool; diff --git a/runtime/flashbox/src/lib.rs b/runtime/flashbox/src/lib.rs index 0f65e7832..1c3be8bc9 100644 --- a/runtime/flashbox/src/lib.rs +++ b/runtime/flashbox/src/lib.rs @@ -22,6 +22,8 @@ #[cfg(feature = "std")] include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); +use frame_support::storage::{with_storage_layer, with_transaction}; +use frame_support::traits::{ExistenceRequirement, WithdrawReasons}; #[cfg(feature = "std")] use sp_version::NativeVersion; use { @@ -31,12 +33,14 @@ use { #[cfg(any(feature = "std", test))] pub use sp_runtime::BuildStorage; +use sp_runtime::{DispatchError, TransactionOutcome}; pub mod weights; #[cfg(test)] mod tests; +use pallet_services_payment::BalanceOf; use { cumulus_pallet_parachain_system::RelayNumberMonotonicallyIncreases, cumulus_primitives_core::{relay_chain::SessionIndex, BodyId, ParaId}, @@ -94,12 +98,13 @@ use { transaction_validity::{TransactionSource, TransactionValidity}, AccountId32, ApplyExtrinsicResult, }, + sp_std::collections::btree_map::BTreeMap, sp_std::{collections::btree_set::BTreeSet, marker::PhantomData, prelude::*}, sp_version::RuntimeVersion, tp_traits::{ apply, derive_storage_traits, GetContainerChainAuthor, GetHostConfiguration, - GetSessionContainerChains, MaybeSelfChainBlockAuthor, RelayStorageRootProvider, - RemoveInvulnerables, RemoveParaIdsWithNoCredits, ShouldRotateAllCollators, + GetSessionContainerChains, MaybeSelfChainBlockAuthor, ParaIdAssignmentHooks, + RelayStorageRootProvider, RemoveInvulnerables, ShouldRotateAllCollators, }, }; pub use { @@ -648,56 +653,126 @@ impl RemoveInvulnerables for RemoveInvulnerablesImpl { } } -pub struct RemoveParaIdsWithNoCreditsImpl; +pub struct ParaIdAssignmentHooksImpl; -impl RemoveParaIdsWithNoCredits for RemoveParaIdsWithNoCreditsImpl { - fn remove_para_ids_with_no_credits( - para_ids: &mut Vec, +impl ParaIdAssignmentHooksImpl { + fn charge_para_ids_internal( + blocks_per_session: tp_traits::BlockNumber, + para_id: ParaId, currently_assigned: &BTreeSet, - ) { - let blocks_per_session = Period::get(); - - para_ids.retain(|para_id| { - // If the para has been assigned collators for this session it must have enough block credits - // for the current and the next session. - let block_credits_needed = if currently_assigned.contains(para_id) { - blocks_per_session * 2 + maybe_tip: &Option>, + ) -> Result { + use frame_support::traits::Currency; + type ServicePaymentCurrency = ::Currency; + + // Check if the container chain has enough credits for a session assignments + let maybe_assignment_imbalance = + if pallet_services_payment::Pallet::::burn_collator_assignment_free_credit_for_para(¶_id).is_err() { + let (amount_to_charge, _weight) = + ::ProvideCollatorAssignmentCost::collator_assignment_cost(¶_id); + Some(>::withdraw( + &pallet_services_payment::Pallet::::parachain_tank(para_id), + amount_to_charge, + WithdrawReasons::FEE, + ExistenceRequirement::KeepAlive, + )?) } else { - blocks_per_session + None }; - // Check if the container chain has enough credits for producing blocks - let free_block_credits = pallet_services_payment::BlockProductionCredits::::get(para_id) - .unwrap_or_default(); - - // Check if the container chain has enough credits for a session assignments - let free_session_credits = pallet_services_payment::CollatorAssignmentCredits::::get(para_id) - .unwrap_or_default(); - - // If para's max tip is set it should have enough to pay for one assignment with tip - let max_tip = pallet_services_payment::MaxTip::::get(para_id).unwrap_or_default() ; - - // Return if we can survive with free credits - if free_block_credits >= block_credits_needed && free_session_credits >= 1 { - // Max tip should always be checked, as it can be withdrawn even if free credits were used - return Balances::can_withdraw(&pallet_services_payment::Pallet::::parachain_tank(*para_id), max_tip).into_result(true).is_ok() + if let Some(tip) = maybe_tip { + if let Err(e) = pallet_services_payment::Pallet::::charge_tip(¶_id, tip) { + // Return assignment imbalance to tank on error + if let Some(assignment_imbalance) = maybe_assignment_imbalance { + ::Currency::resolve_creating( + &pallet_services_payment::Pallet::::parachain_tank(para_id), + assignment_imbalance, + ); + } + return Err(e); } + } - let remaining_block_credits = block_credits_needed.saturating_sub(free_block_credits); - let remaining_session_credits = 1u32.saturating_sub(free_session_credits); + if let Some(assignment_imbalance) = maybe_assignment_imbalance { + ::OnChargeForCollatorAssignment::on_unbalanced(assignment_imbalance); + } - let (block_production_costs, _) = ::ProvideBlockProductionCost::block_cost(para_id); - let (collator_assignment_costs, _) = ::ProvideCollatorAssignmentCost::collator_assignment_cost(para_id); - // let's check if we can withdraw - let remaining_block_credits_to_pay = u128::from(remaining_block_credits).saturating_mul(block_production_costs); - let remaining_session_credits_to_pay = u128::from(remaining_session_credits).saturating_mul(collator_assignment_costs); + // If the para has been assigned collators for this session it must have enough block credits + // for the current and the next session. + let block_credits_needed = if currently_assigned.contains(¶_id) { + blocks_per_session * 2 + } else { + blocks_per_session + }; + // Check if the container chain has enough credits for producing blocks + let free_block_credits = + pallet_services_payment::BlockProductionCredits::::get(para_id) + .unwrap_or_default(); + let remaining_block_credits = block_credits_needed.saturating_sub(free_block_credits); + let (block_production_costs, _) = + ::ProvideBlockProductionCost::block_cost( + ¶_id, + ); + // Check if we can withdraw + let remaining_block_credits_to_pay = + u128::from(remaining_block_credits).saturating_mul(block_production_costs); + let remaining_to_pay = remaining_block_credits_to_pay; + // This should take into account whether we tank goes below ED + // The true refers to keepAlive + Balances::can_withdraw( + &pallet_services_payment::Pallet::::parachain_tank(para_id), + remaining_to_pay, + ) + .into_result(true)?; + // TODO: Have proper weight + Ok(Weight::zero()) + } +} - let remaining_to_pay = remaining_block_credits_to_pay.saturating_add(remaining_session_credits_to_pay).saturating_add(max_tip); +impl ParaIdAssignmentHooks, AC> for ParaIdAssignmentHooksImpl { + fn pre_assignment(para_ids: &mut Vec, currently_assigned: &BTreeSet) { + let blocks_per_session = Period::get(); + para_ids.retain(|para_id| { + with_transaction(|| { + let max_tip = + pallet_services_payment::MaxTip::::get(para_id).unwrap_or_default(); + TransactionOutcome::Rollback(Self::charge_para_ids_internal( + blocks_per_session, + *para_id, + currently_assigned, + &Some(max_tip), + )) + }) + .is_ok() + }); + } - // This should take into account whether we tank goes below ED - // The true refers to keepAlive - Balances::can_withdraw(&pallet_services_payment::Pallet::::parachain_tank(*para_id), remaining_to_pay).into_result(true).is_ok() + fn post_assignment( + current_assigned: &BTreeSet, + new_assigned: &mut BTreeMap>, + maybe_tip: &Option>, + ) -> Weight { + let blocks_per_session = Period::get(); + let mut total_weight = Weight::zero(); + new_assigned.retain(|¶_id, collators| { + // Short-circuit in case collators are empty + if collators.is_empty() { + return true; + } + with_storage_layer(|| { + Self::charge_para_ids_internal( + blocks_per_session, + para_id, + current_assigned, + maybe_tip, + ) + }) + .inspect(|weight| { + total_weight += *weight; + }) + .is_ok() }); + total_weight } /// Make those para ids valid by giving them enough credits, for benchmarking. @@ -742,8 +817,7 @@ impl pallet_collator_assignment::Config for Runtime { type ShouldRotateAllCollators = NeverRotateCollators; type GetRandomnessForNextBlock = (); type RemoveInvulnerables = RemoveInvulnerablesImpl; - type RemoveParaIdsWithNoCredits = RemoveParaIdsWithNoCreditsImpl; - type CollatorAssignmentHook = ServicesPayment; + type ParaIdAssignmentHooks = ParaIdAssignmentHooksImpl; type CollatorAssignmentTip = ServicesPayment; type Currency = Balances; type ForceEmptyOrchestrator = ConstBool; diff --git a/solo-chains/runtime/dancelight/src/lib.rs b/solo-chains/runtime/dancelight/src/lib.rs index f30bd9c31..16b22c516 100644 --- a/solo-chains/runtime/dancelight/src/lib.rs +++ b/solo-chains/runtime/dancelight/src/lib.rs @@ -20,6 +20,7 @@ // `construct_runtime!` does a lot of recursion and requires us to increase the limit. #![recursion_limit = "512"] +use frame_support::storage::{with_storage_layer, with_transaction}; // Fix compile error in impl_runtime_weights! macro use { authority_discovery_primitives::AuthorityId as AuthorityDiscoveryId, @@ -93,7 +94,7 @@ use { }, tp_traits::{ apply, derive_storage_traits, EraIndex, GetHostConfiguration, GetSessionContainerChains, - RegistrarHandler, RemoveParaIdsWithNoCredits, Slot, SlotFrequency, + ParaIdAssignmentHooks, RegistrarHandler, Slot, SlotFrequency, }, }; @@ -601,9 +602,12 @@ pub struct TreasuryBenchmarkHelper(PhantomData); #[cfg(feature = "runtime-benchmarks")] use frame_support::traits::Currency; +use frame_support::traits::{ExistenceRequirement, OnUnbalanced, WithdrawReasons}; +use pallet_services_payment::BalanceOf; #[cfg(feature = "runtime-benchmarks")] use pallet_treasury::ArgumentsFactory; use runtime_parachains::configuration::HostConfiguration; +use sp_runtime::{DispatchError, TransactionOutcome}; #[cfg(feature = "runtime-benchmarks")] impl ArgumentsFactory<(), T::AccountId> for TreasuryBenchmarkHelper @@ -3184,56 +3188,126 @@ impl frame_support::traits::Randomness for BabeCurrentBlockRa } } -pub struct RemoveParaIdsWithNoCreditsImpl; +pub struct ParaIdAssignmentHooksImpl; -impl RemoveParaIdsWithNoCredits for RemoveParaIdsWithNoCreditsImpl { - fn remove_para_ids_with_no_credits( - para_ids: &mut Vec, +impl ParaIdAssignmentHooksImpl { + fn charge_para_ids_internal( + blocks_per_session: BlockNumber, + para_id: ParaId, currently_assigned: &BTreeSet, - ) { - let blocks_per_session = EpochDurationInBlocks::get(); - - para_ids.retain(|para_id| { - // If the para has been assigned collators for this session it must have enough block credits - // for the current and the next session. - let block_credits_needed = if currently_assigned.contains(para_id) { - blocks_per_session * 2 + maybe_tip: &Option>, + ) -> Result { + use frame_support::traits::Currency; + type ServicePaymentCurrency = ::Currency; + + // Check if the container chain has enough credits for a session assignments + let maybe_assignment_imbalance = + if pallet_services_payment::Pallet::::burn_collator_assignment_free_credit_for_para(¶_id).is_err() { + let (amount_to_charge, _weight) = + ::ProvideCollatorAssignmentCost::collator_assignment_cost(¶_id); + Some(>::withdraw( + &pallet_services_payment::Pallet::::parachain_tank(para_id), + amount_to_charge, + WithdrawReasons::FEE, + ExistenceRequirement::KeepAlive, + )?) } else { - blocks_per_session + None }; - // Check if the container chain has enough credits for producing blocks - let free_block_credits = pallet_services_payment::BlockProductionCredits::::get(para_id) - .unwrap_or_default(); - - // Check if the container chain has enough credits for a session assignments - let free_session_credits = pallet_services_payment::CollatorAssignmentCredits::::get(para_id) - .unwrap_or_default(); - - // If para's max tip is set it should have enough to pay for one assignment with tip - let max_tip = pallet_services_payment::MaxTip::::get(para_id).unwrap_or_default() ; - - // Return if we can survive with free credits - if free_block_credits >= block_credits_needed && free_session_credits >= 1 { - // Max tip should always be checked, as it can be withdrawn even if free credits were used - return Balances::can_withdraw(&pallet_services_payment::Pallet::::parachain_tank(*para_id), max_tip).into_result(true).is_ok() + if let Some(tip) = maybe_tip { + if let Err(e) = pallet_services_payment::Pallet::::charge_tip(¶_id, tip) { + // Return assignment imbalance to tank on error + if let Some(assignment_imbalance) = maybe_assignment_imbalance { + ::Currency::resolve_creating( + &pallet_services_payment::Pallet::::parachain_tank(para_id), + assignment_imbalance, + ); + } + return Err(e); } + } - let remaining_block_credits = block_credits_needed.saturating_sub(free_block_credits); - let remaining_session_credits = 1u32.saturating_sub(free_session_credits); + if let Some(assignment_imbalance) = maybe_assignment_imbalance { + ::OnChargeForCollatorAssignment::on_unbalanced(assignment_imbalance); + } - let (block_production_costs, _) = ::ProvideBlockProductionCost::block_cost(para_id); - let (collator_assignment_costs, _) = ::ProvideCollatorAssignmentCost::collator_assignment_cost(para_id); - // let's check if we can withdraw - let remaining_block_credits_to_pay = u128::from(remaining_block_credits).saturating_mul(block_production_costs); - let remaining_session_credits_to_pay = u128::from(remaining_session_credits).saturating_mul(collator_assignment_costs); + // If the para has been assigned collators for this session it must have enough block credits + // for the current and the next session. + let block_credits_needed = if currently_assigned.contains(¶_id) { + blocks_per_session * 2 + } else { + blocks_per_session + }; + // Check if the container chain has enough credits for producing blocks + let free_block_credits = + pallet_services_payment::BlockProductionCredits::::get(para_id) + .unwrap_or_default(); + let remaining_block_credits = block_credits_needed.saturating_sub(free_block_credits); + let (block_production_costs, _) = + ::ProvideBlockProductionCost::block_cost( + ¶_id, + ); + // Check if we can withdraw + let remaining_block_credits_to_pay = + u128::from(remaining_block_credits).saturating_mul(block_production_costs); + let remaining_to_pay = remaining_block_credits_to_pay; + // This should take into account whether we tank goes below ED + // The true refers to keepAlive + Balances::can_withdraw( + &pallet_services_payment::Pallet::::parachain_tank(para_id), + remaining_to_pay, + ) + .into_result(true)?; + // TODO: Have proper weight + Ok(Weight::zero()) + } +} - let remaining_to_pay = remaining_block_credits_to_pay.saturating_add(remaining_session_credits_to_pay).saturating_add(max_tip); +impl ParaIdAssignmentHooks, AC> for ParaIdAssignmentHooksImpl { + fn pre_assignment(para_ids: &mut Vec, currently_assigned: &BTreeSet) { + let blocks_per_session = EpochDurationInBlocks::get(); + para_ids.retain(|para_id| { + with_transaction(|| { + let max_tip = + pallet_services_payment::MaxTip::::get(para_id).unwrap_or_default(); + TransactionOutcome::Rollback(Self::charge_para_ids_internal( + blocks_per_session, + *para_id, + currently_assigned, + &Some(max_tip), + )) + }) + .is_ok() + }); + } - // This should take into account whether we tank goes below ED - // The true refers to keepAlive - Balances::can_withdraw(&pallet_services_payment::Pallet::::parachain_tank(*para_id), remaining_to_pay).into_result(true).is_ok() + fn post_assignment( + current_assigned: &BTreeSet, + new_assigned: &mut BTreeMap>, + maybe_tip: &Option>, + ) -> Weight { + let blocks_per_session = EpochDurationInBlocks::get(); + let mut total_weight = Weight::zero(); + new_assigned.retain(|¶_id, collators| { + // Short-circuit in case collators are empty + if collators.is_empty() { + return true; + } + with_storage_layer(|| { + Self::charge_para_ids_internal( + blocks_per_session, + para_id, + current_assigned, + maybe_tip, + ) + }) + .inspect(|weight| { + total_weight += *weight; + }) + .is_ok() }); + total_weight } /// Make those para ids valid by giving them enough credits, for benchmarking. @@ -3322,8 +3396,7 @@ impl pallet_collator_assignment::Config for Runtime { RotateCollatorsEveryNSessions; type GetRandomnessForNextBlock = BabeGetRandomnessForNextBlock; type RemoveInvulnerables = (); - type RemoveParaIdsWithNoCredits = RemoveParaIdsWithNoCreditsImpl; - type CollatorAssignmentHook = ServicesPayment; + type ParaIdAssignmentHooks = ParaIdAssignmentHooksImpl; type CollatorAssignmentTip = ServicesPayment; type Currency = Balances; type ForceEmptyOrchestrator = ConstBool;