Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
29 changes: 29 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ members = [
"frame/digital-twin",
"frame/xcm-info",
"frame/cps",
"frame/cps/rpc",
"frame/cps/rpc/runtime-api",
"frame/wrapped-asset",
"runtime/robonomics",
"tools/libcps",
Expand Down
10 changes: 10 additions & 0 deletions frame/cps/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ parity-scale-codec = { workspace = true }
sp-runtime = { workspace = true }
sp-std = { workspace = true }
sp-core = { workspace = true }
sp-io = { workspace = true }
log = { workspace = true }
serde = { workspace = true, optional = true }
hex = { workspace = true, optional = true }
frame-system = { workspace = true }
frame-support = { workspace = true }
frame-benchmarking = { workspace = true, optional = true }
Expand All @@ -22,6 +26,7 @@ frame-benchmarking = { workspace = true, optional = true }
sp-io = { workspace = true }
pallet-balances = { workspace = true, features = ["std"] }
pallet-proxy = { workspace = true, features = ["std"] }
serde_json = { workspace = true }

[features]
default = ["std"]
Expand All @@ -30,12 +35,17 @@ std = [
"sp-runtime/std",
"sp-std/std",
"sp-core/std",
"sp-io/std",
"log/std",
"serde?/std",
"frame-system/std",
"frame-support/std",
"frame-benchmarking?/std",
"scale-info/std",
]

offchain-indexer = ["serde", "hex"]

runtime-benchmarks = [
"frame-benchmarking",
"frame-support/runtime-benchmarks",
Expand Down
149 changes: 148 additions & 1 deletion frame/cps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -988,20 +988,167 @@ await api.tx.cps.moveNode(nodeId, newParentId).signAndSend(account);
| **IPFS + DB** | Decentralized storage | No ownership enforcement | Content distribution |
| **ERC-721 NFTs** | Standard, composable | Gas-expensive, limited structure | Digital collectibles |

## Offchain Worker Indexer (Optional Feature)

The CPS pallet includes an optional offchain worker that collects historical data and exposes it via RPC API.

### Architecture

The offchain worker runs in the background and:
1. Monitors on-chain CPS events (node creation, updates, deletions)
2. Indexes events with timestamps in offchain storage
3. Makes historical data queryable via Runtime API and RPC endpoints

### Enabling the Feature

Add the `offchain-worker` feature flag to enable indexing:

```toml
pallet-robonomics-cps = {
default-features = false,
path = "../frame/cps",
features = ["offchain-worker"]
}
```

### Storage Structure

The indexer stores three types of records in offchain storage:

1. **Meta Records**: `cps::meta::<timestamp>` - Metadata updates
2. **Payload Records**: `cps::payload::<timestamp>` - Payload updates
3. **Node Operations**: `cps::operations::<timestamp>` - Node lifecycle events

All records include:
- Unix timestamp (u64)
- Associated data (Vec<u8>)
- Operation type (for node operations)

### Runtime API Integration

Implement the Runtime API in your runtime:

```rust
impl pallet_robonomics_cps_rpc_runtime_api::CpsIndexerApi<Block> for Runtime {
fn get_meta_records(from: u64, to: u64) -> Vec<(u64, Vec<u8>)> {
pallet_robonomics_cps::offchain::storage::get_meta_records(from, to)
}

fn get_payload_records(from: u64, to: u64) -> Vec<(u64, Vec<u8>)> {
pallet_robonomics_cps::offchain::storage::get_payload_records(from, to)
}

fn get_node_operations(from: u64, to: u64) -> Vec<(u64, Vec<u8>, Vec<u8>)> {
pallet_robonomics_cps::offchain::storage::get_node_operations(from, to)
}
}
```

### RPC Extension Setup

Add the RPC extension to your node's RPC handler:

```rust
use pallet_robonomics_cps_rpc::{CpsIndexerRpc, CpsIndexerRpcApiServer};

// In your RPC setup
let cps_rpc = CpsIndexerRpc::new(client.clone());
io.merge(cps_rpc.into_rpc())?;
```

### RPC Endpoints

Query historical data via JSON-RPC:

**Get meta records:**
```bash
curl -H "Content-Type: application/json" \
-d '{"id":1, "jsonrpc":"2.0", "method":"cps_getMetaRecords", "params":[1704067200, 1704153600]}' \
http://localhost:9933
```

**Get payload records:**
```bash
curl -H "Content-Type: application/json" \
-d '{"id":1, "jsonrpc":"2.0", "method":"cps_getPayloadRecords", "params":[1704067200, 1704153600]}' \
http://localhost:9933
```

**Get node operations:**
```bash
curl -H "Content-Type: application/json" \
-d '{"id":1, "jsonrpc":"2.0", "method":"cps_getNodeOperations", "params":[1704067200, 1704153600]}' \
http://localhost:9933
```

### Response Format

All RPC methods return arrays of timestamped records:

```json
{
"jsonrpc": "2.0",
"result": [
{
"timestamp": 1704067200,
"data": "0x48656c6c6f20576f726c64"
},
{
"timestamp": 1704070800,
"data": "0x54657374204461746131"
}
],
"id": 1
}
```

Node operations additionally include an `operationType` field:

```json
{
"timestamp": 1704067200,
"operationType": "create",
"data": "0x..."
}
```

### Benefits

- **Historical Analysis**: Query past system states and changes
- **Audit Trails**: Track when nodes were created/modified
- **Monitoring**: Build dashboards showing system activity over time
- **Debugging**: Investigate issues by reviewing historical events
- **Compliance**: Maintain queryable records for regulatory requirements

### Performance Considerations

- Offchain storage is node-local (not replicated across the network)
- Storage size grows with activity; implement pruning if needed
- Queries are bounded by time range to prevent excessive iteration
- RPC calls are non-blocking and don't affect chain performance
- **Important**: Current implementation iterates through every timestamp in range. For production use with large time ranges, add bounds checking in the Runtime API implementation to prevent potential DoS via excessive queries.

### Security Considerations

- **Public RPC Endpoints**: The RPC endpoints are publicly accessible. Consider implementing rate limiting at the node level for production deployments.
- **No Data Encryption**: Offchain storage is not encrypted, but this is acceptable since indexed data comes from public on-chain events.
- **No Consensus Impact**: Offchain worker operations are isolated and don't affect chain consensus.
- **Local Storage**: Data is stored locally per node, not broadcast to the network.

## Roadmap

**Current (v1):**
- ✅ Hierarchical tree with cycle prevention
- ✅ Plain and encrypted data storage
- ✅ O(1) operations via path storage
- ✅ Compact encoding for efficiency
- ✅ Offchain worker indexer with RPC API (optional feature)

**Planned (v2):**
- 🔮 Multi-owner nodes with role-based permissions
- 🔮 Node templates for rapid deployment
- 🔮 Batch operations for bulk updates
- 🔮 Additional encryption algorithms (AES-GCM, ChaCha20)
- 🔮 Off-chain worker integration for automated maintenance

## Technical Documentation

Expand Down
31 changes: 31 additions & 0 deletions frame/cps/rpc/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[package]
name = "pallet-robonomics-cps-rpc"
description = "RPC extension for Robonomics CPS Indexer"
version = "0.1.0"
authors.workspace = true
edition.workspace = true
homepage.workspace = true
repository.workspace = true
license = "Apache-2.0"

[dependencies]
jsonrpsee = { workspace = true, features = ["client-core", "server", "macros"] }
serde = { workspace = true, features = ["derive"] }
hex = { workspace = true }
parity-scale-codec = { workspace = true }
sp-api = { workspace = true }
sp-blockchain = { workspace = true }
sp-runtime = { workspace = true }
pallet-robonomics-cps = { path = "..", default-features = false, features = ["offchain-indexer"] }
pallet-robonomics-cps-rpc-runtime-api = { path = "./runtime-api", default-features = false }

[features]
default = ["std"]
std = [
"parity-scale-codec/std",
"sp-api/std",
"sp-runtime/std",
"pallet-robonomics-cps/std",
"pallet-robonomics-cps-rpc-runtime-api/std",
]
offchain-indexer = ["pallet-robonomics-cps/offchain-indexer"]
24 changes: 24 additions & 0 deletions frame/cps/rpc/runtime-api/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "pallet-robonomics-cps-rpc-runtime-api"
description = "Runtime API for Robonomics CPS Indexer"
version = "0.1.0"
authors.workspace = true
edition.workspace = true
homepage.workspace = true
repository.workspace = true
license = "Apache-2.0"

[dependencies]
parity-scale-codec = { workspace = true }
sp-api = { workspace = true }
sp-std = { workspace = true }
pallet-robonomics-cps = { path = "../..", default-features = false }

[features]
default = ["std"]
std = [
"parity-scale-codec/std",
"sp-api/std",
"sp-std/std",
"pallet-robonomics-cps/std",
]
68 changes: 68 additions & 0 deletions frame/cps/rpc/runtime-api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
///////////////////////////////////////////////////////////////////////////////
//
// Copyright 2018-2025 Robonomics Network <research@robonomics.network>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
///////////////////////////////////////////////////////////////////////////////
//! Runtime API for CPS Offchain Indexer
//!
//! This API provides access to historical CPS data collected by the offchain worker.

#![cfg_attr(not(feature = "std"), no_std)]

use sp_std::vec::Vec;

// Re-export the storage types for use in runtime API implementations
pub use pallet_robonomics_cps::NodeId;

#[cfg(feature = "std")]
pub use pallet_robonomics_cps::offchain::storage::{MetaRecord, PayloadRecord, NodeOperation};

sp_api::decl_runtime_apis! {
/// Runtime API for querying indexed CPS data
pub trait CpsIndexerApi {
/// Get meta records within optional time range
///
/// # Arguments
/// * `from` - Start timestamp (inclusive), None for all
/// * `to` - End timestamp (inclusive), None for all
/// * `node_id` - Optional node_id filter
///
/// # Returns
/// Vector of encoded MetaRecord structures
fn get_meta_records(from: Option<u64>, to: Option<u64>, node_id: Option<NodeId>) -> Vec<Vec<u8>>;

/// Get payload records within optional time range
///
/// # Arguments
/// * `from` - Start timestamp (inclusive), None for all
/// * `to` - End timestamp (inclusive), None for all
/// * `node_id` - Optional node_id filter
///
/// # Returns
/// Vector of encoded PayloadRecord structures
fn get_payload_records(from: Option<u64>, to: Option<u64>, node_id: Option<NodeId>) -> Vec<Vec<u8>>;

/// Get node operations within optional time range
///
/// # Arguments
/// * `from` - Start timestamp (inclusive), None for all
/// * `to` - End timestamp (inclusive), None for all
/// * `node_id` - Optional node_id filter
///
/// # Returns
/// Vector of encoded NodeOperation structures
fn get_node_operations(from: Option<u64>, to: Option<u64>, node_id: Option<NodeId>) -> Vec<Vec<u8>>;
}
}
Loading