Skip to content

Commit beb3529

Browse files
refac(torus0/namespace): require agent prefix (#111)
1 parent a9dba2b commit beb3529

File tree

17 files changed

+778
-399
lines changed

17 files changed

+778
-399
lines changed

.github/workflows/build-runtime.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ jobs:
3737

3838
- name: Build runtime
3939
run: |
40-
cargo build --release --timings --package torus-runtime
40+
echo "Building ${{ startsWith(github.ref, 'refs/tags/runtime/testnet') && 'with testnet feature flag' || 'without testnet feature flag' }}"
41+
cargo build --release --timings --package torus-runtime ${{ startsWith(github.ref, 'refs/tags/runtime/testnet') && '--features testnet' || '' }}
4142
4243
export SHA256SUM=$(sha256sum target/release/wbuild/torus-runtime/torus_runtime.compact.compressed.wasm | cut -d ' ' -f1)
4344
echo Hash of compact and compressed WASM: $SHA256SUM

docs/namespace.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ The path validation ensures consistency across the network:
2323
- Maximum 255 bytes total length
2424
- Maximum 10 segments (depth limitation)
2525
- Each segment between 1-63 characters
26-
- Valid characters: unicode alphanumerics, `-`, `_`
26+
- Segments must begin and end with alphanumerics
27+
- Valid characters: ASCII alphanumerics, `-`, `_`, and `+`
28+
29+
> Implementations MUST NOT assume the character set will be ASCII only forever. Paths are UTF-8 encoded strings.
2730
2831
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.
2932

justfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ install-try-runtime:
5959
cargo install --git https://github.com/paritytech/try-runtime-cli --locked
6060

6161
try-runtime-upgrade-testnet:
62-
cargo build --release --features try-runtime
62+
cargo build --release --features try-runtime,testnet
6363
RUST_BACKTRACE=1 RUST_LOG=info try-runtime --runtime target/release/wbuild/torus-runtime/torus_runtime.compact.compressed.wasm on-runtime-upgrade --blocktime 8000 live --uri wss://api.testnet.torus.network
6464

6565
try-runtime-upgrade-mainnet:

pallets/permission0/src/ext/namespace_impl.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ pub fn grant_namespace_permission_impl<T: Config>(
4545
let paths = paths
4646
.into_iter()
4747
.map(|path| {
48-
let path = NamespacePath::new(&path).map_err(|_| Error::<T>::NamespacePathIsInvalid)?;
48+
let path =
49+
NamespacePath::new_agent(&path).map_err(|_| Error::<T>::NamespacePathIsInvalid)?;
4950
ensure!(
5051
T::Torus::namespace_exists(&grantor, &path),
5152
Error::<T>::NamespaceDoesNotExist

pallets/torus0/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ runtime-benchmarks = [
2525
"pallet-torus0-api/runtime-benchmarks",
2626
]
2727
try-runtime = ["polkadot-sdk/try-runtime", "pallet-torus0-api/try-runtime"]
28-
28+
testnet = []
2929

3030
[dependencies]
3131
codec = { workspace = true, features = ["derive"] }

pallets/torus0/api/Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,9 @@ try-runtime = ["polkadot-sdk/try-runtime"]
1818
[dependencies]
1919
codec = { workspace = true, features = ["derive"] }
2020
scale-info = { workspace = true, features = ["derive"] }
21-
polkadot-sdk = { workspace = true, features = ["sp-runtime"] }
21+
polkadot-sdk = { workspace = true, features = [
22+
"sp-api",
23+
"sp-core",
24+
"sp-runtime",
25+
"sp-std",
26+
] }

pallets/torus0/api/src/lib.rs

Lines changed: 70 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -62,21 +62,28 @@ pub trait Torus0Api<AccountId, Balance> {
6262
/// This might have to increase in the future, but is a good enough default value.
6363
/// If it ends up being formalized, the length can be described as a u8.
6464
pub const MAX_NAMESPACE_PATH_LENGTH: usize = 256;
65-
/// Number of total bytes a segment can contain.
66-
pub const MAX_SEGMENT_LENGTH: usize = 64;
67-
/// Max number of segments in a path.
65+
/// Number of total bytes a segment can contain. 63 plus dot.
66+
pub const MAX_SEGMENT_LENGTH: usize = 63;
67+
/// Max number of segments in a path. In the common prefix case, an agent will have
68+
/// up to 8 levels of depth to use, 2 being allocated to the agent prefix notation.
6869
pub const MAX_NAMESPACE_SEGMENTS: usize = 10;
6970

7071
pub const NAMESPACE_SEPARATOR: u8 = b'.';
72+
pub const NAMESPACE_AGENT_PREFIX: &[u8] = b"agent.";
7173

7274
pub type NamespacePathInner = BoundedVec<u8, ConstU32<{ MAX_NAMESPACE_PATH_LENGTH as u32 }>>;
7375

7476
#[derive(Encode, Decode, Clone, PartialEq, Eq, PartialOrd, Ord, TypeInfo, MaxEncodedLen)]
7577
pub struct NamespacePath(NamespacePathInner);
7678

7779
impl NamespacePath {
80+
/// The root agent namespace entry.
81+
pub fn agent_root() -> NamespacePath {
82+
NamespacePath(b"agent".to_vec().try_into().unwrap())
83+
}
84+
7885
/// Create a new namespace path from bytes with validation
79-
pub fn new(bytes: &[u8]) -> Result<Self, &'static str> {
86+
pub fn new_agent(bytes: &[u8]) -> Result<Self, &'static str> {
8087
if bytes.is_empty() {
8188
return Err("empty namespace path");
8289
}
@@ -85,6 +92,10 @@ impl NamespacePath {
8592
return Err("path too long");
8693
}
8794

95+
if !bytes.starts_with(NAMESPACE_AGENT_PREFIX) {
96+
return Err("path must begin with agent prefix");
97+
}
98+
8899
let segments: Vec<&[u8]> = bytes.split(|&b| b == NAMESPACE_SEPARATOR).collect();
89100
if segments.len() > MAX_NAMESPACE_SEGMENTS {
90101
return Err("too many namespace segments");
@@ -93,19 +104,19 @@ impl NamespacePath {
93104
for segment in &segments {
94105
let segment = core::str::from_utf8(segment).map_err(|_| "path is invalid itf-8")?;
95106

96-
let first = segment.chars().next().ok_or("empty namespace segment")?;
97-
if !first.is_alphanumeric() {
98-
return Err("namespace segment must start with alphanumeric character");
99-
}
100-
101107
if segment.len() > MAX_SEGMENT_LENGTH {
102108
return Err("namespace segment too long");
103109
}
104110

105-
if segment
106-
.chars()
107-
.any(|c| !c.is_alphanumeric() && c != '-' && c != '_' && c != '+' && c != '=')
108-
{
111+
let first = segment.chars().next().ok_or("empty namespace segment")?;
112+
let last = segment.chars().last().ok_or("empty namespace segment")?;
113+
if !first.is_ascii_alphanumeric() || !last.is_ascii_alphanumeric() {
114+
return Err("namespace segment must start and end with alphanumeric characters");
115+
}
116+
117+
if segment.chars().any(|c| {
118+
!(c.is_ascii_digit() || c.is_ascii_lowercase()) && c != '-' && c != '_' && c != '+'
119+
}) {
109120
return Err("invalid character in namespace segment");
110121
}
111122
}
@@ -128,10 +139,17 @@ impl NamespacePath {
128139
}
129140

130141
/// Parse a namespace path into segments
131-
pub fn segments(&self) -> Vec<&[u8]> {
132-
self.as_bytes()
133-
.split(|&b| b == NAMESPACE_SEPARATOR)
134-
.collect()
142+
pub fn segments(&self) -> impl Iterator<Item = &[u8]> {
143+
self.as_bytes().split(|&b| b == NAMESPACE_SEPARATOR)
144+
}
145+
146+
/// Returns the first segment of the path.
147+
pub fn root(&self) -> Option<Self> {
148+
let Some(root) = self.as_bytes().split(|&b| b == NAMESPACE_SEPARATOR).next() else {
149+
return Some(self.clone());
150+
};
151+
152+
root.to_vec().try_into().ok().map(Self)
135153
}
136154

137155
/// Get the parent path of this namespace
@@ -147,7 +165,7 @@ impl NamespacePath {
147165

148166
/// Get the depth of this namespace (number of segments)
149167
pub fn depth(&self) -> u32 {
150-
self.segments().len() as u32
168+
self.segments().count() as u32
151169
}
152170

153171
/// Check if this path is a parent of another path
@@ -194,7 +212,7 @@ impl FromStr for NamespacePath {
194212
type Err = &'static str;
195213

196214
fn from_str(s: &str) -> Result<Self, Self::Err> {
197-
Self::new(s.as_bytes())
215+
Self::new_agent(s.as_bytes())
198216
}
199217
}
200218

@@ -206,54 +224,60 @@ mod tests {
206224

207225
#[test]
208226
fn namespace_creation_validates_paths() {
209-
assert!(NamespacePath::new(b"agent").is_ok());
210-
assert!(NamespacePath::new(b"agent.alice").is_ok());
211-
assert!(NamespacePath::new(b"agent.alice.memory").is_ok());
212-
assert!(NamespacePath::new(b"agent-1.alice_2.key=val+1").is_ok());
213-
214-
assert!(NamespacePath::new(b"").is_err());
215-
assert!(NamespacePath::new(b".agent").is_err());
216-
assert!(NamespacePath::new(b"agent.").is_err());
217-
assert!(NamespacePath::new(b"agent..alice").is_err());
218-
assert!(NamespacePath::new(b"agent.-alice").is_err());
219-
assert!(NamespacePath::new(b"agent.alice!").is_err());
220-
assert!(NamespacePath::new(b"agent.alice memory").is_err());
227+
assert!(NamespacePath::new_agent(b"agent.alice").is_ok());
228+
assert!(NamespacePath::new_agent(b"agent.alice_2.memory-1.func+1").is_ok());
229+
230+
assert!(NamespacePath::new_agent(format!("agent.alice.{:0<63}", 1).as_bytes()).is_ok());
231+
assert!(NamespacePath::new_agent(format!("agent.alice.{:0<64}", 1).as_bytes()).is_err());
232+
233+
assert!(NamespacePath::new_agent(b"").is_err());
234+
assert!(NamespacePath::new_agent(b"agent").is_err());
235+
assert!(NamespacePath::new_agent(b".agent").is_err());
236+
assert!(NamespacePath::new_agent(b"agent.").is_err());
237+
assert!(NamespacePath::new_agent(b"agent.Alice").is_err());
238+
assert!(NamespacePath::new_agent(b"agent..alice").is_err());
239+
assert!(NamespacePath::new_agent(b"agent.-alice").is_err());
240+
assert!(NamespacePath::new_agent(b"agent.alice-").is_err());
241+
assert!(NamespacePath::new_agent(b"agent.-alice-").is_err());
242+
assert!(NamespacePath::new_agent(b"agent.alice!").is_err());
243+
assert!(NamespacePath::new_agent(b"agent.alice memory").is_err());
244+
assert!(NamespacePath::new_agent("agent.alice.tørûs".as_bytes()).is_err());
221245
}
222246

223247
#[test]
224248
fn namespace_segment_listing() {
225-
let path = NamespacePath::new(b"agent.alice.memory").unwrap();
226-
let segments = path.segments();
227-
assert_eq!(segments.len(), 3);
228-
assert_eq!(segments[0], b"agent");
229-
assert_eq!(segments[1], b"alice");
230-
assert_eq!(segments[2], b"memory");
249+
let path = NamespacePath::new_agent(b"agent.alice.memory").unwrap();
250+
let mut segments = path.segments();
251+
assert_eq!(segments.next(), Some(b"agent".as_slice()));
252+
assert_eq!(segments.next(), Some(b"alice".as_slice()));
253+
assert_eq!(segments.next(), Some(b"memory".as_slice()));
254+
assert_eq!(segments.next(), None);
231255
}
232256

233257
#[test]
234258
fn namespace_parent_returns_correctly() {
235-
let path = NamespacePath::new(b"agent.alice.memory").unwrap();
259+
let path = NamespacePath::new_agent(b"agent.alice.memory").unwrap();
236260
let parent = path.parent().unwrap();
237261
assert_eq!(parent.as_bytes(), b"agent.alice");
238262

239-
let root = NamespacePath::new(b"agent").unwrap();
263+
let root = NamespacePath::agent_root();
240264
assert!(root.parent().is_none());
241265
}
242266

243267
#[test]
244268
fn namespace_depth_calculation() {
245-
let path1 = NamespacePath::new(b"agent").unwrap();
246-
assert_eq!(path1.depth(), 1);
269+
let path1 = NamespacePath::new_agent(b"agent.alice").unwrap();
270+
assert_eq!(path1.depth(), 2);
247271

248-
let path2 = NamespacePath::new(b"agent.alice.memory.twitter").unwrap();
272+
let path2 = NamespacePath::new_agent(b"agent.alice.memory.twitter").unwrap();
249273
assert_eq!(path2.depth(), 4);
250274
}
251275

252276
#[test]
253277
fn test_is_parent_of() {
254-
let parent = NamespacePath::new(b"agent.alice").unwrap();
255-
let child = NamespacePath::new(b"agent.alice.memory").unwrap();
256-
let other = NamespacePath::new(b"agent.bob").unwrap();
278+
let parent = NamespacePath::new_agent(b"agent.alice").unwrap();
279+
let child = NamespacePath::new_agent(b"agent.alice.memory").unwrap();
280+
let other = NamespacePath::new_agent(b"agent.bob").unwrap();
257281

258282
assert!(parent.is_parent_of(&child));
259283
assert!(!parent.is_parent_of(&other));
@@ -262,7 +286,7 @@ mod tests {
262286

263287
#[test]
264288
fn test_parents() {
265-
let path = NamespacePath::new(b"agent.alice.memory.twitter").unwrap();
289+
let path = NamespacePath::new_agent(b"agent.alice.memory.twitter").unwrap();
266290
let parents = path.parents();
267291
assert_eq!(parents.len(), 3);
268292
assert_eq!(parents[0].as_bytes(), b"agent.alice.memory");

pallets/torus0/src/agent.rs

Lines changed: 22 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use codec::{Decode, Encode, MaxEncodedLen};
22
use pallet_emission0_api::Emission0Api;
33
use pallet_governance_api::GovernanceApi;
4+
use pallet_torus0_api::{NamespacePath, NAMESPACE_AGENT_PREFIX};
45
use polkadot_sdk::{
56
frame_election_provider_support::Get,
67
frame_support::{
@@ -11,7 +12,7 @@ use polkadot_sdk::{
1112
},
1213
polkadot_sdk_frame::prelude::BlockNumberFor,
1314
sp_runtime::{traits::Saturating, BoundedVec, DispatchError, Percent},
14-
sp_tracing::debug_span,
15+
sp_tracing::{debug_span, warn},
1516
};
1617
use scale_info::{prelude::vec::Vec, TypeInfo};
1718

@@ -77,7 +78,12 @@ pub fn register<T: crate::Config>(
7778
crate::Error::<T>::TooManyAgentRegistrationsThisInterval
7879
);
7980

80-
validate_agent_name::<T>(&name[..])?;
81+
let namespace_path: Vec<_> = [NAMESPACE_AGENT_PREFIX, &name].concat();
82+
let namespace_path = NamespacePath::new_agent(&namespace_path).map_err(|err| {
83+
warn!("{agent_key:?} tried using invalid name: {err:?}");
84+
crate::Error::<T>::InvalidNamespacePath
85+
})?;
86+
8187
validate_agent_url::<T>(&url[..])?;
8288
validate_agent_metadata::<T>(&metadata[..])?;
8389

@@ -107,6 +113,11 @@ pub fn register<T: crate::Config>(
107113
},
108114
);
109115

116+
crate::namespace::create_namespace::<T>(
117+
crate::namespace::NamespaceOwnership::Account(agent_key.clone()),
118+
namespace_path,
119+
)?;
120+
110121
crate::RegistrationsThisBlock::<T>::mutate(|value| value.saturating_add(1));
111122
crate::RegistrationsThisInterval::<T>::mutate(|value| value.saturating_add(1));
112123

@@ -127,10 +138,15 @@ pub fn unregister<T: crate::Config>(agent_key: AccountIdOf<T>) -> DispatchResult
127138
let span = debug_span!("unregister", agent.key = ?agent_key);
128139
let _guard = span.enter();
129140

130-
ensure!(
131-
exists::<T>(&agent_key),
132-
crate::Error::<T>::AgentDoesNotExist
133-
);
141+
let agent = crate::Agents::<T>::get(&agent_key).ok_or(crate::Error::<T>::AgentDoesNotExist)?;
142+
143+
let namespace_path: Vec<_> = [NAMESPACE_AGENT_PREFIX, &agent.name].concat();
144+
let namespace_path = NamespacePath::new_agent(&namespace_path)
145+
.map_err(|_| crate::Error::<T>::InvalidNamespacePath)?;
146+
crate::namespace::delete_namespace::<T>(
147+
crate::namespace::NamespaceOwnership::Account(agent_key.clone()),
148+
namespace_path,
149+
)?;
134150

135151
crate::Agents::<T>::remove(&agent_key);
136152
crate::stake::clear_key::<T>(&agent_key)?;
@@ -143,7 +159,6 @@ pub fn unregister<T: crate::Config>(agent_key: AccountIdOf<T>) -> DispatchResult
143159
/// Updates the metadata of an existing agent.
144160
pub fn update<T: crate::Config>(
145161
agent_key: AccountIdOf<T>,
146-
name: Vec<u8>,
147162
url: Vec<u8>,
148163
metadata: Option<Vec<u8>>,
149164
staking_fee: Option<Percent>,
@@ -161,9 +176,6 @@ pub fn update<T: crate::Config>(
161176
return Err(crate::Error::<T>::AgentUpdateOnCooldown.into());
162177
}
163178

164-
validate_agent_name::<T>(&name[..])?;
165-
agent.name = BoundedVec::truncate_from(name);
166-
167179
validate_agent_url::<T>(&url[..])?;
168180
agent.url = BoundedVec::truncate_from(url);
169181

@@ -228,31 +240,6 @@ pub fn exists<T: crate::Config>(key: &AccountIdOf<T>) -> bool {
228240
crate::Agents::<T>::contains_key(key)
229241
}
230242

231-
fn validate_agent_name<T: crate::Config>(bytes: &[u8]) -> DispatchResult {
232-
let len: u32 = bytes
233-
.len()
234-
.try_into()
235-
.map_err(|_| crate::Error::<T>::AgentNameTooLong)?;
236-
237-
ensure!(
238-
len >= crate::MinNameLength::<T>::get() as u32,
239-
crate::Error::<T>::AgentNameTooShort
240-
);
241-
242-
ensure!(
243-
len <= (crate::MaxNameLength::<T>::get() as u32)
244-
.min(T::MaxAgentNameLengthConstraint::get()),
245-
crate::Error::<T>::AgentNameTooLong
246-
);
247-
248-
ensure!(
249-
core::str::from_utf8(bytes).is_ok(),
250-
crate::Error::<T>::InvalidAgentName
251-
);
252-
253-
Ok(())
254-
}
255-
256243
fn validate_agent_url<T: crate::Config>(bytes: &[u8]) -> DispatchResult {
257244
let len: u32 = bytes
258245
.len()

pallets/torus0/src/benchmarking.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,6 @@ mod benchmarks {
109109

110110
AgentUpdateCooldown::<T>::set(Default::default());
111111

112-
let name = vec![4, 5, 6];
113112
let url = vec![4, 5, 6];
114113
let metadata = Some(vec![4, 5, 6]);
115114
let staking_fee = Some(Percent::from_percent(10));
@@ -118,7 +117,6 @@ mod benchmarks {
118117
#[extrinsic_call]
119118
update_agent(
120119
RawOrigin::Signed(agent),
121-
name,
122120
url,
123121
metadata,
124122
staking_fee,

0 commit comments

Comments
 (0)