diff --git a/.github/workflows/publish-docker-tannsi-relay.yml b/.github/workflows/publish-docker-tannsi-relay.yml index 177fe2419..aee02e287 100644 --- a/.github/workflows/publish-docker-tannsi-relay.yml +++ b/.github/workflows/publish-docker-tannsi-relay.yml @@ -33,8 +33,16 @@ jobs: mkdir -p build VERSION="${{ github.event.inputs.tag }}" wget "${{ env.BASE_URL }}/$VERSION/${{ matrix.image.file_name }}" -O build/${{ matrix.image.file_name }} + wget "${{ env.BASE_URL }}/$VERSION/${{ matrix.image.file_name }}-execute-worker" -O build/${{ matrix.image.file_name }}-execute-worker + wget "${{ env.BASE_URL }}/$VERSION/${{ matrix.image.file_name }}-prepare-worker" -O build/${{ matrix.image.file_name }}-prepare-worker + wget "${{ env.BASE_URL }}/$VERSION/${{ matrix.image.file_name }}-skylake" -O build/${{ matrix.image.file_name }}-skylake + wget "${{ env.BASE_URL }}/$VERSION/${{ matrix.image.file_name }}-execute-worker-skylake" -O build/${{ matrix.image.file_name }}-execute-worker-skylake + wget "${{ env.BASE_URL }}/$VERSION/${{ matrix.image.file_name }}-prepare-worker-skylake" -O build/${{ matrix.image.file_name }}-prepare-worker-skylake + wget "${{ env.BASE_URL }}/$VERSION/${{ matrix.image.file_name }}-znver3" -O build/${{ matrix.image.file_name }}-znver3 + wget "${{ env.BASE_URL }}/$VERSION/${{ matrix.image.file_name }}-execute-worker-znver3" -O build/${{ matrix.image.file_name }}-execute-worker-znver3 + wget "${{ env.BASE_URL }}/$VERSION/${{ matrix.image.file_name }}-prepare-worker-znver3" -O build/${{ matrix.image.file_name }}-prepare-worker-znver3 - name: push to docker run: | diff --git a/Cargo.lock b/Cargo.lock index fc9820446..2a9e669c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3405,6 +3405,7 @@ dependencies = [ "pallet-external-validator-slashes", "pallet-external-validators", "pallet-external-validators-rewards", + "pallet-external-validators-rewards-runtime-api", "pallet-grandpa", "pallet-identity", "pallet-inflation-rewards", @@ -9605,6 +9606,7 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "log", "pallet-balances", "pallet-session", "pallet-timestamp", @@ -9612,14 +9614,27 @@ dependencies = [ "polkadot-primitives", "polkadot-runtime-parachains", "scale-info", + "snowbridge-core", + "snowbridge-outbound-queue-merkle-tree", "sp-core", "sp-io", "sp-runtime", "sp-staking", "sp-std", + "tp-bridge", "tp-traits", ] +[[package]] +name = "pallet-external-validators-rewards-runtime-api" +version = "0.1.0" +dependencies = [ + "parity-scale-codec", + "snowbridge-outbound-queue-merkle-tree", + "sp-api", + "sp-core", +] + [[package]] name = "pallet-fast-unstake" version = "37.0.0" diff --git a/Cargo.toml b/Cargo.toml index b36c2ea95..c62dc5ee4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,6 +63,7 @@ pallet-data-preservers-runtime-api = { path = "pallets/data-preservers/runtime-a pallet-external-validator-slashes = { path = "pallets/external-validator-slashes", default-features = false } pallet-external-validators = { path = "pallets/external-validators", default-features = false } pallet-external-validators-rewards = { path = "pallets/external-validators-rewards", default-features = false } +pallet-external-validators-rewards-runtime-api = { path = "pallets/external-validators-rewards/runtime-api", default-features = false } pallet-inflation-rewards = { path = "pallets/inflation-rewards", default-features = false } pallet-initializer = { path = "pallets/initializer", default-features = false } pallet-invulnerables = { path = "pallets/invulnerables", default-features = false } @@ -260,9 +261,11 @@ xcm-runtime-apis = { git = "https://github.com/moondance-labs/polkadot-sdk", bra # Bridges (wasm) alloy-sol-types = { version = "0.4.2", default-features = false } +bridge-hub-common = { git = "https://github.com/moondance-labs/polkadot-sdk", branch = "tanssi-polkadot-stable2407", default-features = false } milagro-bls = { package = "snowbridge-milagro-bls", version = "1.5.4", default-features = false } snowbridge-beacon-primitives = { git = "https://github.com/moondance-labs/polkadot-sdk", branch = "tanssi-polkadot-stable2409", default-features = false } snowbridge-core = { git = "https://github.com/moondance-labs/polkadot-sdk", branch = "tanssi-polkadot-stable2409", default-features = false } +snowbridge-outbound-queue-merkle-tree = { git = "https://github.com/moondance-labs/polkadot-sdk", branch = "tanssi-polkadot-stable2409", default-features = false } snowbridge-pallet-ethereum-client = { git = "https://github.com/moondance-labs/polkadot-sdk", branch = "tanssi-polkadot-stable2409", default-features = false } snowbridge-pallet-inbound-queue = { git = "https://github.com/moondance-labs/polkadot-sdk", branch = "tanssi-polkadot-stable2409", default-features = false } snowbridge-pallet-inbound-queue-fixtures = { git = "https://github.com/moondance-labs/polkadot-sdk", branch = "tanssi-polkadot-stable2409", default-features = false } 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/external-validators-rewards/Cargo.toml b/pallets/external-validators-rewards/Cargo.toml index 579f871b0..43f6da0ce 100644 --- a/pallets/external-validators-rewards/Cargo.toml +++ b/pallets/external-validators-rewards/Cargo.toml @@ -13,14 +13,17 @@ targets = [ "x86_64-unknown-linux-gnu" ] workspace = true [dependencies] +log = { workspace = true } parity-scale-codec = { workspace = true } scale-info = { workspace = true, features = [ "derive" ] } frame-support = { workspace = true } frame-system = { workspace = true } +sp-core = { workspace = true } sp-runtime = { workspace = true } sp-staking = { workspace = true } sp-std = { workspace = true } +tp-bridge = { workspace = true } tp-traits = { workspace = true } frame-benchmarking = { workspace = true } @@ -29,11 +32,13 @@ pallet-balances = { workspace = true, optional = true } pallet-session = { workspace = true, features = [ "historical" ] } runtime-parachains = { workspace = true } +snowbridge-core = { workspace = true } +snowbridge-outbound-queue-merkle-tree = { workspace = true } + polkadot-primitives = { workspace = true } [dev-dependencies] pallet-timestamp = { workspace = true } -sp-core = { workspace = true } sp-io = { workspace = true } [features] @@ -42,6 +47,7 @@ std = [ "frame-benchmarking/std", "frame-support/std", "frame-system/std", + "log/std", "pallet-balances/std", "pallet-session/std", "pallet-timestamp/std", @@ -49,11 +55,14 @@ std = [ "polkadot-primitives/std", "runtime-parachains/std", "scale-info/std", + "snowbridge-core/std", + "snowbridge-outbound-queue-merkle-tree/std", "sp-core/std", "sp-io/std", "sp-runtime/std", "sp-staking/std", "sp-std/std", + "tp-bridge/std", "tp-traits/std", ] runtime-benchmarks = [ @@ -64,8 +73,10 @@ runtime-benchmarks = [ "pallet-timestamp/runtime-benchmarks", "polkadot-primitives/runtime-benchmarks", "runtime-parachains/runtime-benchmarks", + "snowbridge-core/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "sp-staking/runtime-benchmarks", + "tp-bridge/runtime-benchmarks", "tp-traits/runtime-benchmarks", ] diff --git a/pallets/external-validators-rewards/runtime-api/Cargo.toml b/pallets/external-validators-rewards/runtime-api/Cargo.toml new file mode 100644 index 000000000..4fe0c85a8 --- /dev/null +++ b/pallets/external-validators-rewards/runtime-api/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "pallet-external-validators-rewards-runtime-api" +authors = { workspace = true } +description = "Runtime API definition of pallet-external-validators-rewards" +edition = "2021" +license = "GPL-3.0-only" +version = "0.1.0" + +[package.metadata.docs.rs] +targets = [ "x86_64-unknown-linux-gnu" ] + +[lints] +workspace = true + +[dependencies] +parity-scale-codec = { workspace = true } +snowbridge-outbound-queue-merkle-tree = { workspace = true } +sp-api = { workspace = true } +sp-core = { workspace = true } + +[features] +default = [ "std" ] +std = [ + "parity-scale-codec/std", + "snowbridge-outbound-queue-merkle-tree/std", + "sp-api/std", + "sp-core/std", +] diff --git a/pallets/external-validators-rewards/runtime-api/src/lib.rs b/pallets/external-validators-rewards/runtime-api/src/lib.rs new file mode 100644 index 000000000..3d8c6e1b6 --- /dev/null +++ b/pallets/external-validators-rewards/runtime-api/src/lib.rs @@ -0,0 +1,32 @@ +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. + +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Tanssi is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see + +//! Runtime API for External Validators Rewards pallet + +#![cfg_attr(not(feature = "std"), no_std)] + +use snowbridge_outbound_queue_merkle_tree::MerkleProof; + +sp_api::decl_runtime_apis! { + pub trait ExternalValidatorsRewardsApi + where + AccountId: parity_scale_codec::Codec, + EraIndex: parity_scale_codec::Codec, + { + fn generate_rewards_merkle_proof(account_id: AccountId, era_index: EraIndex) -> Option; + fn verify_rewards_merkle_proof(merkle_proof: MerkleProof) -> bool; + } +} diff --git a/pallets/external-validators-rewards/src/benchmarking.rs b/pallets/external-validators-rewards/src/benchmarking.rs new file mode 100644 index 000000000..4c068b026 --- /dev/null +++ b/pallets/external-validators-rewards/src/benchmarking.rs @@ -0,0 +1,81 @@ +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. + +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Tanssi is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see + +//! Benchmarking setup for pallet_external_validators_rewards + +use super::*; + +#[allow(unused)] +use crate::Pallet as ExternalValidatorsRewards; +use { + frame_benchmarking::{account, v2::*, BenchmarkError}, + frame_support::traits::{Currency, Get}, + sp_std::prelude::*, + tp_traits::OnEraEnd, +}; + +const SEED: u32 = 0; + +fn create_funded_user( + string: &'static str, + n: u32, + balance_factor: u32, +) -> T::AccountId { + let user = account(string, n, SEED); + let balance = as Currency>::minimum_balance() + * balance_factor.into(); + let _ = as Currency>::make_free_balance_be( + &user, balance, + ); + user +} + +#[allow(clippy::multiple_bound_locations)] +#[benchmarks(where T: pallet_balances::Config)] +mod benchmarks { + use super::*; + + // worst case for the end of an era. + #[benchmark] + fn on_era_end() -> Result<(), BenchmarkError> { + frame_system::Pallet::::set_block_number(0u32.into()); + + let mut era_reward_points = EraRewardPoints::default(); + era_reward_points.total = T::BackingPoints::get() * 1000; + + for i in 0..1000 { + let account_id = create_funded_user::("candidate", i, 100); + era_reward_points + .individual + .insert(account_id, T::BackingPoints::get()); + } + + >::insert(1u32, era_reward_points); + + #[block] + { + as OnEraEnd>::on_era_end(1u32); + } + + Ok(()) + } + + impl_benchmark_test_suite!( + ExternalValidatorsRewards, + crate::mock::new_test_ext(), + crate::mock::Test, + ); +} diff --git a/pallets/external-validators-rewards/src/lib.rs b/pallets/external-validators-rewards/src/lib.rs index 4888c206a..7f2162eff 100644 --- a/pallets/external-validators-rewards/src/lib.rs +++ b/pallets/external-validators-rewards/src/lib.rs @@ -25,20 +25,43 @@ mod mock; #[cfg(test)] mod tests; +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +pub mod weights; + pub use pallet::*; use { frame_support::traits::{Defensive, Get, ValidatorSet}, + parity_scale_codec::Encode, polkadot_primitives::ValidatorIndex, runtime_parachains::session_info, + snowbridge_core::ChannelId, + snowbridge_outbound_queue_merkle_tree::{merkle_proof, merkle_root, verify_proof, MerkleProof}, + sp_core::H256, + sp_runtime::traits::Hash, sp_staking::SessionIndex, sp_std::collections::btree_set::BTreeSet, + sp_std::vec, + sp_std::vec::Vec, + tp_bridge::{Command, DeliverMessage, Message, ValidateMessage}, }; +/// Utils needed to generate/verify merkle roots/proofs inside this pallet. +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct EraRewardsUtils { + pub rewards_merkle_root: H256, + pub leaves: Vec, + pub leaf_index: Option, + pub total_points: u128, +} + #[frame_support::pallet] pub mod pallet { + pub use crate::weights::WeightInfo; use { - frame_support::pallet_prelude::*, sp_std::collections::btree_map::BTreeMap, + super::*, frame_support::pallet_prelude::*, sp_std::collections::btree_map::BTreeMap, tp_traits::EraIndexProvider, }; @@ -64,6 +87,26 @@ pub mod pallet { /// The amount of era points given by dispute voting on a candidate. #[pallet::constant] type DisputeStatementPoints: Get; + + /// Provider to know how may tokens were inflated (added) in a specific era. + type EraInflationProvider: Get; + + /// Provider to retrieve the current block timestamp. + type TimestampProvider: Get; + + /// Hashing tool used to generate/verify merkle roots and proofs. + type Hashing: Hash; + + /// Validate a message that will be sent to Ethereum. + type ValidateMessage: ValidateMessage; + + /// Send a message to Ethereum. Needs to be validated first. + type OutboundQueue: DeliverMessage< + Ticket = <::ValidateMessage as ValidateMessage>::Ticket, + >; + + /// The weight information of this pallet. + type WeightInfo: WeightInfo; } #[pallet::pallet] @@ -104,6 +147,76 @@ pub mod pallet { } }) } + + // Helper function used to generate the following utils: + // - rewards_merkle_root: merkle root corresponding [(validatorId, rewardPoints)] + // for the era_index specified. + // - leaves: that were used to generate the previous merkle root. + // - leaf_index: index of the validatorId's leaf in the previous leaves array (if any). + // - total_points: number of total points of the era_index specified. + pub fn generate_era_rewards_utils( + era_index: EraIndex, + maybe_account_id_check: Option, + ) -> Option { + let era_rewards = RewardPointsForEra::::get(&era_index); + let total_points: u128 = era_rewards.total as u128; + let mut leaves = Vec::with_capacity(era_rewards.individual.len()); + let mut leaf_index = None; + + if let Some(account) = &maybe_account_id_check { + if !era_rewards.individual.contains_key(account) { + log::error!( + target: "ext_validators_rewards", + "AccountId {:?} not found for era {:?}!", + account, + era_index + ); + return None; + } + } + + for (index, (account_id, reward_points)) in era_rewards.individual.iter().enumerate() { + let encoded = (account_id, reward_points).encode(); + let hashed = ::Hashing::hash(&encoded); + leaves.push(hashed); + + if let Some(ref check_account_id) = maybe_account_id_check { + if account_id == check_account_id { + leaf_index = Some(index as u64); + } + } + } + + let rewards_merkle_root = + merkle_root::<::Hashing, _>(leaves.iter().cloned()); + + Some(EraRewardsUtils { + rewards_merkle_root, + leaves, + leaf_index, + total_points, + }) + } + + pub fn generate_rewards_merkle_proof( + account_id: T::AccountId, + era_index: EraIndex, + ) -> Option { + let utils = Self::generate_era_rewards_utils(era_index, Some(account_id))?; + utils.leaf_index.map(|index| { + merkle_proof::<::Hashing, _>(utils.leaves.into_iter(), index) + }) + } + + pub fn verify_rewards_merkle_proof(merkle_proof: MerkleProof) -> bool { + verify_proof::<::Hashing, _, _>( + &merkle_proof.root, + merkle_proof.proof, + merkle_proof.number_of_leaves, + merkle_proof.leaf_index, + merkle_proof.leaf, + ) + } } impl tp_traits::OnEraStart for Pallet { @@ -115,6 +228,53 @@ pub mod pallet { RewardPointsForEra::::remove(era_index_to_delete); } } + + impl tp_traits::OnEraEnd for Pallet { + fn on_era_end(era_index: EraIndex) { + if let Some(utils) = Self::generate_era_rewards_utils(era_index, None) { + let command = Command::ReportRewards { + timestamp: T::TimestampProvider::get(), + era_index, + total_points: utils.total_points, + tokens_inflated: T::EraInflationProvider::get(), + rewards_merkle_root: utils.rewards_merkle_root, + }; + + let channel_id: ChannelId = snowbridge_core::PRIMARY_GOVERNANCE_CHANNEL; + + let outbound_message = Message { + id: None, + channel_id, + command, + }; + + // Validate and deliver the message + match T::ValidateMessage::validate(&outbound_message) { + Ok((ticket, _fee)) => { + if let Err(err) = T::OutboundQueue::deliver(ticket) { + log::error!(target: "ext_validators_rewards", "OutboundQueue delivery of message failed. {err:?}"); + } + } + Err(err) => { + log::error!(target: "ext_validators_rewards", "OutboundQueue validation of message failed. {err:?}"); + } + } + + frame_system::Pallet::::register_extra_weight_unchecked( + T::WeightInfo::on_era_end(), + DispatchClass::Mandatory, + ); + } else { + // Unreachable, this should never happen as we are sending + // None as the second param in Self::generate_era_rewards_utils. + log::error!( + target: "ext_validators_rewards", + "Outbound message not sent for era {:?}!", + era_index + ); + } + } + } } /// Rewards validators for participating in parachains with era points in pallet-staking. diff --git a/pallets/external-validators-rewards/src/mock.rs b/pallets/external-validators-rewards/src/mock.rs index a80eca4fb..4c366f5fb 100644 --- a/pallets/external-validators-rewards/src/mock.rs +++ b/pallets/external-validators-rewards/src/mock.rs @@ -20,9 +20,10 @@ use { traits::{ConstU32, ConstU64}, }, pallet_balances::AccountData, + snowbridge_core::outbound::{SendError, SendMessageFeeProvider}, sp_core::H256, sp_runtime::{ - traits::{BlakeTwo256, IdentityLookup}, + traits::{BlakeTwo256, Get, IdentityLookup, Keccak256}, BuildStorage, }, }; @@ -109,11 +110,41 @@ impl pallet_timestamp::Config for Test { impl mock_data::Config for Test {} +pub struct MockOkOutboundQueue; +impl tp_bridge::DeliverMessage for MockOkOutboundQueue { + type Ticket = (); + + fn deliver(_: Self::Ticket) -> Result { + Ok(H256::zero()) + } +} + +impl SendMessageFeeProvider for MockOkOutboundQueue { + type Balance = u128; + + fn local_fee() -> Self::Balance { + 1 + } +} + +pub struct TimestampProvider; +impl Get for TimestampProvider { + fn get() -> u64 { + Timestamp::get() + } +} + impl pallet_external_validators_rewards::Config for Test { type EraIndexProvider = Mock; type HistoryDepth = ConstU32<10>; type BackingPoints = ConstU32<20>; type DisputeStatementPoints = ConstU32<20>; + type EraInflationProvider = (); + type TimestampProvider = TimestampProvider; + type Hashing = Keccak256; + type ValidateMessage = (); + type OutboundQueue = MockOkOutboundQueue; + type WeightInfo = (); } // Pallet to provide some mock data, used to test diff --git a/pallets/external-validators-rewards/src/weights.rs b/pallets/external-validators-rewards/src/weights.rs new file mode 100644 index 000000000..766adfcfb --- /dev/null +++ b/pallets/external-validators-rewards/src/weights.rs @@ -0,0 +1,116 @@ +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. + +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Tanssi is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see + + +//! Autogenerated weights for pallet_external_validators_rewards +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 43.0.0 +//! DATE: 2024-12-05, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `COV0768`, CPU: `AMD Ryzen 9 7950X 16-Core Processor` +//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("dancelight-dev"), DB CACHE: 1024 + +// Executed Command: +// target/release/tanssi-relay +// benchmark +// pallet +// --execution=wasm +// --wasm-execution=compiled +// --pallet +// pallet_external_validators_rewards +// --extrinsic +// * +// --chain=dancelight-dev +// --steps +// 50 +// --repeat +// 20 +// --template=benchmarking/frame-weight-pallet-template.hbs +// --json-file +// raw.json +// --output +// pallets/external-validators-rewards/src/pallet_external_validators_rewards.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_external_validators_rewards. +pub trait WeightInfo { + fn on_era_end() -> Weight; +} + +/// Weights for pallet_external_validators_rewards using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `ExternalValidatorsRewards::RewardPointsForEra` (r:1 w:0) + /// Proof: `ExternalValidatorsRewards::RewardPointsForEra` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `EthereumSystem::Channels` (r:1 w:0) + /// Proof: `EthereumSystem::Channels` (`max_values`: None, `max_size`: Some(76), added: 2551, mode: `MaxEncodedLen`) + /// Storage: `MessageQueue::BookStateFor` (r:1 w:1) + /// Proof: `MessageQueue::BookStateFor` (`max_values`: None, `max_size`: Some(136), added: 2611, mode: `MaxEncodedLen`) + /// Storage: `MessageQueue::ServiceHead` (r:1 w:1) + /// Proof: `MessageQueue::ServiceHead` (`max_values`: Some(1), `max_size`: Some(33), added: 528, mode: `MaxEncodedLen`) + /// Storage: UNKNOWN KEY `0x3a72656c61795f64697370617463685f71756575655f72656d61696e696e675f` (r:0 w:1) + /// Proof: UNKNOWN KEY `0x3a72656c61795f64697370617463685f71756575655f72656d61696e696e675f` (r:0 w:1) + /// Storage: `MessageQueue::Pages` (r:0 w:1) + /// Proof: `MessageQueue::Pages` (`max_values`: None, `max_size`: Some(32845), added: 35320, mode: `MaxEncodedLen`) + /// Storage: UNKNOWN KEY `0xf5207f03cfdce586301014700e2c2593fad157e461d71fd4c1f936839a5f1f3e` (r:0 w:1) + /// Proof: UNKNOWN KEY `0xf5207f03cfdce586301014700e2c2593fad157e461d71fd4c1f936839a5f1f3e` (r:0 w:1) + fn on_era_end() -> Weight { + // Proof Size summary in bytes: + // Measured: `36522` + // Estimated: `39987` + // Minimum execution time: 1_042_933_000 picoseconds. + Weight::from_parts(1_136_401_000, 39987) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// Storage: `ExternalValidatorsRewards::RewardPointsForEra` (r:1 w:0) + /// Proof: `ExternalValidatorsRewards::RewardPointsForEra` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `EthereumSystem::Channels` (r:1 w:0) + /// Proof: `EthereumSystem::Channels` (`max_values`: None, `max_size`: Some(76), added: 2551, mode: `MaxEncodedLen`) + /// Storage: `MessageQueue::BookStateFor` (r:1 w:1) + /// Proof: `MessageQueue::BookStateFor` (`max_values`: None, `max_size`: Some(136), added: 2611, mode: `MaxEncodedLen`) + /// Storage: `MessageQueue::ServiceHead` (r:1 w:1) + /// Proof: `MessageQueue::ServiceHead` (`max_values`: Some(1), `max_size`: Some(33), added: 528, mode: `MaxEncodedLen`) + /// Storage: UNKNOWN KEY `0x3a72656c61795f64697370617463685f71756575655f72656d61696e696e675f` (r:0 w:1) + /// Proof: UNKNOWN KEY `0x3a72656c61795f64697370617463685f71756575655f72656d61696e696e675f` (r:0 w:1) + /// Storage: `MessageQueue::Pages` (r:0 w:1) + /// Proof: `MessageQueue::Pages` (`max_values`: None, `max_size`: Some(32845), added: 35320, mode: `MaxEncodedLen`) + /// Storage: UNKNOWN KEY `0xf5207f03cfdce586301014700e2c2593fad157e461d71fd4c1f936839a5f1f3e` (r:0 w:1) + /// Proof: UNKNOWN KEY `0xf5207f03cfdce586301014700e2c2593fad157e461d71fd4c1f936839a5f1f3e` (r:0 w:1) + fn on_era_end() -> Weight { + // Proof Size summary in bytes: + // Measured: `36522` + // Estimated: `39987` + // Minimum execution time: 1_042_933_000 picoseconds. + Weight::from_parts(1_136_401_000, 39987) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } +} 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/pnpm-lock.yaml b/pnpm-lock.yaml index 8a000259e..64f93a6ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,7 +29,7 @@ importers: version: 5.6.0(@types/node@22.9.0)(chokidar@3.6.0)(encoding@0.1.13)(jsdom@23.2.0)(ts-node@10.9.2(@types/node@22.9.0)(typescript@5.6.3))(typescript@5.6.3)(zod@3.23.8) '@moonwall/util': specifier: 5.6.0 - version: 5.6.0(@types/node@22.9.0)(@vitest/ui@2.1.5)(chokidar@3.6.0)(encoding@0.1.13)(jsdom@23.2.0)(typescript@5.6.3)(zod@3.23.8) + version: 5.6.0(@types/node@22.9.0)(@vitest/ui@2.1.5(vitest@2.1.5))(chokidar@3.6.0)(encoding@0.1.13)(jsdom@23.2.0)(typescript@5.6.3)(zod@3.23.8) '@polkadot/api': specifier: 14.3.1 version: 14.3.1 @@ -127,46 +127,46 @@ importers: typescript-api: devDependencies: '@polkadot/api': - specifier: ^14.0.0 + specifier: ^14.3.1 version: 14.3.1 '@polkadot/api-augment': - specifier: ^14.0.0 + specifier: ^14.3.1 version: 14.3.1 '@polkadot/api-base': - specifier: ^14.0.0 + specifier: ^14.3.1 version: 14.3.1 '@polkadot/api-derive': - specifier: ^14.0.0 + specifier: ^14.3.1 version: 14.3.1 '@polkadot/rpc-augment': - specifier: ^14.0.0 + specifier: ^14.3.1 version: 14.3.1 '@polkadot/rpc-core': - specifier: ^14.0.0 + specifier: ^14.3.1 version: 14.3.1 '@polkadot/rpc-provider': - specifier: ^14.0.0 + specifier: ^14.3.1 version: 14.3.1 '@polkadot/typegen': - specifier: ^14.0.0 + specifier: ^14.3.1 version: 14.3.1 '@polkadot/types': - specifier: ^14.0.0 + specifier: ^14.3.1 version: 14.3.1 '@polkadot/types-augment': - specifier: ^14.0.0 + specifier: ^14.3.1 version: 14.3.1 '@polkadot/types-codec': - specifier: ^14.0.0 + specifier: ^14.3.1 version: 14.3.1 '@polkadot/types-create': - specifier: ^14.0.0 + specifier: ^14.3.1 version: 14.3.1 '@polkadot/types-known': - specifier: ^14.0.0 + specifier: ^14.3.1 version: 14.3.1 '@polkadot/types-support': - specifier: ^14.0.0 + specifier: ^14.3.1 version: 14.3.1 '@types/node': specifier: 22.5.0 @@ -1077,7 +1077,6 @@ packages: '@substrate/connect@0.8.11': resolution: {integrity: sha512-ofLs1PAO9AtDdPbdyTYj217Pe+lBfTLltdHDs3ds8no0BseoLeAGxpz1mHfi7zB4IxI3YyAiLjH6U8cw4pj4Nw==} - deprecated: versions below 1.x are no longer maintained '@substrate/light-client-extension-helpers@1.0.0': resolution: {integrity: sha512-TdKlni1mBBZptOaeVrKnusMg/UBpWUORNDv5fdCaJklP4RJiFOzBCrzC+CyVI5kQzsXBisZ+2pXm+rIjS38kHg==} @@ -2865,10 +2864,6 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - nock@13.5.5: - resolution: {integrity: sha512-XKYnqUrCwXC8DGG1xX4YH5yNIrlh9c065uaMZZHUoeUUINTOyt+x/G+ezYk0Ft6ExSREVIs+qBJDK503viTfFA==} - engines: {node: '>= 10.13'} - nock@13.5.6: resolution: {integrity: sha512-o2zOYiCpzRqSzPj0Zt/dQ/DqZeYoaQ7TUonc/xUPjCGl9WeHpNbxgVvOquXYAaJzI0M9BXV3HTzG0p8IUAbBTQ==} engines: {node: '>= 10.13'} @@ -4410,7 +4405,7 @@ snapshots: '@acala-network/chopsticks': 1.0.1(debug@4.3.7)(ts-node@10.9.2(@types/node@22.9.0)(typescript@5.6.3)) '@moonbeam-network/api-augment': 0.3200.3 '@moonwall/types': 5.6.0(chokidar@3.6.0)(encoding@0.1.13)(typescript@5.6.3)(zod@3.23.8) - '@moonwall/util': 5.6.0(@types/node@22.9.0)(@vitest/ui@2.1.5)(chokidar@3.6.0)(encoding@0.1.13)(jsdom@23.2.0)(typescript@5.6.3)(zod@3.23.8) + '@moonwall/util': 5.6.0(@types/node@22.9.0)(@vitest/ui@2.1.5(vitest@2.1.5))(chokidar@3.6.0)(encoding@0.1.13)(jsdom@23.2.0)(typescript@5.6.3)(zod@3.23.8) '@octokit/rest': 21.0.2 '@polkadot/api': 14.3.1 '@polkadot/api-derive': 14.3.1 @@ -4513,7 +4508,7 @@ snapshots: - utf-8-validate - zod - '@moonwall/util@5.6.0(@types/node@22.9.0)(@vitest/ui@2.1.5)(chokidar@3.6.0)(encoding@0.1.13)(jsdom@23.2.0)(typescript@5.6.3)(zod@3.23.8)': + '@moonwall/util@5.6.0(@types/node@22.9.0)(@vitest/ui@2.1.5(vitest@2.1.5))(chokidar@3.6.0)(encoding@0.1.13)(jsdom@23.2.0)(typescript@5.6.3)(zod@3.23.8)': dependencies: '@moonbeam-network/api-augment': 0.3200.3 '@moonwall/types': 5.6.0(chokidar@3.6.0)(encoding@0.1.13)(typescript@5.6.3)(zod@3.23.8) @@ -4958,7 +4953,7 @@ snapshots: '@polkadot/x-ws': 13.2.3 eventemitter3: 5.0.1 mock-socket: 9.3.1 - nock: 13.5.5 + nock: 13.5.6 tslib: 2.8.1 optionalDependencies: '@substrate/connect': 0.8.11 @@ -7510,14 +7505,6 @@ snapshots: neo-async@2.6.2: {} - nock@13.5.5: - dependencies: - debug: 4.3.7(supports-color@8.1.1) - json-stringify-safe: 5.0.1 - propagate: 2.0.1 - transitivePeerDependencies: - - supports-color - nock@13.5.6: dependencies: debug: 4.3.7(supports-color@8.1.1) diff --git a/primitives/bridge/src/custom_do_process_message.rs b/primitives/bridge/src/custom_do_process_message.rs index 788fb8587..0a15634d1 100644 --- a/primitives/bridge/src/custom_do_process_message.rs +++ b/primitives/bridge/src/custom_do_process_message.rs @@ -151,6 +151,8 @@ impl ConstantGasMeter { fn maximum_dispatch_gas_used_at_most(command: &Command) -> u64 { match command { Command::Test { .. } => 60_000, + // TODO: revisit gas cost + Command::ReportRewards { .. } => 60_000, } } } diff --git a/primitives/bridge/src/lib.rs b/primitives/bridge/src/lib.rs index cf80217f0..62835bb9e 100644 --- a/primitives/bridge/src/lib.rs +++ b/primitives/bridge/src/lib.rs @@ -27,7 +27,7 @@ use { relay_chain::{AccountId, Balance}, Assets, Location, SendResult, SendXcm, Xcm, XcmHash, }, - ethabi::Token, + ethabi::{Token, U256}, frame_support::{ ensure, pallet_prelude::{Decode, Encode, Get}, @@ -67,6 +67,18 @@ mod custom_send_message; pub enum Command { // TODO: add real commands here Test(Vec), + ReportRewards { + // block timestamp + timestamp: u64, + // index of the era we are sending info of + era_index: u32, + // total_points for the era + total_points: u128, + // new tokens inflated during the era + tokens_inflated: u128, + // merkle root of vec![(validatorId, rewardPoints)] + rewards_merkle_root: H256, + }, } impl Command { @@ -75,6 +87,7 @@ impl Command { match self { // Starting from 32 to keep compatibility with Snowbridge Command enum Command::Test { .. } => 32, + Command::ReportRewards { .. } => 33, } } @@ -84,6 +97,26 @@ impl Command { Command::Test(payload) => { ethabi::encode(&[Token::Tuple(vec![Token::Bytes(payload.clone())])]) } + Command::ReportRewards { + timestamp, + era_index, + total_points, + tokens_inflated, + rewards_merkle_root, + } => { + let timestamp_token = Token::Uint(U256::from(*timestamp)); + let era_index_token = Token::Uint(U256::from(*era_index)); + let total_points_token = Token::Uint(U256::from(*total_points)); + let tokens_inflated_token = Token::Uint(U256::from(*tokens_inflated)); + let rewards_mr_token = Token::FixedBytes(rewards_merkle_root.0.to_vec()); + ethabi::encode(&[Token::Tuple(vec![ + timestamp_token, + era_index_token, + total_points_token, + tokens_inflated_token, + rewards_mr_token, + ])]) + } } } } 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/Cargo.toml b/solo-chains/runtime/dancelight/Cargo.toml index 01dd9bf7e..868e9faf5 100644 --- a/solo-chains/runtime/dancelight/Cargo.toml +++ b/solo-chains/runtime/dancelight/Cargo.toml @@ -74,6 +74,7 @@ pallet-elections-phragmen = { workspace = true } pallet-external-validator-slashes = { workspace = true } pallet-external-validators = { workspace = true } pallet-external-validators-rewards = { workspace = true } +pallet-external-validators-rewards-runtime-api = { workspace = true } pallet-grandpa = { workspace = true } pallet-identity = { workspace = true } pallet-initializer = { workspace = true } @@ -220,6 +221,7 @@ std = [ "pallet-democracy/std", "pallet-elections-phragmen/std", "pallet-external-validator-slashes/std", + "pallet-external-validators-rewards-runtime-api/std", "pallet-external-validators-rewards/std", "pallet-external-validators/std", "pallet-grandpa/std", diff --git a/solo-chains/runtime/dancelight/src/lib.rs b/solo-chains/runtime/dancelight/src/lib.rs index ff39977df..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, @@ -78,6 +79,7 @@ use { scale_info::TypeInfo, serde::{Deserialize, Serialize}, snowbridge_core::ChannelId, + snowbridge_pallet_outbound_queue::MerkleProof, sp_core::{storage::well_known_keys as StorageWellKnownKeys, Get}, sp_genesis_builder::PresetId, sp_runtime::{ @@ -92,7 +94,7 @@ use { }, tp_traits::{ apply, derive_storage_traits, EraIndex, GetHostConfiguration, GetSessionContainerChains, - RegistrarHandler, RemoveParaIdsWithNoCredits, Slot, SlotFrequency, + ParaIdAssignmentHooks, RegistrarHandler, Slot, SlotFrequency, }, }; @@ -555,7 +557,7 @@ impl pallet_session::Config for Runtime { } pub struct FullIdentificationOf; -impl sp_runtime::traits::Convert> for FullIdentificationOf { +impl Convert> for FullIdentificationOf { fn convert(_: AccountId) -> Option<()> { Some(()) } @@ -600,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 @@ -1335,17 +1340,32 @@ impl pallet_external_validators::Config for Runtime { type UnixTime = Timestamp; type SessionsPerEra = SessionsPerEra; type OnEraStart = (ExternalValidatorSlashes, ExternalValidatorsRewards); - type OnEraEnd = (); + type OnEraEnd = ExternalValidatorsRewards; type WeightInfo = weights::pallet_external_validators::SubstrateWeight; #[cfg(feature = "runtime-benchmarks")] type Currency = Balances; } +pub struct TimestampProvider; +impl Get for TimestampProvider { + fn get() -> u64 { + Timestamp::get() + } +} + impl pallet_external_validators_rewards::Config for Runtime { type EraIndexProvider = ExternalValidators; type HistoryDepth = ConstU32<64>; type BackingPoints = ConstU32<20>; type DisputeStatementPoints = ConstU32<20>; + // TODO: add a proper way to retrieve the inflated tokens. + // Will likely be through InflationRewards. + type EraInflationProvider = (); + type TimestampProvider = TimestampProvider; + type Hashing = Keccak256; + type ValidateMessage = tp_bridge::MessageValidator; + type OutboundQueue = tp_bridge::CustomSendMessage; + type WeightInfo = weights::pallet_external_validators_rewards::SubstrateWeight; } impl pallet_external_validator_slashes::Config for Runtime { @@ -2078,6 +2098,7 @@ mod benches { [pallet_registrar, ContainerRegistrar] [pallet_collator_assignment, TanssiCollatorAssignment] [pallet_external_validators, ExternalValidators] + [pallet_external_validators_rewards, ExternalValidatorsRewards] [pallet_external_validator_slashes, ExternalValidatorSlashes] [pallet_invulnerables, TanssiInvulnerables] // XCM @@ -2696,6 +2717,19 @@ sp_api::impl_runtime_apis! { } } + impl pallet_external_validators_rewards_runtime_api::ExternalValidatorsRewardsApi for Runtime + where + EraIndex: parity_scale_codec::Codec, + { + fn generate_rewards_merkle_proof(account_id: AccountId, era_index: EraIndex) -> Option { + ExternalValidatorsRewards::generate_rewards_merkle_proof(account_id, era_index) + } + + fn verify_rewards_merkle_proof(merkle_proof: MerkleProof) -> bool { + ExternalValidatorsRewards::verify_rewards_merkle_proof(merkle_proof) + } + } + impl dp_consensus::TanssiAuthorityAssignmentApi for Runtime { /// Return the current authorities assigned to a given paraId fn para_id_authorities(para_id: ParaId) -> Option> { @@ -3154,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. @@ -3292,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; diff --git a/solo-chains/runtime/dancelight/src/tests/external_validators_tests.rs b/solo-chains/runtime/dancelight/src/tests/external_validators_tests.rs index 630fa2430..2316e4949 100644 --- a/solo-chains/runtime/dancelight/src/tests/external_validators_tests.rs +++ b/solo-chains/runtime/dancelight/src/tests/external_validators_tests.rs @@ -18,10 +18,15 @@ use { crate::{ - tests::common::*, ExternalValidators, MaxExternalValidators, SessionKeys, SessionsPerEra, + tests::common::*, ExternalValidators, ExternalValidatorsRewards, MaxExternalValidators, + RuntimeEvent, SessionKeys, SessionsPerEra, System, }, frame_support::{assert_ok, traits::fungible::Mutate}, pallet_external_validators::Forcing, + parity_scale_codec::Encode, + snowbridge_core::{Channel, PRIMARY_GOVERNANCE_CHANNEL}, + sp_core::H256, + sp_io::hashing::twox_64, std::{collections::HashMap, ops::RangeInclusive}, }; @@ -692,3 +697,145 @@ fn external_validators_manual_reward_points() { ); }); } + +#[test] +fn external_validators_rewards_sends_message_on_era_end() { + ExtBuilder::default() + .with_balances(vec![ + (AccountId::from(ALICE), 210_000 * UNIT), + (AccountId::from(BOB), 100_000 * UNIT), + ]) + .build() + .execute_with(|| { + // SessionsPerEra depends on fast-runtime feature, this test should pass regardless + let sessions_per_era = SessionsPerEra::get(); + + let channel_id = PRIMARY_GOVERNANCE_CHANNEL.encode(); + + // Insert PRIMARY_GOVERNANCE_CHANNEL channel id into storage. + let mut combined_channel_id_key = Vec::new(); + let hashed_key = twox_64(&channel_id); + + combined_channel_id_key.extend_from_slice(&hashed_key); + combined_channel_id_key.extend_from_slice(PRIMARY_GOVERNANCE_CHANNEL.as_ref()); + + let mut full_storage_key = Vec::new(); + full_storage_key.extend_from_slice(&frame_support::storage::storage_prefix( + b"EthereumSystem", + b"Channels", + )); + full_storage_key.extend_from_slice(&combined_channel_id_key); + + let channel = Channel { + agent_id: H256::default(), + para_id: 1000u32.into(), + }; + + frame_support::storage::unhashed::put(&full_storage_key, &channel); + + // This will call on_era_end for era 0 + run_to_session(sessions_per_era); + + let outbound_msg_queue_event = System::events() + .iter() + .filter(|r| match r.event { + RuntimeEvent::EthereumOutboundQueue( + snowbridge_pallet_outbound_queue::Event::MessageQueued { .. }, + ) => true, + _ => false, + }) + .count(); + + assert_eq!( + outbound_msg_queue_event, 1, + "MessageQueued event should be emitted" + ); + }); +} + +#[test] +fn external_validators_rewards_merkle_proofs() { + use {crate::ValidatorIndex, runtime_parachains::inclusion::RewardValidators}; + + ExtBuilder::default() + .with_balances(vec![ + (AccountId::from(ALICE), 210_000 * UNIT), + (AccountId::from(BOB), 100_000 * UNIT), + ]) + .build() + .execute_with(|| { + // SessionsPerEra depends on fast-runtime feature, this test should pass regardless + let sessions_per_era = SessionsPerEra::get(); + + assert_ok!(ExternalValidators::skip_external_validators( + root_origin(), + true + )); + + run_to_session(sessions_per_era); + let validators = Session::validators(); + + // Only whitelisted validators get selected + assert_eq!( + validators, + vec![AccountId::from(ALICE), AccountId::from(BOB)] + ); + + assert!( + pallet_external_validators_rewards::RewardPointsForEra::::iter().count() + == 0 + ); + + // Reward Alice and Bob in era 1 + crate::RewardValidators::reward_backing(vec![ValidatorIndex(0)]); + crate::RewardValidators::reward_backing(vec![ValidatorIndex(1)]); + + assert!( + pallet_external_validators_rewards::RewardPointsForEra::::iter().count() + == 1 + ); + + let alice_merkle_proof = ExternalValidatorsRewards::generate_rewards_merkle_proof( + AccountId::from(ALICE), + 1u32, + ); + let is_alice_merkle_proof_valid = + ExternalValidatorsRewards::verify_rewards_merkle_proof(alice_merkle_proof.unwrap()); + + let bob_merkle_proof = ExternalValidatorsRewards::generate_rewards_merkle_proof( + AccountId::from(BOB), + 1u32, + ); + let is_bob_merkle_proof_valid = + ExternalValidatorsRewards::verify_rewards_merkle_proof(bob_merkle_proof.unwrap()); + + assert!(is_alice_merkle_proof_valid); + assert!(is_bob_merkle_proof_valid); + + // Let's check invalid proofs now. + let charlie_merkle_proof = ExternalValidatorsRewards::generate_rewards_merkle_proof( + AccountId::from(CHARLIE), + 1u32, + ); + + let alice_invalid_merkle_proof = + ExternalValidatorsRewards::generate_rewards_merkle_proof( + AccountId::from(ALICE), + 0u32, + ); + + let bob_invalid_merkle_proof = ExternalValidatorsRewards::generate_rewards_merkle_proof( + AccountId::from(BOB), + 2u32, + ); + + // Charlie is not present in the validator set, so no merkle proof for him. + assert!(charlie_merkle_proof.is_none()); + + // Alice wasn't rewarded for era 0. + assert!(alice_invalid_merkle_proof.is_none()); + + // Proof for a future era should also be invalid. + assert!(bob_invalid_merkle_proof.is_none()); + }); +} diff --git a/solo-chains/runtime/dancelight/src/weights/mod.rs b/solo-chains/runtime/dancelight/src/weights/mod.rs index 9eec1d86d..1673a91c3 100644 --- a/solo-chains/runtime/dancelight/src/weights/mod.rs +++ b/solo-chains/runtime/dancelight/src/weights/mod.rs @@ -22,6 +22,7 @@ pub mod pallet_collator_assignment; pub mod pallet_conviction_voting; pub mod pallet_external_validator_slashes; pub mod pallet_external_validators; +pub mod pallet_external_validators_rewards; pub mod pallet_identity; pub mod pallet_invulnerables; pub mod pallet_message_queue; diff --git a/solo-chains/runtime/dancelight/src/weights/pallet_external_validators_rewards.rs b/solo-chains/runtime/dancelight/src/weights/pallet_external_validators_rewards.rs new file mode 100644 index 000000000..dcdf9b1e0 --- /dev/null +++ b/solo-chains/runtime/dancelight/src/weights/pallet_external_validators_rewards.rs @@ -0,0 +1,82 @@ +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. + +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Tanssi is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see + + +//! Autogenerated weights for pallet_external_validators_rewards +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 43.0.0 +//! DATE: 2024-12-05, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `COV0768`, CPU: `AMD Ryzen 9 7950X 16-Core Processor` +//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("dancelight-dev"), DB CACHE: 1024 + +// Executed Command: +// target/release/tanssi-relay +// benchmark +// pallet +// --execution=wasm +// --wasm-execution=compiled +// --pallet +// pallet_external_validators_rewards +// --extrinsic +// * +// --chain=dancelight-dev +// --steps +// 50 +// --repeat +// 20 +// --template=benchmarking/frame-weight-runtime-template.hbs +// --json-file +// raw.json +// --output +// solo-chains/runtime/dancelight/src/weights/pallet_external_validators_rewards.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weights for pallet_external_validators_rewards using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl pallet_external_validators_rewards::WeightInfo for SubstrateWeight { + /// Storage: `ExternalValidatorsRewards::RewardPointsForEra` (r:1 w:0) + /// Proof: `ExternalValidatorsRewards::RewardPointsForEra` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `EthereumSystem::Channels` (r:1 w:0) + /// Proof: `EthereumSystem::Channels` (`max_values`: None, `max_size`: Some(76), added: 2551, mode: `MaxEncodedLen`) + /// Storage: `MessageQueue::BookStateFor` (r:1 w:1) + /// Proof: `MessageQueue::BookStateFor` (`max_values`: None, `max_size`: Some(136), added: 2611, mode: `MaxEncodedLen`) + /// Storage: `MessageQueue::ServiceHead` (r:1 w:1) + /// Proof: `MessageQueue::ServiceHead` (`max_values`: Some(1), `max_size`: Some(33), added: 528, mode: `MaxEncodedLen`) + /// Storage: UNKNOWN KEY `0x3a72656c61795f64697370617463685f71756575655f72656d61696e696e675f` (r:0 w:1) + /// Proof: UNKNOWN KEY `0x3a72656c61795f64697370617463685f71756575655f72656d61696e696e675f` (r:0 w:1) + /// Storage: `MessageQueue::Pages` (r:0 w:1) + /// Proof: `MessageQueue::Pages` (`max_values`: None, `max_size`: Some(32845), added: 35320, mode: `MaxEncodedLen`) + /// Storage: UNKNOWN KEY `0xf5207f03cfdce586301014700e2c2593fad157e461d71fd4c1f936839a5f1f3e` (r:0 w:1) + /// Proof: UNKNOWN KEY `0xf5207f03cfdce586301014700e2c2593fad157e461d71fd4c1f936839a5f1f3e` (r:0 w:1) + fn on_era_end() -> Weight { + // Proof Size summary in bytes: + // Measured: `36522` + // Estimated: `39987` + // Minimum execution time: 1_005_863_000 picoseconds. + Weight::from_parts(1_017_752_000, 39987) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } +} \ No newline at end of file diff --git a/test/suites/dev-tanssi-relay/external-validators-rewards/test-rewards-runtime-api.ts b/test/suites/dev-tanssi-relay/external-validators-rewards/test-rewards-runtime-api.ts new file mode 100644 index 000000000..06014e305 --- /dev/null +++ b/test/suites/dev-tanssi-relay/external-validators-rewards/test-rewards-runtime-api.ts @@ -0,0 +1,44 @@ +import { beforeAll, customDevRpcRequest, describeSuite, expect } from "@moonwall/cli"; +import { ApiPromise, Keyring } from "@polkadot/api"; +import { jumpToSession } from "util/block"; + +describeSuite({ + id: "DTR0820", + title: "Starlight <> Ethereum - Rewards mapping", + foundationMethods: "dev", + testCases: ({ it, context }) => { + let polkadotJs: ApiPromise; + + beforeAll(async function () { + polkadotJs = context.polkadotJs(); + }); + + it({ + id: "T01", + title: "Should succeed calling runtimeApi for generating/validating merkle proofs", + test: async function () { + const keyring = new Keyring({ type: "sr25519" }); + const aliceStash = keyring.addFromUri("//Alice//stash"); + await context.createBlock(); + // Send RPC call to enable para inherent candidate generation + await customDevRpcRequest("mock_enableParaInherentCandidate", []); + // Since collators are not assigned until session 2, we need to go till session 2 to actually see heads being injected + await jumpToSession(context, 3); + await context.createBlock(); + + // We will only check alice's proof as she is the only one validating candidates + const aliceMerkleProof = await polkadotJs.call.externalValidatorsRewardsApi.generateRewardsMerkleProof( + aliceStash.address, + 1 + ); + expect(aliceMerkleProof.isEmpty).to.be.false; + + const isValidProofAlice = await polkadotJs.call.externalValidatorsRewardsApi.verifyRewardsMerkleProof( + aliceMerkleProof.toJSON() + ); + + expect(isValidProofAlice.toJSON()).to.be.eq(true); + }, + }); + }, +}); diff --git a/test/suites/zombie_tanssi_relay_eth_bridge/test_zombie_tanssi_relay_eth_bridge.ts b/test/suites/zombie_tanssi_relay_eth_bridge/test_zombie_tanssi_relay_eth_bridge.ts index 686aa4bf9..d747d93fd 100644 --- a/test/suites/zombie_tanssi_relay_eth_bridge/test_zombie_tanssi_relay_eth_bridge.ts +++ b/test/suites/zombie_tanssi_relay_eth_bridge/test_zombie_tanssi_relay_eth_bridge.ts @@ -196,7 +196,21 @@ describeSuite({ } // wait some time for the data to be relayed - await waitSessions(context, relayApi, 2, null, "Tanssi-relay"); + await waitSessions( + context, + relayApi, + 6, + async () => { + try { + const externalValidators = await relayApi.query.externalValidators.externalValidators(); + expect(externalValidators).to.not.deep.eq(externalValidatorsBefore); + } catch (error) { + return false; + } + return true; + }, + "Tanssi-relay" + ); const externalValidators = await relayApi.query.externalValidators.externalValidators(); expect(externalValidators).to.not.deep.eq(externalValidatorsBefore); diff --git a/typescript-api/package.json b/typescript-api/package.json index f03fa559a..cc2157974 100644 --- a/typescript-api/package.json +++ b/typescript-api/package.json @@ -95,20 +95,20 @@ "@polkadot/types-codec": "14.x" }, "devDependencies": { - "@polkadot/api": "^14.0.0", - "@polkadot/api-augment": "^14.0.0", - "@polkadot/api-base": "^14.0.0", - "@polkadot/api-derive": "^14.0.0", - "@polkadot/rpc-augment": "^14.0.0", - "@polkadot/rpc-core": "^14.0.0", - "@polkadot/rpc-provider": "^14.0.0", - "@polkadot/typegen": "^14.0.0", - "@polkadot/types": "^14.0.0", - "@polkadot/types-augment": "^14.0.0", - "@polkadot/types-codec": "^14.0.0", - "@polkadot/types-create": "^14.0.0", - "@polkadot/types-known": "^14.0.0", - "@polkadot/types-support": "^14.0.0", + "@polkadot/api": "^14.3.1", + "@polkadot/api-augment": "^14.3.1", + "@polkadot/api-base": "^14.3.1", + "@polkadot/api-derive": "^14.3.1", + "@polkadot/rpc-augment": "^14.3.1", + "@polkadot/rpc-core": "^14.3.1", + "@polkadot/rpc-provider": "^14.3.1", + "@polkadot/typegen": "^14.3.1", + "@polkadot/types": "^14.3.1", + "@polkadot/types-augment": "^14.3.1", + "@polkadot/types-codec": "^14.3.1", + "@polkadot/types-create": "^14.3.1", + "@polkadot/types-known": "^14.3.1", + "@polkadot/types-support": "^14.3.1", "@types/node": "22.5.0", "chalk": "^5.3.0", "prettier": "^3.3.3",