🏗️ Cyber-Physical System Pallet
pallet-robonomics-cps is a Substrate pallet providing an efficient, secure, and minimal hierarchical data model (tree/forest) for representing Cyber-Physical Systems (CPS) on-chain.
🎯 What Can You Build?
- Smart Homes: Model rooms, devices, sensors as a tree
- Sensor Networks: Organize monitoring stations and their sensors
- Robotics: Structure robot fleets with subsystems and components
- Industrial IoT: Represent factories, production lines, and equipment
- Any CPS: Flexible enough for any hierarchical cyber-physical system
✨ Key Features
- ⚡ O(1) Performance: All operations are constant-time (no recursive traversals)
- 💾 Compact Storage: SCALE encoding saves 75-87% storage for typical node IDs
- 🔐 Privacy-Aware: Mix public and encrypted data in the same tree
- 🎛️ Configurable: Adjust depth limits, children limits, data sizes
- 🔗 Runtime Hooks: React to payload changes with custom logic
- 🌲 Forest Support: Multiple independent CPS trees in one registry
📖 Quick Start: Building a Smart Home
Let's build a simple smart home CPS step-by-step:
Home (root)
├─ Kitchen
│ ├─ Smart Lamp
│ └─ Temperature Sensor
└─ Living Room
└─ Motion Detector
Step 1: Create the Home (Root Node)
use pallet_robonomics_cps::{NodeData, NodeId};
use frame_support::BoundedVec;
// Plain metadata describing the home
let home_meta = NodeData::Plain(
BoundedVec::try_from(
br#"{"type":"building","name":"My Smart Home"}"#. to_vec()
). unwrap()
);
// Create root node (parent = None)
Cps::create_node(origin, None, Some(home_meta), None)?;
// → Creates NodeId(0)
Step 2: Add Kitchen Room
let kitchen_meta = NodeData::Plain(
BoundedVec::try_from(
br#"{"type":"room","name":"Kitchen","floor":1}"#.to_vec()
).unwrap()
);
// Create as child of Home
Cps::create_node(origin, Some(NodeId(0)), Some(kitchen_meta), None)?;
// → Creates NodeId(1)
Step 3: Add Smart Lamp with Encrypted Config
use pallet_robonomics_cps::DefaultEncryptedData;
// Public metadata
let lamp_meta = NodeData::Plain(
BoundedVec::try_from(
br#"{"type":"device","model":"SmartBulb Pro"}"#.to_vec()
).unwrap()
);
// Encrypted WiFi credentials (encrypted off-chain before submission)
let encrypted_config = DefaultEncryptedData::XChaCha20Poly1305(
BoundedVec::try_from(encrypted_wifi_creds). unwrap()
);
let lamp_payload = NodeData::Encrypted(encrypted_config);
// Create lamp under Kitchen
Cps::create_node(
origin,
Some(NodeId(1)), // Kitchen
Some(lamp_meta),
Some(lamp_payload)
)?;
// → Creates NodeId(2)
Step 4: Update Sensor Data
// Temperature sensor sending telemetry
let sensor_reading = NodeData::Plain(
BoundedVec::try_from(
br#"{"temp_c":22.5,"humidity":45,"timestamp":1733011200}"#.to_vec()
).unwrap()
);
Cps::set_payload(origin, NodeId(3), Some(sensor_reading))? ;
// Triggers OnPayloadSet callback if configured
Step 5: Move a Device
// Move lamp from Kitchen to Living Room
Cps::move_node(origin, NodeId(2), NodeId(4))?;
// Automatically updates paths of lamp and all descendants
🏛️ Architecture Overview
┌─────────────────────────────────────────────────────────────┐
│ USER LAYER │
│ Wallet • Mobile App • IoT Gateway • Automation Service │
└──────────────────────┬──────────────────────────────────────┘
│
│ Extrinsics (create, update, move)
▼
┌─────────────────────────────────────────────────────────────┐
│ ON-CHAIN PALLET │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Nodes │ │ NodesByParent│ │ RootNodes │ │
│ │ (main map) │ │ (index) │ │ (index) │ │
│ └─────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ Storage: Tree structure + Plain/Encrypted data blobs │
└──────────────────────┬──────────────────────────────────────┘
│
│ RPC Queries (read state)
▼
┌─────────────────────────────────────────────────────────────┐
│ OFF-CHAIN LOGIC │
│ • Fetch tree structure │
│ • Decrypt encrypted nodes (client-side) │
│ • Build semantic CPS model │
│ • Control physical devices │
│ • Display UI • Analytics • Automation │
└─────────────────────────────────────────────────────────────┘
🗂️ Data Structures Explained
Node Structure
pub struct Node<AccountId, T: Config> {
parent: Option<NodeId>, // None = root node
owner: AccountId, // Who controls this node
path: BoundedVec<NodeId, MaxDepth>, // [grandparent, parent] for O(1) ops
meta: Option<NodeData>, // Config, type, name, specs
payload: Option<NodeData>, // State, readings, telemetry
}
Example Node:
{
"parent": 1,
"owner": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
"path": [0, 1],
"meta": {
"Plain": "{\"type\":\"sensor\",\"name\":\"Temp-01\"}"
},
"payload": {
"Plain": "{\"temp_c\":23.1,\"timestamp\":1733011200}"
}
}
NodeData: Plain vs Encrypted
| Plain Data |
Encrypted Data |
NodeData::Plain(bytes) |
NodeData::Encrypted(DefaultEncryptedData::XChaCha20Poly1305(ciphertext)) |
| Readable by everyone |
Only readable with decryption keys (off-chain) |
| Use for: public specs, model numbers |
Use for: WiFi passwords, API keys, personal data |
Example: {"type":"device"} |
Example: 0x8f3a2... (encrypted blob) |
Encryption Algorithms Supported
pub enum DefaultEncryptedData {
XChaCha20Poly1305(BoundedVec<u8, MaxDataSize>), // Recommended
AesGcm256(BoundedVec<u8, MaxDataSize>), // Hardware accelerated
ChaCha20Poly1305(BoundedVec<u8, MaxDataSize>), // Software optimized
}
🔧 Core Operations (Extrinsics)
1️⃣ create_node - Add Node to Tree
Signature:
fn create_node(
origin: OriginFor<T>,
parent_id: Option<NodeId>,
meta: Option<NodeData<T::EncryptedData>>,
payload: Option<NodeData<T::EncryptedData>>,
) -> DispatchResult
Use Cases:
parent_id = None → Create a new CPS tree (root node)
parent_id = Some(id) → Add child to existing node
Rules:
- Child inherits parent's owner
- Checks max depth and max children limits
- Updates
Nodes, NodesByParent, and RootNodes storage
2️⃣ set_meta - Update Node Metadata
Signature:
fn set_meta(
origin: OriginFor<T>,
node_id: NodeId,
meta: Option<NodeData<T::EncryptedData>>,
) -> DispatchResult
Example:
// Change device name
let new_meta = NodeData::Plain(
BoundedVec::try_from(br#"{"type":"device","name":"Kitchen Lamp Pro"}"#.to_vec()).unwrap()
);
Cps::set_meta(origin, NodeId(2), Some(new_meta))? ;
3️⃣ set_payload - Update Node Payload
Signature:
fn set_payload(
origin: OriginFor<T>,
node_id: NodeId,
payload: Option<NodeData<T::EncryptedData>>,
) -> DispatchResult
Use Cases:
- Sensors pushing telemetry
- Devices updating their state
- Configuration updates
Special: Triggers OnPayloadSet callback for custom runtime logic
4️⃣ move_node - Reorganize Tree
Signature:
fn move_node(
origin: OriginFor<T>,
node_id: NodeId,
new_parent_id: NodeId,
) -> DispatchResult
Example:
// Move sensor from Kitchen to Living Room
Cps::move_node(origin, NodeId(3), NodeId(4))?;
Safety Guarantees:
- ✅ Prevents cycles (O(1) path check)
- ✅ Enforces same owner for node and new parent
- ✅ Updates all descendant paths automatically
- ✅ Maintains index consistency
5️⃣ delete_node - Remove Node
Signature:
fn delete_node(
origin: OriginFor<T>,
node_id: NodeId,
) -> DispatchResult
Rules:
- ❌ Cannot delete nodes with children (safety)
- ✅ Cleans up all indexes (
NodesByParent, RootNodes)
- ✅ Only owner can delete
🔐 Security Model
Ownership-Based Access Control
┌────────────────────────────────────────────────┐
│ Every node has an OWNER │
│ Only the owner can: │
│ • Update metadata (set_meta) │
│ • Update payload (set_payload) │
│ • Move node (move_node) │
│ • Delete node (delete_node) │
│ • Create children (create_node) │
└────────────────────────────────────────────────┘
Invariants Enforced
✅ No Cycles: Path-based O(1) cycle detection
✅ Ownership Inheritance: Children always have parent's owner
✅ Index Consistency: All storage items stay synchronized
✅ Safe Deletion: Nodes with children cannot be deleted
✅ Depth Limits: Tree depth never exceeds MaxTreeDepth
🎭 Privacy: Plain vs Encrypted
Visibility Comparison
| Scenario |
Plain Meta |
Encrypted Meta |
Who Can See? |
| Public device specs |
✅ |
❌ |
Everyone |
| WiFi credentials |
❌ |
✅ |
Owner only (with key) |
| Sensor model number |
✅ |
❌ |
Everyone |
| Personal health data |
❌ |
✅ |
Owner only (with key) |
| Room names |
✅ |
❌ |
Everyone |
| Access codes |
❌ |
✅ |
Owner only (with key) |
Mixed Privacy Example
// Public: What the device is
let public_meta = NodeData::Plain(
BoundedVec::try_from(br#"{"type":"camera","model":"SecureCam 2000"}"#.to_vec()).unwrap()
);
// Private: How to access it
let private_payload = NodeData::Encrypted(
DefaultEncryptedData::XChaCha20Poly1305(encrypted_rtsp_url)
);
Cps::create_node(origin, Some(parent), Some(public_meta), Some(private_payload))?;
Result:
- 🌐 Everyone sees: "This is a SecureCam 2000"
- 🔒 Only owner with key sees:
rtsp://admin:[email protected]:554/stream
📊 Storage Layout
Three Storage Items
1. Nodes: StorageMap<NodeId, Node>
├─ NodeId(0) → Node { parent: None, owner: Alice, ... }
├─ NodeId(1) → Node { parent: Some(0), owner: Alice, ... }
└─ NodeId(2) → Node { parent: Some(1), owner: Alice, ... }
2. NodesByParent: StorageMap<NodeId, Vec<NodeId>>
├─ NodeId(0) → [1, 4] // Home has Kitchen and Living Room
├─ NodeId(1) → [2, 3] // Kitchen has Lamp and Sensor
└─ NodeId(4) → [5] // Living Room has Motion Detector
3. RootNodes: StorageValue<Vec<NodeId>>
└─ [0, 10, 20] // Three independent CPS trees
Path-Based Performance
Traditional Tree (Recursive):
Cycle Check: O(n) - traverse up the tree
Depth Check: O(n) - count ancestors
Ancestor Test: O(n) - walk up the tree
CPS Pallet (Path-Based):
Cycle Check: O(1) - new_parent. path.contains(node_id)
Depth Check: O(1) - parent.path.len()
Ancestor Test: O(1) - path.contains(ancestor_id)
🔗 Runtime Hooks: OnPayloadSet
Use Cases for Callbacks
pub trait OnPayloadSet<AccountId, EncryptedData> {
fn on_payload_set(
node_id: NodeId,
meta: Option<NodeData<EncryptedData>>,
payload: Option<NodeData<EncryptedData>>,
);
}
Example Implementations:
- 📊 Indexing: Track payload changes for off-chain queries
- 🔔 Notifications: Trigger alerts when sensors update
- 📈 Analytics: Collect metrics about system usage
- 🤖 Automation: Chain actions (e.g., sensor triggers actuator)
- 📝 Audit Trail: Log all payload modifications
Configuration:
// No callback
type OnPayloadSet = ();
// Single handler
type OnPayloadSet = MyHandler;
// Multiple handlers
type OnPayloadSet = (IndexerHandler, NotificationHandler, AnalyticsHandler);
🔑 Proxy-Based Access Delegation
The pallet integrates with Substrate's pallet-proxy to enable delegated access without transferring ownership. Think of it as giving someone a key to your house, but not the deed.
🎯 Why Use Proxies?
┌─────────────────────────────────────────────────────────┐
│ OWNER (Alice) │
│ Controls Everything │
└────────────┬───────────────┬────────────────────────────┘
│ │
│ grants proxy │ grants proxy
│ │
┌──────▼──────┐ ┌───▼──────────┐
│ Gateway │ │ Engineer │
│ (Bob) │ │ (Carol) │
└──────┬──────┘ └───┬──────────┘
│ │
│ can update │ can reorganize
│ sensor data │ node structure
│ │
┌──────▼───────────────▼──────────┐
│ Alice's CPS Tree │
│ ✅ Ownership stays with Alice │
│ ✅ Access is revocable │
│ ✅ Actions are auditable │
└─────────────────────────────────┘
Use Cases:
- 🌡️ IoT Device Management: Gateways update sensor data without owner keys
- 👥 Team Collaboration: Multiple engineers manage department nodes
- 🤖 Automated Systems: Bots trigger updates based on external events
- ⏰ Temporary Access: Time-limited permissions for contractors
- 🏢 Hierarchical Delegation: Department leads manage their subtrees
🌐 User Story: IoT Sensor Network
Scenario: Alice owns temperature sensors. She wants her IoT gateway to update readings without risking her account.
The Setup
┌─────────────────────────────────────────────────────────┐
│ Step 1: Alice creates sensor hierarchy │
├─────────────────────────────────────────────────────────┤
│ │
│ Building A Sensors (NodeId 0) │
│ └── Room 101 Temp Sensor (NodeId 1) │
│ 📊 Current: 22.5°C │
│ │
│ Owner: Alice (5GrwvaEF...) │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Step 2: Alice grants proxy to Gateway │
├─────────────────────────────────────────────────────────┤
│ │
│ Proxy::add_proxy( │
│ owner: Alice, │
│ delegate: Gateway, │
│ type: CpsWrite(None), ← Only CPS operations │
│ delay: 0 blocks ← Immediately active │
│ ) │
│ │
│ ✅ Gateway can now update CPS nodes │
│ ❌ Gateway CANNOT transfer Alice's funds │
│ ❌ Gateway CANNOT change ownership │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Step 3: Gateway updates sensor (acting as Alice) │
├─────────────────────────────────────────────────────────┤
│ │
│ Proxy::proxy( │
│ signer: Gateway, │
│ real_account: Alice, │
│ call: Cps::set_payload(NodeId(1), "23.1°C") │
│ ) │
│ │
│ 📊 Sensor now shows: 23.1°C │
│ 👤 Owner still: Alice │
│ 📝 Event logged: Gateway acted on Alice's behalf │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Step 4: Alice revokes access when done │
├─────────────────────────────────────────────────────────┤
│ │
│ Proxy::remove_proxy(Alice, Gateway, ...) │
│ │
│ ❌ Gateway can no longer update nodes │
│ ✅ Alice retains full control │
└─────────────────────────────────────────────────────────┘
🎭 Proxy Types Comparison
| Proxy Type |
Scope |
Best For |
Security Level |
CpsWrite(None) |
All CPS nodes |
Trusted gateways, team members |
🟡 Medium - can modify all nodes |
CpsWrite(Some(NodeId)) |
Specific node + children |
Contractors, scoped automation |
🟢 High - limited to subtree |
| Time-delayed (100 blocks) |
Any of above + waiting period |
High-value operations |
🟢 High - review before activation |
📊 Access Control Visualization
Without Proxy (Traditional)
┌──────────────┐
│ Alice's Keys │ ← Single point of failure
└──────┬───────┘
│
├─→ Update Sensor 1
├─→ Update Sensor 2
├─→ Transfer Funds } All or nothing access
├─→ Change Settings }
└─→ Delete Nodes }
With CPS Proxy
┌──────────────┐
│ Alice's Keys │ ← Stays secure
└──────┬───────┘
│
├─→ Gateway Proxy ────→ ✅ Update Sensors only
│ ❌ Cannot transfer funds
│ ❌ Cannot change ownership
│
├─→ Engineer Proxy ───→ ✅ Reorganize tree structure
│ ❌ Cannot delete root nodes
│ ❌ Cannot access other departments
│
└─→ Bot Proxy ────────→ ✅ Set alerts/notifications
❌ Cannot create new nodes
❌ Limited to monitoring subtree
🔐 Security Features
┌───────────────────────────────────────────────────────────┐
│ PROXY SECURITY GUARANTEES │
├───────────────────────────────────────────────────────────┤
│ │
│ 🎯 Type Safety │
│ ProxyType::CpsWrite restricts to CPS operations only │
│ │
│ 🌳 Node-Level Granularity │
│ CpsWrite(Some(id)) limits to specific subtree │
│ │
│ 👤 Ownership Preserved │
│ All operations maintain original owner │
│ │
│ 🔄 Revocable │
│ Owner can remove proxy access anytime │
│ │
│ 📝 Auditable │
│ All proxy actions recorded in blockchain events │
│ │
│ 🚫 No Privilege Escalation │
│ Proxies cannot grant permissions to others │
│ │
│ ⏰ Time-Locked Safety │
│ Delayed proxies allow review before activation │
│ │
└───────────────────────────────────────────────────────────┘
🎯 Common Patterns
Pattern 1: IoT Gateway (Full CPS Access)
Owner grants: CpsWrite(None), delay=0
├─ Gateway can: Update all sensor payloads
├─ Gateway can: Create new device nodes
└─ Gateway cannot: Transfer funds or delete root nodes
Pattern 2: Contractor (Scoped Access)
Owner grants: CpsWrite(Some(NodeId(5))), delay=0
├─ Contractor can: Update node 5 and children
├─ Contractor can: Create nodes under node 5
└─ Contractor cannot: Access any other nodes
Pattern 3: Security Audit (Time-Delayed)
Owner grants: CpsWrite(None), delay=100 blocks
├─ Auditor must wait: ~10 minutes (100 blocks)
├─ Owner can review: Delegation before it activates
└─ Owner can cancel: If delegation seems suspicious
Pattern 4: Team Collaboration (Multiple Proxies)
Owner grants multiple:
├─ Engineer A: CpsWrite(Some(NodeId(10)))
├─ Engineer B: CpsWrite(Some(NodeId(20)))
└─ Manager: CpsWrite(None)
Result: Each team member manages their area
🚀 Quick Setup Guide
1️⃣ Define ProxyType in Your Runtime
Required once per runtime configuration. See full code example in official docs.
pub enum ProxyType {
CpsWrite(Option<NodeId>), // CPS-only access
// ... other types
}
2️⃣ Grant Proxy Access
// Owner grants access to delegate
Proxy::add_proxy(
RuntimeOrigin::signed(owner),
delegate_account,
ProxyType::CpsWrite(None), // or Some(node_id) for scoped
0 // delay in blocks
)
3️⃣ Delegate Performs Action
// Delegate acts on owner's behalf
Proxy::proxy(
RuntimeOrigin::signed(delegate),
owner_account,
None,
Box::new(RuntimeCall::Cps(Call::set_payload { ... }))
)
4️⃣ Revoke When Done
// Owner removes access
Proxy::remove_proxy(
RuntimeOrigin::signed(owner),
delegate_account,
ProxyType::CpsWrite(None),
0
)
💾 Compact Encoding Benefits
Storage Savings with SCALE Compact
| Node ID Range |
Standard Encoding |
Compact Encoding |
Savings |
| 0-63 |
8 bytes |
1 byte |
87% |
| 64-16,383 |
8 bytes |
2 bytes |
75% |
| 16,384-2,097,151 |
8 bytes |
3 bytes |
62% |
Real-World Impact:
- Smart home with 50 devices: ~350 bytes saved in node IDs alone
- Industrial facility with 1,000 sensors: ~7 KB saved
- City-scale IoT network with 100,000 nodes: ~700 KB saved
🎓 Advanced Example: Industrial IoT
Scenario: Factory Monitoring System
Factory (root)
├─ Production Line A
│ ├─ Robot Arm 1
│ │ ├─ Motor Controller (encrypted diagnostics)
│ │ └─ Gripper Sensor
│ └─ Quality Camera (encrypted RTSP stream)
└─ Production Line B
├─ Conveyor Belt
└─ Weight Sensor (plain telemetry)
Implementation
// 1. Create factory root
let factory_meta = NodeData::Plain(
BoundedVec::try_from(br#"{"type":"facility","name":"Factory Berlin","location":"DE"}"#.to_vec()).unwrap()
);
Cps::create_node(origin. clone(), None, Some(factory_meta), None)?;
let factory_id = NodeId(0);
// 2. Add production line
let line_meta = NodeData::Plain(
BoundedVec::try_from(br#"{"type":"production_line","name":"Line A","capacity":1000}"#.to_vec()).unwrap()
);
Cps::create_node(origin.clone(), Some(factory_id), Some(line_meta), None)?;
let line_id = NodeId(1);
// 3. Add robot with encrypted maintenance logs
let robot_meta = NodeData::Plain(
BoundedVec::try_from(br#"{"type":"robot","model":"KUKA KR 6","serial":"ABC123"}"#.to_vec()).unwrap()
);
let encrypted_logs = NodeData::Encrypted(
DefaultEncryptedData::AesGcm256(encrypted_maintenance_data)
);
Cps::create_node(origin.clone(), Some(line_id), Some(robot_meta), Some(encrypted_logs))? ;
let robot_id = NodeId(2);
// 4. Robot sends hourly diagnostics
let diagnostics = NodeData::Encrypted(
DefaultEncryptedData::XChaCha20Poly1305(encrypted_diagnostic_report)
);
Cps::set_payload(origin. clone(), robot_id, Some(diagnostics))?;
// → Triggers OnPayloadSet for monitoring system
// 5. Reorganize: Move robot to Line B
Cps::move_node(origin, robot_id, NodeId(5))?;
// → All descendant paths updated automatically
🧪 Testing Your CPS
Query the Tree
// Get a node
let node = Nodes::<T>::get(NodeId(2)).ok_or(Error::<T>::NodeNotFound)?;
println!("Owner: {:?}", node.owner);
println!("Path: {:?}", node.path);
// Get all children
let children = NodesByParent::<T>::get(NodeId(0));
println!("Children: {:?}", children);
// Get all root nodes
let roots = RootNodes::<T>::get();
println!("Root nodes: {:?}", roots);
// Check if node is ancestor (O(1))
let is_ancestor = node.path.contains(&NodeId(0));
🚀 Getting Started
1. Add to Your Runtime
impl pallet_robonomics_cps::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type MaxTreeDepth = ConstU32<16>;
type MaxChildrenPerNode = ConstU32<100>;
type MaxRootNodes = ConstU32<1000>;
type EncryptedData = pallet_robonomics_cps::DefaultEncryptedData;
type OnPayloadSet = (); // Or your custom handler
type WeightInfo = ();
}
2. Create Your First CPS
// In your extrinsic or test
let meta = NodeData::Plain(
BoundedVec::try_from(b"My First CPS". to_vec()).unwrap()
);
Cps::create_node(origin, None, Some(meta), None)?;
3. Explore the Implementation
📂 Source Code: [frame/cps/src/lib.rs](https://github.com/airalab/robonomics/blob/feat/cps1_0/frame/cps/src/lib. rs)
🌿 Branch: feat/cps1_0
📚 Summary
pallet-robonomics-cps provides:
✅ Hierarchical CPS registry with O(1) performance
✅ Flexible plain/encrypted data storage
✅ Strong ownership-based security model
✅ Configurable limits for safety and scalability
✅ Runtime hooks for extensibility
✅ Compact encoding for storage efficiency
✅ Support for multiple encryption algorithms
✅ Safe tree operations (move, delete) with automatic path updates
Perfect for: Smart homes, sensor networks, robotics, industrial IoT, and any hierarchical cyber-physical system that needs decentralized, privacy-aware on-chain coordination.
Tasks
Pallet
CLI & Library
Integration tests
Indexer
🏗️ Cyber-Physical System Pallet
pallet-robonomics-cpsis a Substrate pallet providing an efficient, secure, and minimal hierarchical data model (tree/forest) for representing Cyber-Physical Systems (CPS) on-chain.🎯 What Can You Build?
✨ Key Features
📖 Quick Start: Building a Smart Home
Let's build a simple smart home CPS step-by-step:
Step 1: Create the Home (Root Node)
Step 2: Add Kitchen Room
Step 3: Add Smart Lamp with Encrypted Config
Step 4: Update Sensor Data
Step 5: Move a Device
🏛️ Architecture Overview
🗂️ Data Structures Explained
Node Structure
Example Node:
{ "parent": 1, "owner": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", "path": [0, 1], "meta": { "Plain": "{\"type\":\"sensor\",\"name\":\"Temp-01\"}" }, "payload": { "Plain": "{\"temp_c\":23.1,\"timestamp\":1733011200}" } }NodeData: Plain vs Encrypted
NodeData::Plain(bytes)NodeData::Encrypted(DefaultEncryptedData::XChaCha20Poly1305(ciphertext)){"type":"device"}0x8f3a2...(encrypted blob)Encryption Algorithms Supported
🔧 Core Operations (Extrinsics)
1️⃣
create_node- Add Node to TreeSignature:
Use Cases:
parent_id = None→ Create a new CPS tree (root node)parent_id = Some(id)→ Add child to existing nodeRules:
Nodes,NodesByParent, andRootNodesstorage2️⃣
set_meta- Update Node MetadataSignature:
Example:
3️⃣
set_payload- Update Node PayloadSignature:
Use Cases:
Special: Triggers
OnPayloadSetcallback for custom runtime logic4️⃣
move_node- Reorganize TreeSignature:
Example:
Safety Guarantees:
5️⃣
delete_node- Remove NodeSignature:
Rules:
NodesByParent,RootNodes)🔐 Security Model
Ownership-Based Access Control
Invariants Enforced
✅ No Cycles: Path-based O(1) cycle detection
✅ Ownership Inheritance: Children always have parent's owner
✅ Index Consistency: All storage items stay synchronized
✅ Safe Deletion: Nodes with children cannot be deleted
✅ Depth Limits: Tree depth never exceeds
MaxTreeDepth🎭 Privacy: Plain vs Encrypted
Visibility Comparison
Mixed Privacy Example
Result:
rtsp://admin:[email protected]:554/stream📊 Storage Layout
Three Storage Items
Path-Based Performance
Traditional Tree (Recursive):
CPS Pallet (Path-Based):
🔗 Runtime Hooks: OnPayloadSet
Use Cases for Callbacks
Example Implementations:
Configuration:
🔑 Proxy-Based Access Delegation
The pallet integrates with Substrate's
pallet-proxyto enable delegated access without transferring ownership. Think of it as giving someone a key to your house, but not the deed.🎯 Why Use Proxies?
Use Cases:
🌐 User Story: IoT Sensor Network
Scenario: Alice owns temperature sensors. She wants her IoT gateway to update readings without risking her account.
The Setup
🎭 Proxy Types Comparison
CpsWrite(None)CpsWrite(Some(NodeId))📊 Access Control Visualization
Without Proxy (Traditional)
With CPS Proxy
🔐 Security Features
🎯 Common Patterns
Pattern 1: IoT Gateway (Full CPS Access)
Pattern 2: Contractor (Scoped Access)
Pattern 3: Security Audit (Time-Delayed)
Pattern 4: Team Collaboration (Multiple Proxies)
🚀 Quick Setup Guide
1️⃣ Define ProxyType in Your Runtime
Required once per runtime configuration. See full code example in official docs.
2️⃣ Grant Proxy Access
3️⃣ Delegate Performs Action
4️⃣ Revoke When Done
💾 Compact Encoding Benefits
Storage Savings with SCALE Compact
Real-World Impact:
🎓 Advanced Example: Industrial IoT
Scenario: Factory Monitoring System
Implementation
🧪 Testing Your CPS
Query the Tree
🚀 Getting Started
1. Add to Your Runtime
2. Create Your First CPS
3. Explore the Implementation
📂 Source Code: [
frame/cps/src/lib.rs](https://github.com/airalab/robonomics/blob/feat/cps1_0/frame/cps/src/lib. rs)🌿 Branch:
feat/cps1_0📚 Summary
pallet-robonomics-cps provides:
✅ Hierarchical CPS registry with O(1) performance
✅ Flexible plain/encrypted data storage
✅ Strong ownership-based security model
✅ Configurable limits for safety and scalability
✅ Runtime hooks for extensibility
✅ Compact encoding for storage efficiency
✅ Support for multiple encryption algorithms
✅ Safe tree operations (move, delete) with automatic path updates
Perfect for: Smart homes, sensor networks, robotics, industrial IoT, and any hierarchical cyber-physical system that needs decentralized, privacy-aware on-chain coordination.
Tasks
Pallet
CLI & Library
acl/proxysupport into CLI application / libraryrwssupport into CLI application / libraryIntegration tests
Indexer