Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loose coupling between Staking and Clusters pallets #116

Merged
merged 5 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading