diff --git a/Cargo.lock b/Cargo.lock index f55fdba7f..7e7a17917 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5051,9 +5051,11 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "lazy_static", "pallet-balances", "pallet-timestamp", "parity-scale-codec", + "parking_lot 0.12.1", "scale-info", "sp-core", "sp-io", diff --git a/pallets/ddc-clusters/src/lib.rs b/pallets/ddc-clusters/src/lib.rs index 6fa286e99..1928dd620 100644 --- a/pallets/ddc-clusters/src/lib.rs +++ b/pallets/ddc-clusters/src/lib.rs @@ -25,8 +25,8 @@ use crate::{ node_provider_auth::{NodeProviderAuthContract, NodeProviderAuthContractError}, }; use ddc_primitives::{ - ClusterFeesParams, ClusterGovParams, ClusterId, ClusterParams, ClusterPricingParams, - NodePubKey, NodeType, + ClusterBondingParams, ClusterFeesParams, ClusterGovParams, ClusterId, ClusterParams, + ClusterPricingParams, NodePubKey, NodeType, }; use ddc_traits::{ cluster::{ClusterCreator, ClusterVisitor, ClusterVisitorError}, @@ -52,6 +52,7 @@ pub type BalanceOf = #[frame_support::pallet] pub mod pallet { use super::*; + use ddc_traits::cluster::{ClusterManager, ClusterManagerError}; use pallet_contracts::chain_extension::UncheckedFrom; #[pallet::pallet] @@ -83,12 +84,12 @@ pub mod pallet { ClusterDoesNotExist, ClusterParamsExceedsLimit, AttemptToAddNonExistentNode, + AttemptToAddAlreadyAssignedNode, AttemptToRemoveNonExistentNode, - NodeIsAlreadyAssigned, - NodeIsNotAssigned, + AttemptToRemoveNotAssignedNode, OnlyClusterManager, NodeIsNotAuthorized, - NodeHasNoStake, + NodeHasNoActivatedStake, NodeStakeIsInvalid, /// Cluster candidate should not plan to chill. NodeChillingIsProhibited, @@ -150,28 +151,30 @@ pub mod pallet { let caller_id = ensure_signed(origin)?; let cluster = Clusters::::try_get(cluster_id).map_err(|_| Error::::ClusterDoesNotExist)?; - ensure!(cluster.manager_id == caller_id, Error::::OnlyClusterManager); - // Node with this node with this public key exists. - let mut node = T::NodeRepository::get(node_pub_key.clone()) - .map_err(|_| Error::::AttemptToAddNonExistentNode)?; - ensure!(node.get_cluster_id().is_none(), Error::::NodeIsAlreadyAssigned); + ensure!(cluster.manager_id == caller_id, Error::::OnlyClusterManager); // Sufficient funds are locked at the DDC Staking module. - let has_stake = T::StakingVisitor::node_has_stake(&node_pub_key, &cluster_id) - .map_err(Into::>::into)?; - ensure!(has_stake, Error::::NodeHasNoStake); + let has_activated_stake = + T::StakingVisitor::has_activated_stake(&node_pub_key, &cluster_id) + .map_err(Into::>::into)?; + ensure!(has_activated_stake, Error::::NodeHasNoActivatedStake); // Candidate is not planning to pause operations any time soon. - let is_chilling = T::StakingVisitor::node_is_chilling(&node_pub_key) + let has_chilling_attempt = T::StakingVisitor::has_chilling_attempt(&node_pub_key) .map_err(Into::>::into)?; - ensure!(!is_chilling, Error::::NodeChillingIsProhibited); + ensure!(!has_chilling_attempt, Error::::NodeChillingIsProhibited); // Cluster extension smart contract allows joining. let auth_contract = NodeProviderAuthContract::::new( cluster.props.node_provider_auth_contract, caller_id, ); + + // Node with this node with this public key exists. + let node = T::NodeRepository::get(node_pub_key.clone()) + .map_err(|_| Error::::AttemptToAddNonExistentNode)?; + let is_authorized = auth_contract .is_authorized( node.get_provider_id().to_owned(), @@ -182,9 +185,8 @@ pub mod pallet { ensure!(is_authorized, Error::::NodeIsNotAuthorized); // Add node to the cluster. - node.set_cluster_id(Some(cluster_id)); - T::NodeRepository::update(node).map_err(|_| Error::::AttemptToAddNonExistentNode)?; - ClustersNodes::::insert(cluster_id, node_pub_key.clone(), true); + >::add_node(&cluster_id, &node_pub_key) + .map_err(Into::>::into)?; Self::deposit_event(Event::::ClusterNodeAdded { cluster_id, node_pub_key }); Ok(()) @@ -199,14 +201,12 @@ pub mod pallet { let caller_id = ensure_signed(origin)?; let cluster = Clusters::::try_get(cluster_id).map_err(|_| Error::::ClusterDoesNotExist)?; + ensure!(cluster.manager_id == caller_id, Error::::OnlyClusterManager); - let mut node = T::NodeRepository::get(node_pub_key.clone()) - .map_err(|_| Error::::AttemptToRemoveNonExistentNode)?; - ensure!(node.get_cluster_id() == &Some(cluster_id), Error::::NodeIsNotAssigned); - node.set_cluster_id(None); - T::NodeRepository::update(node) - .map_err(|_| Error::::AttemptToRemoveNonExistentNode)?; - ClustersNodes::::remove(cluster_id, node_pub_key.clone()); + + // Remove node from the cluster. + >::remove_node(&cluster_id, &node_pub_key) + .map_err(Into::>::into)?; Self::deposit_event(Event::::ClusterNodeRemoved { cluster_id, node_pub_key }); Ok(()) @@ -269,10 +269,6 @@ pub mod pallet { } impl ClusterVisitor for Pallet { - fn cluster_has_node(cluster_id: &ClusterId, node_pub_key: &NodePubKey) -> bool { - ClustersNodes::::get(cluster_id, node_pub_key).is_some() - } - fn ensure_cluster(cluster_id: &ClusterId) -> Result<(), ClusterVisitorError> { Clusters::::get(cluster_id) .map(|_| ()) @@ -349,6 +345,69 @@ pub mod pallet { NodeType::CDN => Ok(cluster_gov_params.cdn_unbonding_delay), } } + + fn get_bonding_params( + cluster_id: &ClusterId, + ) -> Result, ClusterVisitorError> { + let cluster_gov_params = ClustersGovParams::::try_get(cluster_id) + .map_err(|_| ClusterVisitorError::ClusterGovParamsNotSet)?; + Ok(ClusterBondingParams { + cdn_bond_size: cluster_gov_params.cdn_bond_size.saturated_into::(), + cdn_chill_delay: cluster_gov_params.cdn_chill_delay, + cdn_unbonding_delay: cluster_gov_params.cdn_unbonding_delay, + storage_bond_size: cluster_gov_params.storage_bond_size.saturated_into::(), + storage_chill_delay: cluster_gov_params.storage_chill_delay, + storage_unbonding_delay: cluster_gov_params.storage_unbonding_delay, + }) + } + } + + impl ClusterManager for Pallet { + fn contains_node(cluster_id: &ClusterId, node_pub_key: &NodePubKey) -> bool { + ClustersNodes::::get(cluster_id, node_pub_key).is_some() + } + + fn add_node( + cluster_id: &ClusterId, + node_pub_key: &NodePubKey, + ) -> Result<(), ClusterManagerError> { + let mut node = T::NodeRepository::get(node_pub_key.clone()) + .map_err(|_| ClusterManagerError::AttemptToAddNonExistentNode)?; + + ensure!( + node.get_cluster_id().is_none(), + ClusterManagerError::AttemptToAddAlreadyAssignedNode + ); + + node.set_cluster_id(Some(*cluster_id)); + T::NodeRepository::update(node) + .map_err(|_| ClusterManagerError::AttemptToAddNonExistentNode)?; + + ClustersNodes::::insert(cluster_id, node_pub_key.clone(), true); + + Ok(()) + } + + fn remove_node( + cluster_id: &ClusterId, + node_pub_key: &NodePubKey, + ) -> Result<(), ClusterManagerError> { + let mut node = T::NodeRepository::get(node_pub_key.clone()) + .map_err(|_| ClusterManagerError::AttemptToRemoveNonExistentNode)?; + + ensure!( + node.get_cluster_id() == &Some(*cluster_id), + ClusterManagerError::AttemptToRemoveNotAssignedNode + ); + + node.set_cluster_id(None); + T::NodeRepository::update(node) + .map_err(|_| ClusterManagerError::AttemptToRemoveNonExistentNode)?; + + ClustersNodes::::remove(cluster_id, node_pub_key.clone()); + + Ok(()) + } } impl ClusterCreator> for Pallet @@ -375,7 +434,7 @@ pub mod pallet { impl From for Error { fn from(error: StakingVisitorError) -> Self { match error { - StakingVisitorError::NodeStakeDoesNotExist => Error::::NodeHasNoStake, + StakingVisitorError::NodeStakeDoesNotExist => Error::::NodeHasNoActivatedStake, StakingVisitorError::NodeStakeIsInBadState => Error::::NodeStakeIsInvalid, } } @@ -389,4 +448,19 @@ pub mod pallet { } } } + + impl From for Error { + fn from(error: ClusterManagerError) -> Self { + match error { + ClusterManagerError::AttemptToRemoveNotAssignedNode => + Error::::AttemptToRemoveNotAssignedNode, + ClusterManagerError::AttemptToRemoveNonExistentNode => + Error::::AttemptToRemoveNonExistentNode, + ClusterManagerError::AttemptToAddNonExistentNode => + Error::::AttemptToAddNonExistentNode, + ClusterManagerError::AttemptToAddAlreadyAssignedNode => + Error::::AttemptToAddAlreadyAssignedNode, + } + } + } } diff --git a/pallets/ddc-clusters/src/mock.rs b/pallets/ddc-clusters/src/mock.rs index b6c2e4fd2..7a9ae78cb 100644 --- a/pallets/ddc-clusters/src/mock.rs +++ b/pallets/ddc-clusters/src/mock.rs @@ -181,6 +181,7 @@ impl pallet_timestamp::Config for Test { impl pallet_ddc_nodes::Config for Test { type RuntimeEvent = RuntimeEvent; + type StakingVisitor = TestStakingVisitor; } impl crate::pallet::Config for Test { @@ -194,13 +195,16 @@ pub(crate) type DdcStakingCall = crate::Call; pub(crate) type TestRuntimeCall = ::RuntimeCall; pub struct TestStakingVisitor; impl StakingVisitor for TestStakingVisitor { - fn node_has_stake( + fn has_activated_stake( _node_pub_key: &NodePubKey, _cluster_id: &ClusterId, ) -> Result { Ok(true) } - fn node_is_chilling(_node_pub_key: &NodePubKey) -> Result { + fn has_stake(_node_pub_key: &NodePubKey) -> bool { + true + } + fn has_chilling_attempt(_node_pub_key: &NodePubKey) -> Result { Ok(false) } } diff --git a/pallets/ddc-clusters/src/tests.rs b/pallets/ddc-clusters/src/tests.rs index 3a9ccb556..c3c9e4872 100644 --- a/pallets/ddc-clusters/src/tests.rs +++ b/pallets/ddc-clusters/src/tests.rs @@ -183,7 +183,7 @@ fn add_and_delete_node_works() { ClusterId::from([1; 20]), NodePubKey::CDNPubKey(AccountId::from([4; 32])), ), - Error::::NodeIsAlreadyAssigned + Error::::AttemptToAddAlreadyAssignedNode ); // Checking that event was emitted @@ -218,7 +218,7 @@ fn add_and_delete_node_works() { ClusterId::from([1; 20]), NodePubKey::CDNPubKey(AccountId::from([4; 32])), ), - Error::::NodeIsNotAssigned + Error::::AttemptToRemoveNotAssignedNode ); pub const CTOR_SELECTOR: [u8; 4] = hex!("9bae9d5e"); diff --git a/pallets/ddc-customers/src/mock.rs b/pallets/ddc-customers/src/mock.rs index e4d05e8e7..11af9b667 100644 --- a/pallets/ddc-customers/src/mock.rs +++ b/pallets/ddc-customers/src/mock.rs @@ -2,10 +2,13 @@ use crate::{self as pallet_ddc_customers, *}; use ddc_primitives::{ - ClusterFeesParams, ClusterGovParams, ClusterId, ClusterParams, ClusterPricingParams, - NodePubKey, NodeType, + ClusterBondingParams, ClusterFeesParams, ClusterGovParams, ClusterId, ClusterParams, + ClusterPricingParams, NodePubKey, NodeType, }; -use ddc_traits::cluster::{ClusterCreator, ClusterVisitor, ClusterVisitorError}; +use ddc_traits::cluster::{ + ClusterCreator, ClusterManager, ClusterManagerError, ClusterVisitor, ClusterVisitorError, +}; + use frame_support::{ construct_runtime, parameter_types, traits::{ConstU32, ConstU64, Everything}, @@ -109,9 +112,6 @@ impl crate::pallet::Config for Test { pub struct TestClusterVisitor; impl ClusterVisitor for TestClusterVisitor { - fn cluster_has_node(_cluster_id: &ClusterId, _node_pub_key: &NodePubKey) -> bool { - true - } fn ensure_cluster(_cluster_id: &ClusterId) -> Result<(), ClusterVisitorError> { Ok(()) } @@ -158,6 +158,64 @@ impl ClusterVisitor for TestClusterVisitor { ) -> Result { Err(ClusterVisitorError::ClusterDoesNotExist) } + + fn get_bonding_params( + cluster_id: &ClusterId, + ) -> Result, ClusterVisitorError> { + Ok(ClusterBondingParams { + cdn_bond_size: >::get_bond_size( + cluster_id, + NodeType::CDN, + ) + .unwrap_or_default(), + cdn_chill_delay: >::get_chill_delay( + cluster_id, + NodeType::CDN, + ) + .unwrap_or_default(), + cdn_unbonding_delay: >::get_unbonding_delay( + cluster_id, + NodeType::CDN, + ) + .unwrap_or_default(), + storage_bond_size: >::get_bond_size( + cluster_id, + NodeType::Storage, + ) + .unwrap_or_default(), + storage_chill_delay: >::get_chill_delay( + cluster_id, + NodeType::Storage, + ) + .unwrap_or_default(), + storage_unbonding_delay: + >::get_unbonding_delay( + cluster_id, + NodeType::Storage, + ) + .unwrap_or_default(), + }) + } +} + +pub struct TestClusterManager; +impl ClusterManager for TestClusterManager { + fn contains_node(_cluster_id: &ClusterId, _node_pub_key: &NodePubKey) -> bool { + true + } + fn add_node( + _cluster_id: &ClusterId, + _node_pub_key: &NodePubKey, + ) -> Result<(), ClusterManagerError> { + Ok(()) + } + + fn remove_node( + _cluster_id: &ClusterId, + _node_pub_key: &NodePubKey, + ) -> Result<(), ClusterManagerError> { + Ok(()) + } } pub struct TestClusterCreator; diff --git a/pallets/ddc-nodes/src/lib.rs b/pallets/ddc-nodes/src/lib.rs index f18d724b7..4cea30ea7 100644 --- a/pallets/ddc-nodes/src/lib.rs +++ b/pallets/ddc-nodes/src/lib.rs @@ -20,7 +20,11 @@ pub(crate) mod mock; mod tests; use ddc_primitives::{CDNNodePubKey, ClusterId, NodePubKey, StorageNodePubKey}; -use ddc_traits::node::{NodeVisitor, NodeVisitorError}; +use ddc_traits::{ + node::{NodeVisitor, NodeVisitorError}, + staking::StakingVisitor, +}; + use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; use sp_std::prelude::*; @@ -48,6 +52,7 @@ pub mod pallet { #[pallet::config] pub trait Config: frame_system::Config { type RuntimeEvent: From> + IsType<::RuntimeEvent>; + type StakingVisitor: StakingVisitor; } #[pallet::event] @@ -68,6 +73,7 @@ pub mod pallet { OnlyNodeProvider, NodeIsAssignedToCluster, HostLenExceedsLimit, + NodeHasDanglingStake, } #[pallet::storage] @@ -101,6 +107,8 @@ pub mod pallet { let node = Self::get(node_pub_key.clone()).map_err(Into::>::into)?; ensure!(node.get_provider_id() == &caller_id, Error::::OnlyNodeProvider); ensure!(node.get_cluster_id().is_none(), Error::::NodeIsAssignedToCluster); + let has_stake = T::StakingVisitor::has_stake(&node_pub_key); + ensure!(!has_stake, Error::::NodeHasDanglingStake); Self::delete(node_pub_key.clone()).map_err(Into::>::into)?; Self::deposit_event(Event::::NodeDeleted { node_pub_key }); Ok(()) @@ -220,5 +228,9 @@ pub mod pallet { Self::get(node_pub_key.clone()).map_err(|_| NodeVisitorError::NodeDoesNotExist)?; Ok(*node.get_cluster_id()) } + + fn exists(node_pub_key: &NodePubKey) -> bool { + Self::get(node_pub_key.clone()).is_ok() + } } } diff --git a/pallets/ddc-nodes/src/mock.rs b/pallets/ddc-nodes/src/mock.rs index 18377ac58..30cf49869 100644 --- a/pallets/ddc-nodes/src/mock.rs +++ b/pallets/ddc-nodes/src/mock.rs @@ -3,6 +3,7 @@ #![allow(dead_code)] use crate::{self as pallet_ddc_nodes, *}; +use ddc_traits::staking::{StakingVisitor, StakingVisitorError}; use frame_support::{ construct_runtime, parameter_types, traits::{ConstU32, ConstU64, Everything}, @@ -90,6 +91,23 @@ impl pallet_timestamp::Config for Test { impl crate::pallet::Config for Test { type RuntimeEvent = RuntimeEvent; + type StakingVisitor = TestStakingVisitor; +} + +pub struct TestStakingVisitor; +impl StakingVisitor for TestStakingVisitor { + fn has_activated_stake( + _node_pub_key: &NodePubKey, + _cluster_id: &ClusterId, + ) -> Result { + Ok(false) + } + fn has_stake(_node_pub_key: &NodePubKey) -> bool { + false + } + fn has_chilling_attempt(_node_pub_key: &NodePubKey) -> Result { + Ok(false) + } } pub(crate) type TestRuntimeCall = ::RuntimeCall; diff --git a/pallets/ddc-payouts/src/mock.rs b/pallets/ddc-payouts/src/mock.rs index 74abd95f2..7a4e79cf6 100644 --- a/pallets/ddc-payouts/src/mock.rs +++ b/pallets/ddc-payouts/src/mock.rs @@ -3,7 +3,7 @@ #![allow(dead_code)] use crate::{self as pallet_ddc_payouts, *}; -use ddc_primitives::{ClusterFeesParams, ClusterPricingParams, NodePubKey, NodeType}; +use ddc_primitives::{ClusterBondingParams, ClusterFeesParams, ClusterPricingParams, NodeType}; use ddc_traits::{ cluster::{ClusterVisitor, ClusterVisitorError}, customer::CustomerCharger, @@ -261,9 +261,6 @@ pub fn get_fees(cluster_id: &ClusterId) -> Result ClusterVisitor for TestClusterVisitor { - fn cluster_has_node(_cluster_id: &ClusterId, _node_pub_key: &NodePubKey) -> bool { - true - } fn ensure_cluster(_cluster_id: &ClusterId) -> Result<(), ClusterVisitorError> { Ok(()) } @@ -306,6 +303,12 @@ impl ClusterVisitor for TestClusterVisitor { let reserve_account = RESERVE_ACCOUNT_ID.to_ne_bytes(); Ok(T::AccountId::decode(&mut &reserve_account[..]).unwrap()) } + + fn get_bonding_params( + _cluster_id: &ClusterId, + ) -> Result, ClusterVisitorError> { + unimplemented!() + } } pub(crate) type TestRuntimeCall = ::RuntimeCall; diff --git a/pallets/ddc-staking/Cargo.toml b/pallets/ddc-staking/Cargo.toml index 67dddf18a..fb5f83367 100644 --- a/pallets/ddc-staking/Cargo.toml +++ b/pallets/ddc-staking/Cargo.toml @@ -21,6 +21,8 @@ pallet-timestamp = { git = "https://github.com/paritytech/substrate.git", branch sp-core = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.30" } sp-tracing = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.30" } substrate-test-utils = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.30" } +lazy_static = "1.4.0" +parking_lot = "0.12.1" [features] default = ["std"] diff --git a/pallets/ddc-staking/src/lib.rs b/pallets/ddc-staking/src/lib.rs index c7efa74c7..3587f34df 100644 --- a/pallets/ddc-staking/src/lib.rs +++ b/pallets/ddc-staking/src/lib.rs @@ -141,6 +141,8 @@ impl< #[frame_support::pallet] pub mod pallet { + use ddc_traits::{cluster::ClusterManager, node::NodeVisitorError}; + use super::*; #[pallet::pallet] @@ -159,6 +161,8 @@ pub mod pallet { type ClusterVisitor: ClusterVisitor; + type ClusterManager: ClusterManager; + type NodeVisitor: NodeVisitor; } @@ -195,6 +199,16 @@ pub mod pallet { #[pallet::getter(fn providers)] pub type Providers = StorageMap<_, Twox64Concat, T::AccountId, NodePubKey>; + /// Map of Storage node provider stash accounts that aim to leave a cluster + #[pallet::storage] + #[pallet::getter(fn leaving_storages)] + pub type LeavingStorages = StorageMap<_, Twox64Concat, T::AccountId, ClusterId>; + + // Map of CDN node provider stash accounts that aim to leave a cluster + #[pallet::storage] + #[pallet::getter(fn leaving_cdns)] + pub type LeavingCDNs = StorageMap<_, Twox64Concat, T::AccountId, ClusterId>; + #[pallet::genesis_config] pub struct GenesisConfig { #[allow(clippy::type_complexity)] @@ -273,6 +287,12 @@ pub mod pallet { /// An account that started participating as either a storage network or CDN participant. /// \[stash\] Activated(T::AccountId), + /// An account that started unbonding tokens below the minimum value set for the cluster + /// his CDN or Storage node is assigned to \[stash\] + LeaveSoon(T::AccountId), + /// An account that unbonded tokens below the minimum value set for the cluster his + /// CDN or Storage node was assigned to \[stash\] + Left(T::AccountId), } #[pallet::error] @@ -302,6 +322,8 @@ pub mod pallet { NotNodeController, /// No stake found associated with the provided node. NodeHasNoStake, + /// No cluster found + NoCluster, /// No cluster governance params found for cluster NoClusterGovParams, /// Conditions for fast chill are not met, try the regular `chill` from @@ -314,6 +336,11 @@ pub mod pallet { ArithmeticOverflow, /// Arithmetic underflow occurred ArithmeticUnderflow, + /// Attempt to associate stake with non-existing node + NodeIsNotFound, + /// Action is prohibited for a node provider stash account that is in the process of + /// leaving a cluster + NodeIsLeaving, } #[pallet::call] @@ -355,6 +382,9 @@ pub mod pallet { Err(Error::::AlreadyPaired)? } + // Checks that the node is registered in the network + ensure!(T::NodeVisitor::exists(&node), Error::::NodeIsNotFound); + frame_system::Pallet::::inc_consumers(&stash).map_err(|_| Error::::BadState)?; Nodes::::insert(&node, &stash); @@ -404,6 +434,7 @@ pub mod pallet { ) -> DispatchResult { let controller = ensure_signed(origin)?; let mut ledger = Self::ledger(&controller).ok_or(Error::::NotController)?; + ensure!( ledger.unlocking.len() < MaxUnlockingChunks::get() as usize, Error::::NoMoreChunks, @@ -432,6 +463,8 @@ pub mod pallet { .map_err(Into::>::into)?; bond_size.saturated_into::>() } else { + // If node is not assigned to a cluster or node is chilling, allow to unbond + // any available amount. Zero::zero() }; @@ -439,36 +472,50 @@ pub mod pallet { // cluster. If a user runs into this error, they should chill first. ensure!(ledger.active >= min_active_bond, Error::::InsufficientBond); - let unbonding_delay_in_blocks = if let Some(cluster_id) = Self::cdns(&ledger.stash) - { - T::ClusterVisitor::get_unbonding_delay(&cluster_id, NodeType::CDN) - .map_err(Into::>::into)? - } else if let Some(cluster_id) = Self::storages(&ledger.stash) { - T::ClusterVisitor::get_unbonding_delay(&cluster_id, NodeType::Storage) - .map_err(Into::>::into)? - } else { - let node_pub_key = - >::get(&ledger.stash).ok_or(Error::::BadState)?; + let node_pub_key = + >::get(&ledger.stash).ok_or(Error::::BadState)?; + + let unbonding_delay = if T::NodeVisitor::exists(&node_pub_key) { + let node_cluster_id = T::NodeVisitor::get_cluster_id(&node_pub_key) + .map_err(Into::>::into)?; + + if let Some(cluster_id) = node_cluster_id { + let bonding_params = T::ClusterVisitor::get_bonding_params(&cluster_id) + .map_err(Into::>::into)?; + + let min_bond_size = match node_pub_key { + NodePubKey::CDNPubKey(_) => bonding_params.cdn_bond_size, + NodePubKey::StoragePubKey(_) => bonding_params.storage_bond_size, + }; + + // If provider is trying to unbond after chilling and aims to leave the + // cluster eventually, we keep its stake till the end of unbonding period. + if ledger.active < min_bond_size.saturated_into::>() { + match node_pub_key { + NodePubKey::CDNPubKey(_) => + LeavingCDNs::::insert(ledger.stash.clone(), cluster_id), + NodePubKey::StoragePubKey(_) => + LeavingStorages::::insert(ledger.stash.clone(), cluster_id), + }; + + Self::deposit_event(Event::::LeaveSoon(ledger.stash.clone())); + }; - if let Ok(Some(cluster_id)) = T::NodeVisitor::get_cluster_id(&node_pub_key) { match node_pub_key { - NodePubKey::CDNPubKey(_) => - T::ClusterVisitor::get_unbonding_delay(&cluster_id, NodeType::CDN) - .map_err(Into::>::into)?, - NodePubKey::StoragePubKey(_) => T::ClusterVisitor::get_unbonding_delay( - &cluster_id, - NodeType::Storage, - ) - .map_err(Into::>::into)?, + NodePubKey::CDNPubKey(_) => bonding_params.cdn_unbonding_delay, + NodePubKey::StoragePubKey(_) => bonding_params.storage_unbonding_delay, } } else { // If node is not a member of any cluster, allow immediate unbonding. T::BlockNumber::from(0u32) } + } else { + // If node was deleted, allow immediate unbonding. + T::BlockNumber::from(0u32) }; // block number + configuration -> no overflow - let block = >::block_number() + unbonding_delay_in_blocks; + let block = >::block_number() + unbonding_delay; if let Some(chunk) = ledger.unlocking.last_mut().filter(|chunk| chunk.block == block) { @@ -505,6 +552,7 @@ pub mod pallet { let controller = ensure_signed(origin)?; let mut ledger = Self::ledger(&controller).ok_or(Error::::NotController)?; let (stash, old_total) = (ledger.stash.clone(), ledger.total); + let node_pub_key = >::get(stash.clone()).ok_or(Error::::BadState)?; ledger = ledger.consolidate_unlocked(>::block_number()); @@ -526,7 +574,22 @@ pub mod pallet { // Already checked that this won't overflow by entry condition. let value = old_total.checked_sub(&ledger.total).ok_or(Error::::ArithmeticUnderflow)?; - Self::deposit_event(Event::::Withdrawn(stash, value)); + Self::deposit_event(Event::::Withdrawn(stash.clone(), value)); + + // If provider aimed to leave the cluster and the unbonding period ends, remove + // the node from the cluster + if let Some(cluster_id) = + >::get(&stash).or_else(|| >::get(&stash)) + { + // Cluster manager could remove the node from cluster by this moment already, so + // it is ok to ignore result. + let _ = T::ClusterManager::remove_node(&cluster_id, &node_pub_key); + + >::remove(&stash); + >::remove(&stash); + + Self::deposit_event(Event::::Left(stash)); + } } Ok(()) @@ -573,6 +636,10 @@ pub mod pallet { // Cancel previous "chill" attempts Self::reset_chilling(&controller); return Ok(()) + } else { + // Can't participate in new CDN network if provider hasn't left the previous cluster + // yet + ensure!(!LeavingCDNs::::contains_key(stash), Error::::NodeIsLeaving); } Self::do_add_cdn(stash, cluster_id); @@ -621,6 +688,10 @@ pub mod pallet { // Cancel previous "chill" attempts Self::reset_chilling(&controller); return Ok(()) + } else { + // Can't participate in new Storage network if provider hasn't left the previous + // cluster yet + ensure!(!LeavingStorages::::contains_key(stash), Error::::NodeIsLeaving); } Self::do_add_storage(stash, cluster_id); @@ -661,7 +732,7 @@ pub mod pallet { .map_err(Into::>::into)?; (cluster, chill_delay) } else { - return Ok(()) // already chilled + return Ok(()) // node is already chilling or leaving the cluster }; if delay == T::BlockNumber::from(0u32) { @@ -741,6 +812,11 @@ pub mod pallet { ensure!(!>::contains_key(&stash), Error::::AlreadyInRole); ensure!(!>::contains_key(&stash), Error::::AlreadyInRole); + // Ensure that provider is not about leaving the cluster as it may cause the removal + // of an unexpected node after unbonding. + ensure!(!>::contains_key(&stash), Error::::NodeIsLeaving); + ensure!(!>::contains_key(&stash), Error::::NodeIsLeaving); + >::insert(new_node.clone(), stash.clone()); >::insert(stash, new_node); @@ -763,7 +839,7 @@ pub mod pallet { .or_else(|| >::get(&stash)) .ok_or(Error::::NodeHasNoStake)?; - let is_cluster_node = T::ClusterVisitor::cluster_has_node(&cluster_id, &node_pub_key); + let is_cluster_node = T::ClusterManager::contains_node(&cluster_id, &node_pub_key); ensure!(!is_cluster_node, Error::::FastChillProhibited); // block number + 1 => no overflow @@ -880,7 +956,7 @@ pub mod pallet { } impl StakingVisitor for Pallet { - fn node_has_stake( + fn has_activated_stake( node_pub_key: &NodePubKey, cluster_id: &ClusterId, ) -> Result { @@ -889,34 +965,46 @@ pub mod pallet { let maybe_cdn_in_cluster = CDNs::::get(&stash); let maybe_storage_in_cluster = Storages::::get(&stash); - let has_stake: bool = maybe_cdn_in_cluster + let has_activated_stake: bool = maybe_cdn_in_cluster .or(maybe_storage_in_cluster) .is_some_and(|staking_cluster| staking_cluster == *cluster_id); - Ok(has_stake) + Ok(has_activated_stake) + } + + fn has_stake(node_pub_key: &NodePubKey) -> bool { + >::get(node_pub_key).is_some() } - fn node_is_chilling(node_pub_key: &NodePubKey) -> Result { + fn has_chilling_attempt(node_pub_key: &NodePubKey) -> Result { let stash = >::get(node_pub_key).ok_or(StakingVisitorError::NodeStakeDoesNotExist)?; let controller = >::get(&stash).ok_or(StakingVisitorError::NodeStakeIsInBadState)?; - let is_chilling = >::get(&controller) + let is_chilling_attempt = >::get(&controller) .ok_or(StakingVisitorError::NodeStakeIsInBadState)? .chilling .is_some(); - Ok(is_chilling) + Ok(is_chilling_attempt) } } impl From for Error { fn from(error: ClusterVisitorError) -> Self { match error { - ClusterVisitorError::ClusterDoesNotExist => Error::::NodeHasNoStake, + ClusterVisitorError::ClusterDoesNotExist => Error::::NoCluster, ClusterVisitorError::ClusterGovParamsNotSet => Error::::NoClusterGovParams, } } } + + impl From for Error { + fn from(error: NodeVisitorError) -> Self { + match error { + NodeVisitorError::NodeDoesNotExist => Error::::NodeIsNotFound, + } + } + } } diff --git a/pallets/ddc-staking/src/mock.rs b/pallets/ddc-staking/src/mock.rs index 9085b5ed5..13c3023ca 100644 --- a/pallets/ddc-staking/src/mock.rs +++ b/pallets/ddc-staking/src/mock.rs @@ -3,9 +3,11 @@ #![allow(dead_code)] use crate::{self as pallet_ddc_staking, *}; -use ddc_primitives::{CDNNodePubKey, ClusterFeesParams, ClusterPricingParams, StorageNodePubKey}; +use ddc_primitives::{ + CDNNodePubKey, ClusterBondingParams, ClusterFeesParams, ClusterPricingParams, StorageNodePubKey, +}; use ddc_traits::{ - cluster::{ClusterVisitor, ClusterVisitorError}, + cluster::{ClusterManager, ClusterManagerError, ClusterVisitor, ClusterVisitorError}, node::{NodeVisitor, NodeVisitorError}, }; @@ -15,6 +17,8 @@ use frame_support::{ weights::constants::RocksDbWeight, }; use frame_system::mocking::{MockBlock, MockUncheckedExtrinsic}; +use lazy_static::lazy_static; +use parking_lot::{ReentrantMutex, ReentrantMutexGuard}; use sp_core::H256; use sp_io::TestExternalities; use sp_runtime::{ @@ -23,6 +27,7 @@ use sp_runtime::{ Perquintill, }; use sp_std::collections::btree_map::BTreeMap; +use std::cell::RefCell; /// The AccountId alias in this test module. pub(crate) type AccountId = u64; @@ -101,16 +106,14 @@ impl crate::pallet::Config for Test { type RuntimeEvent = RuntimeEvent; type WeightInfo = (); type ClusterVisitor = TestClusterVisitor; - type NodeVisitor = TestNodeVisitor; + type ClusterManager = TestClusterManager; + type NodeVisitor = MockNodeVisitor; } pub(crate) type DdcStakingCall = crate::Call; pub(crate) type TestRuntimeCall = ::RuntimeCall; pub struct TestClusterVisitor; impl ClusterVisitor for TestClusterVisitor { - fn cluster_has_node(_cluster_id: &ClusterId, _node_pub_key: &NodePubKey) -> bool { - true - } fn ensure_cluster(_cluster_id: &ClusterId) -> Result<(), ClusterVisitorError> { Ok(()) } @@ -157,12 +160,113 @@ impl ClusterVisitor for TestClusterVisitor { ) -> Result { Err(ClusterVisitorError::ClusterDoesNotExist) } + + fn get_bonding_params( + cluster_id: &ClusterId, + ) -> Result, ClusterVisitorError> { + Ok(ClusterBondingParams { + cdn_bond_size: >::get_bond_size( + cluster_id, + NodeType::CDN, + ) + .unwrap_or_default(), + cdn_chill_delay: >::get_chill_delay( + cluster_id, + NodeType::CDN, + ) + .unwrap_or_default(), + cdn_unbonding_delay: >::get_unbonding_delay( + cluster_id, + NodeType::CDN, + ) + .unwrap_or_default(), + storage_bond_size: >::get_bond_size( + cluster_id, + NodeType::Storage, + ) + .unwrap_or_default(), + storage_chill_delay: >::get_chill_delay( + cluster_id, + NodeType::Storage, + ) + .unwrap_or_default(), + storage_unbonding_delay: + >::get_unbonding_delay( + cluster_id, + NodeType::Storage, + ) + .unwrap_or_default(), + }) + } } -pub struct TestNodeVisitor; -impl NodeVisitor for TestNodeVisitor { +pub struct TestClusterManager; +impl ClusterManager for TestClusterManager { + fn contains_node(_cluster_id: &ClusterId, _node_pub_key: &NodePubKey) -> bool { + true + } + + fn add_node( + _cluster_id: &ClusterId, + _node_pub_key: &NodePubKey, + ) -> Result<(), ClusterManagerError> { + Ok(()) + } + + fn remove_node( + _cluster_id: &ClusterId, + _node_pub_key: &NodePubKey, + ) -> Result<(), ClusterManagerError> { + Ok(()) + } +} + +lazy_static! { + // We have to use the ReentrantMutex as every test's thread that needs to perform some configuration on the mock acquires the lock at least 2 times: + // the first time when the mock configuration happens, and + // the second time when the pallet calls the MockNodeVisitor during execution + static ref MOCK_NODE: ReentrantMutex> = + ReentrantMutex::new(RefCell::new(MockNode::default())); +} + +pub struct MockNode { + pub cluster_id: Option, + pub exists: bool, +} + +impl Default for MockNode { + fn default() -> Self { + Self { cluster_id: None, exists: true } + } +} + +pub struct MockNodeVisitor; + +impl MockNodeVisitor { + // Every test's thread must hold the lock till the end of its test + pub fn set_and_hold_lock(mock: MockNode) -> ReentrantMutexGuard<'static, RefCell> { + let lock = MOCK_NODE.lock(); + *lock.borrow_mut() = mock; + lock + } + + // Every test's thread must release the lock that it previously acquired in the end of its + // test + pub fn reset_and_release_lock(lock: ReentrantMutexGuard<'static, RefCell>) { + *lock.borrow_mut() = MockNode::default(); + } +} + +impl NodeVisitor for MockNodeVisitor { fn get_cluster_id(_node_pub_key: &NodePubKey) -> Result, NodeVisitorError> { - Ok(None) + let lock = MOCK_NODE.lock(); + let mock_ref = lock.borrow(); + Ok(mock_ref.cluster_id) + } + fn exists(_node_pub_key: &NodePubKey) -> bool { + let lock = MOCK_NODE.lock(); + let mock_ref = lock.borrow(); + mock_ref.exists } } diff --git a/pallets/ddc-staking/src/tests.rs b/pallets/ddc-staking/src/tests.rs index d0756f3d4..c38b36cda 100644 --- a/pallets/ddc-staking/src/tests.rs +++ b/pallets/ddc-staking/src/tests.rs @@ -1,7 +1,7 @@ //! Tests for the module. use super::{mock::*, *}; -use ddc_primitives::CDNNodePubKey; +use ddc_primitives::{CDNNodePubKey, StorageNodePubKey}; use frame_support::{assert_noop, assert_ok, traits::ReservableCurrency}; use pallet_balances::Error as BalancesError; @@ -290,3 +290,166 @@ fn staking_should_work() { assert_eq!(DdcStaking::cdns(3), None); }); } + +#[test] +fn cdn_full_unbonding_works() { + ExtBuilder::default().build_and_execute(|| { + System::set_block_number(1); + + let provider_stash: u64 = 1; + let provider_controller: u64 = 2; + let cluster_id = ClusterId::from([1; 20]); + let node_pub_key = NodePubKey::CDNPubKey(CDNNodePubKey::new([1; 32])); + + let lock = MockNodeVisitor::set_and_hold_lock(MockNode { + cluster_id: Some(cluster_id), + exists: true, + }); + + let cdn_bond_size = 10_u128; + let cdn_chill_delay = 10_u64; + let cdn_unbond_delay = 10_u64; + + // Put some money in account that we'll use. + let _ = Balances::make_free_balance_be(&provider_controller, 2000); + let _ = Balances::make_free_balance_be(&provider_stash, 2000); + + // Add new CDN participant, account 1 controlled by 2 with node 1. + assert_ok!(DdcStaking::bond( + RuntimeOrigin::signed(provider_stash), + provider_controller, + node_pub_key.clone(), + cdn_bond_size, // min bond size + )); + System::assert_last_event(Event::Bonded(provider_stash, cdn_bond_size).into()); + assert_ok!(DdcStaking::serve(RuntimeOrigin::signed(provider_controller), cluster_id)); + System::assert_last_event(Event::Activated(provider_stash).into()); + + assert_eq!(DdcStaking::cdns(provider_stash), Some(cluster_id)); + assert_eq!(DdcStaking::nodes(node_pub_key), Some(provider_stash)); + + // Set block timestamp. + Timestamp::set_timestamp(System::block_number() * BLOCK_TIME + INIT_TIMESTAMP); + + // Schedule CDN participant removal. + assert_ok!(DdcStaking::chill(RuntimeOrigin::signed(provider_controller))); + let chilling = System::block_number() + cdn_chill_delay; + System::assert_last_event(Event::ChillSoon(provider_stash, cluster_id, chilling).into()); + + // Set the block number that allows us to chill. + while System::block_number() < chilling { + System::set_block_number(System::block_number() + 1); + Timestamp::set_timestamp(System::block_number() * BLOCK_TIME + INIT_TIMESTAMP); + } + + // Actual CDN participant removal. + assert_ok!(DdcStaking::chill(RuntimeOrigin::signed(provider_controller))); + System::assert_last_event(Event::Chilled(provider_stash).into()); + + // Account is no longer a CDN participant. + assert_eq!(DdcStaking::cdns(provider_stash), None); + + // Start unbonding all tokens + assert_ok!(DdcStaking::unbond(RuntimeOrigin::signed(provider_controller), cdn_bond_size)); + System::assert_has_event(Event::LeaveSoon(provider_stash).into()); + assert_eq!(DdcStaking::leaving_cdns(provider_stash), Some(cluster_id)); + System::assert_last_event(Event::Unbonded(provider_stash, cdn_bond_size).into()); + + let unbonding = System::block_number() + cdn_unbond_delay; + // Set the block number that allows us to chill. + while System::block_number() < unbonding { + System::set_block_number(System::block_number() + 1); + Timestamp::set_timestamp(System::block_number() * BLOCK_TIME + INIT_TIMESTAMP); + } + + assert_ok!(DdcStaking::withdraw_unbonded(RuntimeOrigin::signed(provider_controller))); + System::assert_has_event(Event::Withdrawn(provider_stash, cdn_bond_size).into()); + assert_eq!(DdcStaking::leaving_cdns(provider_stash), None); + System::assert_last_event(Event::Left(provider_stash).into()); + + MockNodeVisitor::reset_and_release_lock(lock); + }); +} + +#[test] +fn storage_full_unbonding_works() { + ExtBuilder::default().build_and_execute(|| { + System::set_block_number(1); + + let provider_stash: u64 = 3; + let provider_controller: u64 = 4; + let cluster_id = ClusterId::from([1; 20]); + let node_pub_key = NodePubKey::StoragePubKey(StorageNodePubKey::new([2; 32])); + + let lock = MockNodeVisitor::set_and_hold_lock(MockNode { + cluster_id: Some(cluster_id), + exists: true, + }); + + let storage_bond_size = 10_u128; + let storage_chill_delay = 10_u64; + let storage_unbond_delay = 10_u64; + + // Put some money in account that we'll use. + let _ = Balances::make_free_balance_be(&provider_controller, 2000); + let _ = Balances::make_free_balance_be(&provider_stash, 2000); + + // Add new Storage participant, account 1 controlled by 2 with node 1. + assert_ok!(DdcStaking::bond( + RuntimeOrigin::signed(provider_stash), + provider_controller, + node_pub_key.clone(), + storage_bond_size, // min bond size + )); + System::assert_last_event(Event::Bonded(provider_stash, storage_bond_size).into()); + assert_ok!(DdcStaking::store(RuntimeOrigin::signed(provider_controller), cluster_id)); + System::assert_last_event(Event::Activated(provider_stash).into()); + + assert_eq!(DdcStaking::storages(provider_stash), Some(cluster_id)); + assert_eq!(DdcStaking::nodes(node_pub_key), Some(provider_stash)); + + // Set block timestamp. + Timestamp::set_timestamp(System::block_number() * BLOCK_TIME + INIT_TIMESTAMP); + + // Schedule Storage participant removal. + assert_ok!(DdcStaking::chill(RuntimeOrigin::signed(provider_controller))); + let chilling = System::block_number() + storage_chill_delay; + System::assert_last_event(Event::ChillSoon(provider_stash, cluster_id, chilling).into()); + + // Set the block number that allows us to chill. + while System::block_number() < chilling { + System::set_block_number(System::block_number() + 1); + Timestamp::set_timestamp(System::block_number() * BLOCK_TIME + INIT_TIMESTAMP); + } + + // Actual Storage participant removal. + assert_ok!(DdcStaking::chill(RuntimeOrigin::signed(provider_controller))); + System::assert_last_event(Event::Chilled(provider_stash).into()); + + // Account is no longer a Storage participant. + assert_eq!(DdcStaking::storages(provider_stash), None); + + // Start unbonding all tokens + assert_ok!(DdcStaking::unbond( + RuntimeOrigin::signed(provider_controller), + storage_bond_size + )); + System::assert_has_event(Event::LeaveSoon(provider_stash).into()); + assert_eq!(DdcStaking::leaving_storages(provider_stash), Some(cluster_id)); + System::assert_last_event(Event::Unbonded(provider_stash, storage_bond_size).into()); + + let unbonding = System::block_number() + storage_unbond_delay; + // Set the block number that allows us to chill. + while System::block_number() < unbonding { + System::set_block_number(System::block_number() + 1); + Timestamp::set_timestamp(System::block_number() * BLOCK_TIME + INIT_TIMESTAMP); + } + + assert_ok!(DdcStaking::withdraw_unbonded(RuntimeOrigin::signed(provider_controller))); + System::assert_has_event(Event::Withdrawn(provider_stash, storage_bond_size).into()); + assert_eq!(DdcStaking::leaving_storages(provider_stash), None); + System::assert_last_event(Event::Left(provider_stash).into()); + + MockNodeVisitor::reset_and_release_lock(lock); + }); +} diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index a69994f4f..53e2d48e9 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -53,6 +53,16 @@ pub struct ClusterFeesParams { pub cluster_reserve_share: Perquintill, } +#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo, PartialEq)] +pub struct ClusterBondingParams { + pub cdn_bond_size: u128, + pub cdn_chill_delay: BlockNumber, + pub cdn_unbonding_delay: BlockNumber, + pub storage_bond_size: u128, + pub storage_chill_delay: BlockNumber, + pub storage_unbonding_delay: BlockNumber, +} + #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo, PartialEq)] pub enum NodePubKey { diff --git a/runtime/cere-dev/src/lib.rs b/runtime/cere-dev/src/lib.rs index c78496d82..a0bf11e37 100644 --- a/runtime/cere-dev/src/lib.rs +++ b/runtime/cere-dev/src/lib.rs @@ -1322,6 +1322,7 @@ impl pallet_ddc_staking::Config for Runtime { type RuntimeEvent = RuntimeEvent; type WeightInfo = pallet_ddc_staking::weights::SubstrateWeight; type ClusterVisitor = pallet_ddc_clusters::Pallet; + type ClusterManager = pallet_ddc_clusters::Pallet; type NodeVisitor = pallet_ddc_nodes::Pallet; } @@ -1342,6 +1343,7 @@ impl pallet_ddc_customers::Config for Runtime { impl pallet_ddc_nodes::Config for Runtime { type RuntimeEvent = RuntimeEvent; + type StakingVisitor = pallet_ddc_staking::Pallet; } impl pallet_ddc_clusters::Config for Runtime { diff --git a/traits/src/cluster.rs b/traits/src/cluster.rs index 39ea08e08..d980c1ae4 100644 --- a/traits/src/cluster.rs +++ b/traits/src/cluster.rs @@ -1,7 +1,7 @@ use codec::{Decode, Encode}; use ddc_primitives::{ - ClusterFeesParams, ClusterGovParams, ClusterId, ClusterParams, ClusterPricingParams, - NodePubKey, NodeType, + ClusterBondingParams, ClusterFeesParams, ClusterGovParams, ClusterId, ClusterParams, + ClusterPricingParams, NodePubKey, NodeType, }; use frame_support::dispatch::DispatchResult; use frame_system::Config; @@ -9,8 +9,6 @@ use scale_info::TypeInfo; use sp_runtime::RuntimeDebug; pub trait ClusterVisitor { - fn cluster_has_node(cluster_id: &ClusterId, node_pub_key: &NodePubKey) -> bool; - fn ensure_cluster(cluster_id: &ClusterId) -> Result<(), ClusterVisitorError>; fn get_bond_size( @@ -35,6 +33,10 @@ pub trait ClusterVisitor { cluster_id: &ClusterId, node_type: NodeType, ) -> Result; + + fn get_bonding_params( + cluster_id: &ClusterId, + ) -> Result, ClusterVisitorError>; } pub trait ClusterCreator { @@ -52,3 +54,22 @@ pub enum ClusterVisitorError { ClusterDoesNotExist, ClusterGovParamsNotSet, } + +pub trait ClusterManager { + fn contains_node(cluster_id: &ClusterId, node_pub_key: &NodePubKey) -> bool; + fn add_node( + cluster_id: &ClusterId, + node_pub_key: &NodePubKey, + ) -> Result<(), ClusterManagerError>; + fn remove_node( + cluster_id: &ClusterId, + node_pub_key: &NodePubKey, + ) -> Result<(), ClusterManagerError>; +} + +pub enum ClusterManagerError { + AttemptToAddNonExistentNode, + AttemptToAddAlreadyAssignedNode, + AttemptToRemoveNotAssignedNode, + AttemptToRemoveNonExistentNode, +} diff --git a/traits/src/node.rs b/traits/src/node.rs index af22b5a8b..0ab55c1d1 100644 --- a/traits/src/node.rs +++ b/traits/src/node.rs @@ -3,6 +3,7 @@ use frame_system::Config; pub trait NodeVisitor { fn get_cluster_id(node_pub_key: &NodePubKey) -> Result, NodeVisitorError>; + fn exists(node_pub_key: &NodePubKey) -> bool; } pub enum NodeVisitorError { diff --git a/traits/src/staking.rs b/traits/src/staking.rs index 5ec1e8bcf..af1a185e2 100644 --- a/traits/src/staking.rs +++ b/traits/src/staking.rs @@ -2,12 +2,14 @@ use ddc_primitives::{ClusterId, NodePubKey}; use frame_system::Config; pub trait StakingVisitor { - fn node_has_stake( + fn has_activated_stake( node_pub_key: &NodePubKey, cluster_id: &ClusterId, ) -> Result; - fn node_is_chilling(node_pub_key: &NodePubKey) -> Result; + fn has_stake(node_pub_key: &NodePubKey) -> bool; + + fn has_chilling_attempt(node_pub_key: &NodePubKey) -> Result; } pub enum StakingVisitorError {