Skip to content

Commit

Permalink
Merge pull request #116 from Cerebellum-Network/feat/ddc-traits
Browse files Browse the repository at this point in the history
Loose coupling between Staking and Clusters pallets
  • Loading branch information
yahortsaryk authored Nov 6, 2023
2 parents 1a1eaaa + 232427a commit 3a09091
Show file tree
Hide file tree
Showing 19 changed files with 570 additions and 357 deletions.
345 changes: 179 additions & 166 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions pallets/ddc-clusters/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ edition = "2021"
[dependencies]
codec = { package = "parity-scale-codec", version = "3.1.5", default-features = false, features = ["derive"] }
ddc-primitives = { version = "0.1.0", default-features = false, path = "../../primitives" }
ddc-traits = { version = "0.1.0", default-features = false, path = "../../traits" }
frame-benchmarking = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.30", optional = true }
frame-support = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.30" }
frame-system = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.30" }
Expand All @@ -18,7 +19,6 @@ sp-std = { version = "4.0.0-dev", default-features = false, git = "https://githu
sp-core = { version = "6.0.0", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.30", default-features = false }
pallet-contracts = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.30" }
pallet-ddc-nodes = { version = "4.7.0", default-features = false, path = "../ddc-nodes" }
pallet-ddc-staking = { version = "4.8.1", default-features = false, path = "../ddc-staking" }

[dev-dependencies]
sp-core = { version = "6.0.0", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.30" }
Expand All @@ -35,7 +35,6 @@ std = [
"frame-benchmarking/std",
"pallet-contracts/std",
"pallet-ddc-nodes/std",
"pallet-ddc-staking/std",
"scale-info/std",
"sp-core/std",
"sp-io/std",
Expand Down
157 changes: 65 additions & 92 deletions pallets/ddc-clusters/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,23 @@
#![recursion_limit = "256"]
#![feature(is_some_and)] // ToDo: delete at rustc > 1.70

use crate::{
cluster::{Cluster, ClusterError, ClusterParams},
node_provider_auth::{NodeProviderAuthContract, NodeProviderAuthContractError},
};
use ddc_primitives::{ClusterId, NodePubKey};
use ddc_traits::{
cluster::{ClusterVisitor, ClusterVisitorError},
staking::{StakingVisitor, StakingVisitorError},
};
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
pub use pallet::*;
use pallet_ddc_nodes::{NodeRepository, NodeTrait};
use sp_std::prelude::*;
mod cluster;

pub use crate::cluster::{Cluster, ClusterError, ClusterParams};

/// ink! 4.x selector for the "is_authorized" message, equals to the first four bytes of the
/// blake2("is_authorized"). See also: https://use.ink/basics/selectors#selector-calculation/,
/// https://use.ink/macros-attributes/selector/.
const INK_SELECTOR_IS_AUTHORIZED: [u8; 4] = [0x96, 0xb0, 0x45, 0x3e];

/// The maximum amount of weight that the cluster extension contract call is allowed to consume.
/// See also https://github.com/paritytech/substrate/blob/a3ed0119c45cdd0d571ad34e5b3ee7518c8cef8d/frame/contracts/rpc/src/lib.rs#L63.
const EXTENSION_CALL_GAS_LIMIT: Weight = Weight::from_ref_time(5_000_000_000_000);
mod cluster;
mod node_provider_auth;

#[frame_support::pallet]
pub mod pallet {
Expand All @@ -45,11 +44,10 @@ pub mod pallet {
pub struct Pallet<T>(_);

#[pallet::config]
pub trait Config:
frame_system::Config + pallet_contracts::Config + pallet_ddc_staking::Config
{
pub trait Config: frame_system::Config + pallet_contracts::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type NodeRepository: NodeRepository<Self>;
type NodeRepository: NodeRepository<Self>; // todo: get rid of tight coupling with nodes-pallet
type StakingVisitor: StakingVisitor<Self>;
}

#[pallet::event]
Expand All @@ -71,15 +69,12 @@ pub mod pallet {
NodeIsAlreadyAssigned,
NodeIsNotAssigned,
OnlyClusterManager,
NotAuthorized,
NoStake,
/// Conditions for fast chill are not met, try the regular `chill` from
/// `pallet-ddc-staking`.
FastChillProhibited,
NodeIsNotAuthorized,
NodeHasNoStake,
NodeStakeIsInvalid,
/// Cluster candidate should not plan to chill.
ChillingProhibited,
/// Origin of the call is not a controller of the stake associated with the provided node.
NotNodeController,
NodeChillingIsProhibited,
NodeAuthContractCallFailed,
}

#[pallet::storage]
Expand All @@ -96,7 +91,7 @@ pub mod pallet {
Blake2_128Concat,
NodePubKey,
bool,
ValueQuery,
OptionQuery,
>;

#[pallet::call]
Expand Down Expand Up @@ -129,58 +124,37 @@ pub mod pallet {
let cluster =
Clusters::<T>::try_get(&cluster_id).map_err(|_| Error::<T>::ClusterDoesNotExist)?;
ensure!(cluster.manager_id == caller_id, Error::<T>::OnlyClusterManager);

// Node with this node with this public key exists.
let mut node = T::NodeRepository::get(node_pub_key.clone())
.map_err(|_| Error::<T>::AttemptToAddNonExistentNode)?;
ensure!(node.get_cluster_id().is_none(), Error::<T>::NodeIsAlreadyAssigned);

// Sufficient funds are locked at the DDC Staking module.
let node_provider_stash =
<pallet_ddc_staking::Pallet<T>>::nodes(&node_pub_key).ok_or(Error::<T>::NoStake)?;
let maybe_edge_in_cluster =
<pallet_ddc_staking::Pallet<T>>::edges(&node_provider_stash);
let maybe_storage_in_cluster =
<pallet_ddc_staking::Pallet<T>>::storages(&node_provider_stash);
let has_stake = maybe_edge_in_cluster
.or(maybe_storage_in_cluster)
.is_some_and(|staking_cluster| staking_cluster == cluster_id);
ensure!(has_stake, Error::<T>::NoStake);
let has_stake = T::StakingVisitor::node_has_stake(&node_pub_key, &cluster_id)
.map_err(|e| Into::<Error<T>>::into(StakingVisitorError::from(e)))?;
ensure!(has_stake, Error::<T>::NodeHasNoStake);

// Candidate is not planning to pause operations any time soon.
let node_provider_controller =
<pallet_ddc_staking::Pallet<T>>::bonded(&node_provider_stash)
.ok_or(<pallet_ddc_staking::Error<T>>::BadState)?;
let chilling = <pallet_ddc_staking::Pallet<T>>::ledger(&node_provider_controller)
.ok_or(<pallet_ddc_staking::Error<T>>::BadState)?
.chilling
.is_some();
ensure!(!chilling, Error::<T>::ChillingProhibited);
let is_chilling = T::StakingVisitor::node_is_chilling(&node_pub_key)
.map_err(|e| Into::<Error<T>>::into(StakingVisitorError::from(e)))?;
ensure!(!is_chilling, Error::<T>::NodeChillingIsProhibited);

// Cluster extension smart contract allows joining.
let call_data = {
// is_authorized(node_provider: AccountId, node: Vec<u8>, node_variant: u8) -> bool
let args: ([u8; 4], T::AccountId, Vec<u8>, u8) = (
INK_SELECTOR_IS_AUTHORIZED,
node_provider_stash,
node_pub_key.encode()[1..].to_vec(), // remove the first byte added by SCALE
node_pub_key.variant_as_number(),
);
args.encode()
};
let is_authorized = pallet_contracts::Pallet::<T>::bare_call(
caller_id,
let auth_contract = NodeProviderAuthContract::<T>::new(
cluster.props.node_provider_auth_contract,
Default::default(),
EXTENSION_CALL_GAS_LIMIT,
None,
call_data,
false,
)
.result?
.data
.first()
.is_some_and(|x| *x == 1);
ensure!(is_authorized, Error::<T>::NotAuthorized);

caller_id,
);
let is_authorized = auth_contract
.is_authorized(
node.get_provider_id().to_owned(),
node.get_pub_key().to_owned(),
node.get_type(),
)
.map_err(|e| Into::<Error<T>>::into(NodeProviderAuthContractError::from(e)))?;
ensure!(is_authorized, Error::<T>::NodeIsNotAuthorized);

// Add node to the cluster.
node.set_cluster_id(Some(cluster_id.clone()));
T::NodeRepository::update(node).map_err(|_| Error::<T>::AttemptToAddNonExistentNode)?;
ClustersNodes::<T>::insert(cluster_id.clone(), node_pub_key.clone(), true);
Expand Down Expand Up @@ -230,36 +204,35 @@ pub mod pallet {

Ok(())
}
}

/// Allow cluster node candidate to chill in the next DDC era.
///
/// The dispatch origin for this call must be _Signed_ by the controller.
#[pallet::weight(10_000)]
pub fn fast_chill(origin: OriginFor<T>, node: NodePubKey) -> DispatchResult {
let controller = ensure_signed(origin)?;

let stash = <pallet_ddc_staking::Pallet<T>>::ledger(&controller)
.ok_or(<pallet_ddc_staking::Error<T>>::NotController)?
.stash;
let node_stash = <pallet_ddc_staking::Pallet<T>>::nodes(&node)
.ok_or(<pallet_ddc_staking::Error<T>>::BadState)?;
ensure!(stash == node_stash, Error::<T>::NotNodeController);
impl<T: Config> ClusterVisitor<T> for Pallet<T> {
fn cluster_has_node(cluster_id: &ClusterId, node_pub_key: &NodePubKey) -> bool {
ClustersNodes::<T>::get(cluster_id, node_pub_key).is_some()
}

let cluster = <pallet_ddc_staking::Pallet<T>>::edges(&stash)
.or(<pallet_ddc_staking::Pallet<T>>::storages(&stash))
.ok_or(Error::<T>::NoStake)?;
let is_cluster_node = ClustersNodes::<T>::get(cluster, node);
ensure!(!is_cluster_node, Error::<T>::FastChillProhibited);
fn ensure_cluster(cluster_id: &ClusterId) -> Result<(), ClusterVisitorError> {
Clusters::<T>::get(&cluster_id)
.map(|_| ())
.ok_or(ClusterVisitorError::ClusterDoesNotExist)
}
}

let can_chill_from = <pallet_ddc_staking::Pallet<T>>::current_era().unwrap_or(0) + 1;
<pallet_ddc_staking::Pallet<T>>::chill_stash_soon(
&stash,
&controller,
cluster,
can_chill_from,
);
impl<T> From<StakingVisitorError> for Error<T> {
fn from(error: StakingVisitorError) -> Self {
match error {
StakingVisitorError::NodeStakeDoesNotExist => Error::<T>::NodeHasNoStake,
StakingVisitorError::NodeStakeIsInBadState => Error::<T>::NodeStakeIsInvalid,
}
}
}

Ok(())
impl<T> From<NodeProviderAuthContractError> for Error<T> {
fn from(error: NodeProviderAuthContractError) -> Self {
match error {
NodeProviderAuthContractError::ContractCallFailed =>
Error::<T>::NodeAuthContractCallFailed,
}
}
}
}
69 changes: 69 additions & 0 deletions pallets/ddc-clusters/src/node_provider_auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use crate::Config;
use codec::Encode;
use ddc_primitives::{NodePubKey, NodeType};
use frame_support::weights::Weight;
use pallet_contracts::chain_extension::UncheckedFrom;
use sp_std::prelude::Vec;

/// ink! 4.x selector for the "is_authorized" message, equals to the first four bytes of the
/// blake2("is_authorized"). See also: https://use.ink/basics/selectors#selector-calculation/,
/// https://use.ink/macros-attributes/selector/.
const INK_SELECTOR_IS_AUTHORIZED: [u8; 4] = [0x96, 0xb0, 0x45, 0x3e];

/// The maximum amount of weight that the cluster extension contract call is allowed to consume.
/// See also https://github.com/paritytech/substrate/blob/a3ed0119c45cdd0d571ad34e5b3ee7518c8cef8d/frame/contracts/rpc/src/lib.rs#L63.
const EXTENSION_CALL_GAS_LIMIT: Weight = Weight::from_ref_time(5_000_000_000_000);

pub struct NodeProviderAuthContract<T: Config> {
contract_id: T::AccountId,
caller_id: T::AccountId,
}

impl<T: Config> NodeProviderAuthContract<T>
where
T::AccountId: UncheckedFrom<T::Hash> + AsRef<[u8]>,
{
pub fn is_authorized(
&self,
node_provider_id: T::AccountId,
node_pub_key: NodePubKey,
node_type: NodeType,
) -> Result<bool, NodeProviderAuthContractError> {
let call_data = {
// is_authorized(node_provider: AccountId, node: Vec<u8>, node_variant: u8) -> bool
let args: ([u8; 4], T::AccountId, Vec<u8>, u8) = (
INK_SELECTOR_IS_AUTHORIZED,
node_provider_id,
/* remove the first byte* added by SCALE */
node_pub_key.encode()[1..].to_vec(),
node_type.into(),
);
args.encode()
};

let is_authorized = pallet_contracts::Pallet::<T>::bare_call(
self.caller_id.clone(),
self.contract_id.clone(),
Default::default(),
EXTENSION_CALL_GAS_LIMIT,
None,
call_data,
false,
)
.result
.map_err(|_| NodeProviderAuthContractError::ContractCallFailed)?
.data
.first()
.is_some_and(|x| *x == 1);

Ok(is_authorized)
}

pub fn new(contract_id: T::AccountId, caller_id: T::AccountId) -> Self {
Self { contract_id, caller_id }
}
}

pub enum NodeProviderAuthContractError {
ContractCallFailed,
}
21 changes: 10 additions & 11 deletions pallets/ddc-nodes/src/cdn_node.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
use crate::node::{
Node, NodeError, NodeParams, NodeProps, NodePropsRef, NodePubKeyRef, NodeTrait, NodeType,
};
use crate::node::{Node, NodeError, NodeParams, NodeProps, NodePropsRef, NodePubKeyRef, NodeTrait};
use codec::{Decode, Encode};
use ddc_primitives::{CDNNodePubKey, ClusterId, NodePubKey};
use ddc_primitives::{CDNNodePubKey, ClusterId, NodePubKey, NodeType};
use frame_support::{parameter_types, BoundedVec};
use scale_info::TypeInfo;
use sp_runtime::RuntimeDebug;
Expand All @@ -13,9 +11,10 @@ parameter_types! {
}

#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo, PartialEq)]
pub struct CDNNode<AccountId> {
#[scale_info(skip_type_params(T))]
pub struct CDNNode<T: frame_system::Config> {
pub pub_key: CDNNodePubKey,
pub provider_id: AccountId,
pub provider_id: T::AccountId,
pub cluster_id: Option<ClusterId>,
pub props: CDNNodeProps,
}
Expand All @@ -32,11 +31,11 @@ pub struct CDNNodeParams {
pub params: Vec<u8>, // should be replaced with specific parameters for this type of node
}

impl<AccountId> NodeTrait<AccountId> for CDNNode<AccountId> {
impl<T: frame_system::Config> NodeTrait<T> for CDNNode<T> {
fn get_pub_key<'a>(&'a self) -> NodePubKeyRef<'a> {
NodePubKeyRef::CDNPubKeyRef(&self.pub_key)
}
fn get_provider_id(&self) -> &AccountId {
fn get_provider_id(&self) -> &T::AccountId {
&self.provider_id
}
fn get_props<'a>(&'a self) -> NodePropsRef<'a> {
Expand Down Expand Up @@ -70,12 +69,12 @@ impl<AccountId> NodeTrait<AccountId> for CDNNode<AccountId> {
}
fn new(
node_pub_key: NodePubKey,
provider_id: AccountId,
provider_id: T::AccountId,
node_params: NodeParams,
) -> Result<Node<AccountId>, NodeError> {
) -> Result<Node<T>, NodeError> {
match node_pub_key {
NodePubKey::CDNPubKey(pub_key) => match node_params {
NodeParams::CDNParams(node_params) => Ok(Node::CDN(CDNNode::<AccountId> {
NodeParams::CDNParams(node_params) => Ok(Node::CDN(CDNNode::<T> {
provider_id,
pub_key,
cluster_id: None,
Expand Down
Loading

0 comments on commit 3a09091

Please sign in to comment.