Skip to content

Commit 78a6481

Browse files
feat(namespaces): initial impl (#105)
This is the initial implementation for namespaces. Please look into https://github.com/renlabs-dev/torus-substrate/blob/feat/namespaces-initial-impl/docs/namespace.md. Closes CHAIN-97.
1 parent 87cb3d3 commit 78a6481

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1893
-862
lines changed

Cargo.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ num-traits = { version = "0.2.19", default-features = false, features = [
5353
] }
5454
bitflags = { version = "2.9.1", default-features = false }
5555
rand = { version = "0.9.1", default-features = false }
56+
libm = { version = "0.2.15", default-features = false }
5657

5758
# Frontier / EVM
5859
fc-api = { git = "https://github.com/paritytech/frontier.git", rev = "b9b1c620c8b418bdeeadc79725f9cfa4703c0333" }

docs/namespace.md

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# Torus Namespaces: Integrating Off-chain Agent Capabilities with the Chain Permission System
2+
3+
## Overview
4+
5+
The namespace system provides a hierarchical tree naming structure for the Torus Protocol, enabling agents to organize and delegate access to their off-chain services. Think of it as a decentralized DNS where agents control their own namespace trees and can share branches with others through the permission system, delegating authority over APIs.
6+
7+
An agent running a Twitter memory service might register agent.alice.memory.twitter, while another agent providing market data could own agent.bob.data.markets.crypto. These dot-separated paths create a natural hierarchy that mirrors how we think about organizing resources, with agents serving as the root nodes of their namespace trees.
8+
9+
The system emerges from a practical need, as agents begin offering specialized off-chain services, they need a way to organize these services and delegate access to specific components.
10+
11+
## Namespace Paths
12+
13+
Every namespace follows a hierarchical dot-separated format with strict validation rules. Each segment can contain alphanumeric characters, hyphens, and underscores, with a maximum length of 63 characters per segment.
14+
15+
```rust
16+
pub struct NamespacePath {
17+
inner: BoundedVec<u8, ConstU32<MAX_NAMESPACE_PATH_LENGTH>>,
18+
}
19+
```
20+
21+
The path validation ensures consistency across the network:
22+
23+
- Maximum 255 bytes total length
24+
- Maximum 10 segments (depth limitation)
25+
- Each segment between 1-63 characters
26+
- Valid characters: unicode alphanumerics, `-`, `_`
27+
28+
This structure creates clear ownership: `agent.alice` owns all paths under that prefix, from `agent.alice.api` to `agent.alice.memory.twitter.v2`. The depth limitation prevents excessive nesting while still allowing meaningful organization.
29+
30+
## Design Philosophy
31+
32+
Initially, we considered complex tree structures like Patricia tries or custom prefix trees that would enable on-chain traversal. However, analyzing actual usage, we see that almost all operations are simple existence checks.
33+
34+
When an off-chain service receives a request, it needs to verify that a namespace exists and check permissions. Both operations are direct lookups. The service already knows the exact path it's checking, there's no need for prefix searches or tree traversal on-chain.
35+
36+
```rust
37+
pub type Namespaces<T: Config> = StorageDoubleMap<
38+
_,
39+
Blake2_128Concat,
40+
T::AccountId,
41+
Blake2_128Concat,
42+
NamespacePath,
43+
NamespaceMetadata<T>,
44+
>;
45+
```
46+
47+
By using a double map with the agent as the first key and the full path as the second, we achieve O(1) lookups for all common operations. Agent-level enumeration remains efficient, and the storage structure is straightforward to understand and maintain.
48+
49+
## Economic Model
50+
51+
The namespace pricing model balances accessibility with spam prevention through a simple yet effective fee structure. Rather than the sigmoid curve described in early designs, the implemented system uses a base fee that goes to the treasury plus a refundable deposit based on storage consumption.
52+
53+
```rust
54+
pub struct NamespacePricingConfig<T: Config> {
55+
pub deposit_per_byte: BalanceOf<T>,
56+
pub base_fee: BalanceOf<T>,
57+
pub count_midpoint: u32,
58+
pub fee_steepness: Percent,
59+
pub max_fee_multiplier: u32,
60+
}
61+
```
62+
63+
Currently, the fee calculation returns a flat base fee, though the structure allows other pricing engines in the future. The deposit ensures that agents must lock tokens proportional to the storage they consume, which are returned when the namespace is deleted.
64+
65+
This approach creates natural incentives. Agents think carefully about namespace creation since deposits are locked. The base fee contributes to the treasury, funding network development. Storage deposits scale linearly with path length, discouraging excessively long names.
66+
67+
## Storage Architecture
68+
69+
Each namespace stores minimal metadata:
70+
71+
```rust
72+
pub struct NamespaceMetadata<T: Config> {
73+
pub created_at: BlockNumberFor<T>,
74+
pub deposit: BalanceOf<T>,
75+
}
76+
```
77+
78+
This approach means each namespace consumes minimal storage. `deposit` tracks the locked amount for refunds.
79+
80+
## Creating Namespaces
81+
82+
When creating a deep path like `agent.alice.memory.twitter.v2`, the system automatically creates any missing intermediate namespaces. This saves users from manual step-by-step creation while ensuring the tree remains consistent.
83+
84+
```rust
85+
fn create_namespace(origin: OriginFor<T>, path: Vec<u8>) -> DispatchResult;
86+
```
87+
88+
> This extrinsic lives inside the Torus0 pallet.
89+
90+
The algorithm determines which parent paths need creation by checking from the deepest level upward. It calculates the total fee and deposit required, processes payment atomically, then creates all namespaces in a single transaction.
91+
92+
## Deletion Strategy
93+
94+
The deletion process ensures that namespaces with active permission delegations cannot be deleted, preventing disruption to services depending on those paths. It automatically removes all child namespaces, maintaining tree consistency. All deposits are refunded to the owner, making deletion economically neutral beyond the initial fee.
95+
96+
```rust
97+
fn delete_namespace(origin: OriginFor<T>, path: Vec<u8>) -> DispatchResult;
98+
```
99+
100+
> This extrinsic lives inside the Torus0 pallet.
101+
102+
## Permission Integration
103+
104+
Namespaces gain their true power through integration with the permission system. An agent can delegate access to specific namespace paths or entire subtrees, enabling access control for off-chain services.
105+
106+
```rust
107+
pub struct NamespaceScope<T: Config> {
108+
pub paths: BoundedBTreeSet<NamespacePath, T::MaxNamespacesPerPermission>,
109+
}
110+
```
111+
112+
The namespace permission scope contains a set of paths that the grantee can access. The permission system's existing infrastructure handles the complexity of duration, revocation terms, and enforcement authorities. This means namespace permissions can be temporary, require multi-signature revocation, or include third-party controllers. Read more in [permission0.md](permission0.md).
113+
114+
This integration creates composition possibilities. An agent running a data aggregation service could delegate read access to `agent.alice.data.public` while keeping `agent.alice.data.private` restricted, or delegate the entire data scope: `agent.alice.data`. The delegation could be time-limited, revocable by designated arbiters, or controlled by enforcement authorities who verify off-chain conditions.
115+
116+
## Practical Applications
117+
118+
A memory service agent registers `agent.memory` and creates specialized sub-namespaces like `agent.memory.twitter`, `agent.memory.discord`, and `agent.memory.telegram`. Each represents a different data source with potentially different access requirements. The agent can delegate read access to `agent.memory.twitter` to analytics agents while keeping other sources private.
119+
120+
A compute marketplace might use `agent.compute.gpu.nvidia.a100` to represent specific hardware resources. Delegating this namespace grants access to submit jobs to those specific GPUs. The hierarchical structure naturally represents the hardware taxonomy while permissions control access.
121+
122+
API versioning is viable with paths like `agent.api.v1` and `agent.api.v2`. Services can maintain backward compatibility by keeping old namespaces active while encouraging migration to newer versions. Permissions can be time-limited to enforce deprecation schedules.
123+
124+
Data feeds benefit from hierarchical organization: `agent.data.markets.crypto.btc.price` clearly indicates the data type and source. Subscribers can receive permissions for specific data points or entire categories, with granular control over access duration and revocation.
125+
126+
## Implementation Trade-offs
127+
128+
By optimizing for direct lookups, we sacrificed on-chain traversal capabilities. Services cannot efficiently query "all namespaces under `agent.alice.memory`" without iterating through all of Alice's namespaces. This pushes complexity to off-chain indexers, which can build specialized data structures for such queries. Also, we don't expect huge amounts of namespaces from the beginning, and the pricing mechanism should counter that problem to a certain degree.
129+
130+
Storage efficiency took precedence over feature richness. Each namespace stores minimal metadata rather than extensive configuration. This keeps the on-chain footprint small but means additional features require off-chain coordination or separate storage.
131+
132+
The flat fee structure, while simple, doesn't capture the true cost difference between shallow and deep namespaces. This may be refined in future versions as usage patterns emerge and economic requirements become clearer.
133+
134+
## Future Evolution
135+
136+
The namespace system's design anticipates future growth while maintaining backward compatibility. The versioned storage pattern allows seamless upgrades if requirements change. Several enhancements are possible without breaking existing namespaces:
137+
138+
The pricing configuration structure already supports the sigmoid-based fee calculation described in the original design. As the network grows and usage patterns emerge, this more sophisticated pricing can be enabled to better balance accessibility with resource consumption.
139+
140+
The metadata structure could be extended to include additional fields like expiration times, usage counters, or permission defaults. The storage migration system makes such upgrades straightforward.
141+
142+
Off-chain indexing services will likely emerge to provide sophisticated query capabilities. These could offer GraphQL APIs for namespace exploration, real-time updates via WebSocket, and specialized search functionality.
143+
144+
## Security Considerations
145+
146+
We will need a anti-spam system to emerge in the near future. The current version will, however, allow curators to delete/toggle namespaces.
147+
148+
The delegation check during deletion prevents a class of griefing attacks where namespace owners could disrupt dependent services. By requiring delegations to be revoked before deletion, services have warning and can negotiate continued access or migration paths.
149+
150+
Path validation prevents injection attacks and ensures consistent parsing across different implementations. The character restrictions and length limits bound resource consumption while allowing meaningful names.
151+
152+
The economic model creates natural spam resistance. The combination of base fees and storage deposits means namespace squatting has real costs. The treasury receives fees from creation, funding network development rather than enriching early adopters.

pallets/emission0/src/benchmarking.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ mod benchmarks {
2626
<T::Torus>::force_register_agent(&module_key2, vec![], vec![], vec![])
2727
.expect("failed to register agent");
2828

29+
<T::Governance>::force_set_whitelisted(&module_key);
30+
<T::Governance>::force_set_whitelisted(&module_key2);
31+
2932
<T::Governance>::set_allocator(&module_key2);
3033
let _ = <T::Currency>::deposit_creating(&module_key2, <T::Torus>::min_validator_stake());
3134
<T::Torus>::force_set_stake(
@@ -51,6 +54,9 @@ mod benchmarks {
5154
<T::Torus>::force_register_agent(&module_key2, vec![], vec![], vec![])
5255
.expect("failed to register agent");
5356

57+
<T::Governance>::force_set_whitelisted(&module_key);
58+
<T::Governance>::force_set_whitelisted(&module_key2);
59+
5460
<T::Governance>::set_allocator(&module_key2);
5561

5662
#[extrinsic_call]
@@ -67,6 +73,9 @@ mod benchmarks {
6773
<T::Torus>::force_register_agent(&module_key2, vec![], vec![], vec![])
6874
.expect("failed to register agent");
6975

76+
<T::Governance>::force_set_whitelisted(&module_key);
77+
<T::Governance>::force_set_whitelisted(&module_key2);
78+
7079
<T::Governance>::set_allocator(&module_key);
7180
<T::Governance>::set_allocator(&module_key2);
7281

pallets/emission0/src/distribute.rs

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use polkadot_sdk::{
2222
sp_tracing::{error, info},
2323
};
2424

25-
use crate::{BalanceOf, Config, ConsensusMember, IncentivesRatio, Weights};
25+
use crate::{BalanceOf, Config, ConsensusMember, IncentivesRatio, NegativeImbalanceOf, Weights};
2626

2727
mod math;
2828

@@ -115,7 +115,9 @@ impl<T: Config> ConsensusMemberInput<T> {
115115
pub fn all_members() -> BTreeMap<T::AccountId, ConsensusMemberInput<T>> {
116116
let min_validator_stake = <T::Torus>::min_validator_stake();
117117

118-
let mut registered_agents: BTreeSet<_> = <T::Torus>::agent_ids().collect();
118+
let mut registered_agents: BTreeSet<_> = <T::Torus>::agent_ids()
119+
.filter(<T::Governance>::is_whitelisted)
120+
.collect();
119121
let mut consensus_members: BTreeMap<_, _> = crate::ConsensusMembers::<T>::iter().collect();
120122

121123
let mut inputs: Vec<_> = crate::WeightControlDelegation::<T>::iter()
@@ -209,7 +211,8 @@ impl<T: Config> ConsensusMemberInput<T> {
209211
.unwrap_or_default();
210212

211213
ConsensusMemberInput {
212-
registered: <T::Torus>::is_agent_registered(&agent_id),
214+
registered: <T::Torus>::is_agent_registered(&agent_id)
215+
&& <T::Governance>::is_whitelisted(&agent_id),
213216

214217
agent_id,
215218
validator_permit,
@@ -262,9 +265,7 @@ impl<T: Config> ConsensusMemberInput<T> {
262265
}
263266

264267
#[must_use]
265-
fn linear_rewards<T: Config>(
266-
mut emission: <T::Currency as Currency<T::AccountId>>::NegativeImbalance,
267-
) -> <T::Currency as Currency<T::AccountId>>::NegativeImbalance {
268+
fn linear_rewards<T: Config>(mut emission: NegativeImbalanceOf<T>) -> NegativeImbalanceOf<T> {
268269
let treasury_fee = <T::Governance>::treasury_emission_fee();
269270
if !treasury_fee.is_zero() {
270271
let treasury_fee = treasury_fee.mul_floor(emission.peek());
@@ -365,18 +366,17 @@ fn linear_rewards<T: Config>(
365366
.zip(upscaled_incentives)
366367
.zip(upscaled_dividends)
367368
{
368-
let add_stake =
369-
|staker, mut amount: <T::Currency as Currency<T::AccountId>>::NegativeImbalance| {
370-
<T::Permission0>::accumulate_emissions(
371-
&staker,
372-
&pallet_permission0_api::generate_root_stream_id(&staker),
373-
&mut amount,
374-
);
375-
376-
let raw_amount = amount.peek();
377-
T::Currency::resolve_creating(&staker, amount);
378-
let _ = <T::Torus>::stake_to(&staker, &input.agent_id, raw_amount);
379-
};
369+
let add_stake = |staker, mut amount: NegativeImbalanceOf<T>| {
370+
<T::Permission0>::accumulate_emissions(
371+
&staker,
372+
&pallet_permission0_api::generate_root_stream_id(&staker),
373+
&mut amount,
374+
);
375+
376+
let raw_amount = amount.peek();
377+
T::Currency::resolve_creating(&staker, amount);
378+
let _ = <T::Torus>::stake_to(&staker, &input.agent_id, raw_amount);
379+
};
380380

381381
if dividend.peek() != 0 {
382382
let fixed_dividend = dividend.peek();
@@ -417,12 +417,12 @@ fn linear_rewards<T: Config>(
417417
}
418418

419419
struct Emissions<T: Config> {
420-
dividends: Vec<<T::Currency as Currency<T::AccountId>>::NegativeImbalance>,
421-
incentives: Vec<<T::Currency as Currency<T::AccountId>>::NegativeImbalance>,
420+
dividends: Vec<NegativeImbalanceOf<T>>,
421+
incentives: Vec<NegativeImbalanceOf<T>>,
422422
}
423423

424424
fn compute_emissions<T: Config>(
425-
emission: &mut <T::Currency as Currency<T::AccountId>>::NegativeImbalance,
425+
emission: &mut NegativeImbalanceOf<T>,
426426
stake: &[FixedU128],
427427
incentives: Vec<FixedU128>,
428428
dividends: Vec<FixedU128>,

pallets/emission0/src/ext.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
use polkadot_sdk::frame_support::traits::Currency;
1+
use polkadot_sdk::{frame_support::traits::Currency, frame_system};
22

3-
pub(super) type BalanceOf<T> = <<T as crate::Config>::Currency as Currency<
4-
<T as polkadot_sdk::frame_system::Config>::AccountId,
5-
>>::Balance;
3+
pub type BalanceOf<T> =
4+
<<T as crate::Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
65

7-
pub(super) type AccountIdOf<T> = <T as polkadot_sdk::frame_system::Config>::AccountId;
6+
pub type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
7+
8+
pub type NegativeImbalanceOf<T> = <<T as crate::Config>::Currency as Currency<
9+
<T as frame_system::Config>::AccountId,
10+
>>::NegativeImbalance;

pallets/emission0/src/lib.rs

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ pub mod pallet {
3131
use frame::prelude::BlockNumberFor;
3232
use frame_system::ensure_signed;
3333
use pallet_governance_api::GovernanceApi;
34-
use pallet_permission0_api::Permission0Api;
34+
use pallet_permission0_api::{Permission0Api, Permission0EmissionApi};
3535
use pallet_torus0_api::Torus0Api;
3636
use polkadot_sdk::sp_std;
3737
use weights::WeightInfo;
@@ -94,21 +94,18 @@ pub mod pallet {
9494

9595
type Currency: Currency<Self::AccountId, Balance = u128> + Send + Sync;
9696

97-
type Torus: Torus0Api<
98-
Self::AccountId,
99-
<Self::Currency as Currency<Self::AccountId>>::Balance,
100-
<Self::Currency as Currency<Self::AccountId>>::NegativeImbalance,
101-
>;
97+
type Torus: Torus0Api<Self::AccountId, BalanceOf<Self>>;
10298

10399
type Governance: GovernanceApi<Self::AccountId>;
104100

105-
type Permission0: Permission0Api<
106-
Self::AccountId,
107-
OriginFor<Self>,
108-
BlockNumberFor<Self>,
109-
crate::BalanceOf<Self>,
110-
<Self::Currency as Currency<Self::AccountId>>::NegativeImbalance,
111-
>;
101+
type Permission0: Permission0Api<OriginFor<Self>>
102+
+ Permission0EmissionApi<
103+
Self::AccountId,
104+
OriginFor<Self>,
105+
BlockNumberFor<Self>,
106+
BalanceOf<Self>,
107+
NegativeImbalanceOf<Self>,
108+
>;
112109

113110
type WeightInfo: WeightInfo;
114111
}

pallets/emission0/src/weight_control.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ pub fn set_weights<T: crate::Config>(
2424
);
2525

2626
ensure!(
27-
<T::Torus>::is_agent_registered(&acc_id),
27+
<T::Torus>::is_agent_registered(&acc_id) && <T::Governance>::is_whitelisted(&acc_id),
2828
crate::Error::<T>::AgentIsNotRegistered
2929
);
3030

@@ -44,7 +44,7 @@ pub fn set_weights<T: crate::Config>(
4444
);
4545

4646
ensure!(
47-
<T::Torus>::is_agent_registered(target),
47+
<T::Torus>::is_agent_registered(target) && <T::Governance>::is_whitelisted(target),
4848
crate::Error::<T>::AgentIsNotRegistered
4949
);
5050
}
@@ -75,12 +75,12 @@ pub fn delegate_weight_control<T: crate::Config>(
7575
);
7676

7777
ensure!(
78-
<T::Torus>::is_agent_registered(&delegator),
78+
<T::Torus>::is_agent_registered(&delegator) && <T::Governance>::is_whitelisted(&delegator),
7979
crate::Error::<T>::AgentIsNotRegistered
8080
);
8181

8282
ensure!(
83-
<T::Torus>::is_agent_registered(&delegatee),
83+
<T::Torus>::is_agent_registered(&delegatee) && <T::Governance>::is_whitelisted(&delegatee),
8484
crate::Error::<T>::AgentIsNotRegistered
8585
);
8686

0 commit comments

Comments
 (0)