synor/crates/synor-hosting/src/registry.rs
2026-02-02 05:58:22 +05:30

467 lines
13 KiB
Rust

//! Name Registry - On-chain name to CID mapping
//!
//! Manages the registration and resolution of human-readable names
//! to content identifiers (CIDs) on the Synor blockchain.
use crate::error::{Error, Result};
use crate::validate_name;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use synor_storage::ContentId;
/// Address type (32 bytes)
pub type Address = [u8; 32];
/// Name record stored on-chain
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NameRecord {
/// The registered name
pub name: String,
/// Owner address
pub owner: Address,
/// Current CID the name points to
pub cid: ContentId,
/// Block number when registered
pub registered_at: u64,
/// Block number when expires (0 = never)
pub expires_at: u64,
/// Custom domains linked to this name
pub custom_domains: Vec<String>,
/// Metadata (optional JSON)
pub metadata: Option<String>,
}
/// Request to register a new name
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegistrationRequest {
/// Name to register
pub name: String,
/// Initial CID
pub cid: ContentId,
/// Registration duration in blocks (0 = permanent)
pub duration: u64,
/// Optional metadata
pub metadata: Option<String>,
}
/// Name registry state
#[derive(Debug, Default)]
pub struct NameRegistry {
/// Name to record mapping
records: HashMap<String, NameRecord>,
/// Custom domain to name mapping
domain_to_name: HashMap<String, String>,
/// Registration fee (atomic SYNOR)
registration_fee: u64,
/// Annual renewal fee (atomic SYNOR)
renewal_fee: u64,
/// Grace period in blocks after expiry
grace_period: u64,
}
impl NameRegistry {
/// Create a new name registry
pub fn new() -> Self {
Self {
records: HashMap::new(),
domain_to_name: HashMap::new(),
registration_fee: 10_000_000_000, // 10 SYNOR
renewal_fee: 5_000_000_000, // 5 SYNOR
grace_period: 432_000, // ~30 days at 6s blocks
}
}
/// Create registry with custom fees
pub fn with_fees(registration_fee: u64, renewal_fee: u64, grace_period: u64) -> Self {
Self {
records: HashMap::new(),
domain_to_name: HashMap::new(),
registration_fee,
renewal_fee,
grace_period,
}
}
/// Register a new name
pub fn register(
&mut self,
request: RegistrationRequest,
owner: Address,
current_block: u64,
) -> Result<NameRecord> {
// Validate name
validate_name(&request.name)?;
// Check if name is available
if let Some(existing) = self.records.get(&request.name) {
// Check if expired and past grace period
if existing.expires_at > 0 && current_block > existing.expires_at + self.grace_period {
// Name can be re-registered
} else {
return Err(Error::NameTaken(request.name));
}
}
// Calculate expiry
let expires_at = if request.duration == 0 {
0 // Permanent
} else {
current_block + request.duration
};
let record = NameRecord {
name: request.name.clone(),
owner,
cid: request.cid,
registered_at: current_block,
expires_at,
custom_domains: Vec::new(),
metadata: request.metadata,
};
self.records.insert(request.name, record.clone());
Ok(record)
}
/// Update CID for a name (must be owner)
pub fn update(
&mut self,
name: &str,
new_cid: ContentId,
caller: Address,
current_block: u64,
) -> Result<()> {
let record = self.get_record_mut(name, current_block)?;
if record.owner != caller {
return Err(Error::NotOwner);
}
record.cid = new_cid;
Ok(())
}
/// Transfer ownership of a name
pub fn transfer(
&mut self,
name: &str,
new_owner: Address,
caller: Address,
current_block: u64,
) -> Result<()> {
let record = self.get_record_mut(name, current_block)?;
if record.owner != caller {
return Err(Error::NotOwner);
}
record.owner = new_owner;
Ok(())
}
/// Renew a name registration
pub fn renew(
&mut self,
name: &str,
additional_duration: u64,
caller: Address,
current_block: u64,
) -> Result<u64> {
let record = self.get_record_mut(name, current_block)?;
if record.owner != caller {
return Err(Error::NotOwner);
}
if record.expires_at == 0 {
return Err(Error::InvalidOperation("Name is permanent".into()));
}
// Extend from current expiry or current block (if in grace period)
let base = if record.expires_at > current_block {
record.expires_at
} else {
current_block
};
record.expires_at = base + additional_duration;
Ok(record.expires_at)
}
/// Resolve a name to its CID
pub fn resolve(&self, name: &str, current_block: u64) -> Option<ContentId> {
self.records.get(name).and_then(|record| {
if self.is_valid(record, current_block) {
Some(record.cid.clone())
} else {
None
}
})
}
/// Get full record for a name
pub fn get_record(&self, name: &str, current_block: u64) -> Result<&NameRecord> {
let record = self
.records
.get(name)
.ok_or_else(|| Error::NameNotFound(name.to_string()))?;
if !self.is_valid(record, current_block) {
return Err(Error::NameExpired(name.to_string()));
}
Ok(record)
}
/// Get mutable record
fn get_record_mut(&mut self, name: &str, current_block: u64) -> Result<&mut NameRecord> {
let record = self
.records
.get(name)
.ok_or_else(|| Error::NameNotFound(name.to_string()))?;
// Check validity before returning mutable reference
if !self.is_valid(record, current_block) {
return Err(Error::NameExpired(name.to_string()));
}
Ok(self.records.get_mut(name).unwrap())
}
/// Check if a record is valid (not expired or within grace period)
fn is_valid(&self, record: &NameRecord, current_block: u64) -> bool {
if record.expires_at == 0 {
return true; // Permanent
}
current_block <= record.expires_at + self.grace_period
}
/// Check if a name is available for registration
pub fn is_available(&self, name: &str, current_block: u64) -> bool {
match self.records.get(name) {
None => validate_name(name).is_ok(),
Some(record) => {
record.expires_at > 0 && current_block > record.expires_at + self.grace_period
}
}
}
/// Add a custom domain to a name
pub fn add_custom_domain(
&mut self,
name: &str,
domain: String,
caller: Address,
current_block: u64,
) -> Result<()> {
// Check domain isn't already linked
if self.domain_to_name.contains_key(&domain) {
return Err(Error::DomainAlreadyLinked(domain));
}
let record = self.get_record_mut(name, current_block)?;
if record.owner != caller {
return Err(Error::NotOwner);
}
record.custom_domains.push(domain.clone());
self.domain_to_name.insert(domain, name.to_string());
Ok(())
}
/// Remove a custom domain from a name
pub fn remove_custom_domain(
&mut self,
name: &str,
domain: &str,
caller: Address,
current_block: u64,
) -> Result<()> {
let record = self.get_record_mut(name, current_block)?;
if record.owner != caller {
return Err(Error::NotOwner);
}
record.custom_domains.retain(|d| d != domain);
self.domain_to_name.remove(domain);
Ok(())
}
/// Resolve a custom domain to a name
pub fn resolve_domain(&self, domain: &str) -> Option<&str> {
self.domain_to_name.get(domain).map(|s| s.as_str())
}
/// Resolve a custom domain directly to CID
pub fn resolve_domain_to_cid(&self, domain: &str, current_block: u64) -> Option<ContentId> {
self.resolve_domain(domain)
.and_then(|name| self.resolve(name, current_block))
}
/// Get registration fee
pub fn registration_fee(&self) -> u64 {
self.registration_fee
}
/// Get renewal fee
pub fn renewal_fee(&self) -> u64 {
self.renewal_fee
}
/// Get all names owned by an address
pub fn names_by_owner(&self, owner: &Address, current_block: u64) -> Vec<&NameRecord> {
self.records
.values()
.filter(|r| r.owner == *owner && self.is_valid(r, current_block))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_cid(data: &[u8]) -> ContentId {
ContentId::from_content(data)
}
#[test]
fn test_register_and_resolve() {
let mut registry = NameRegistry::new();
let owner = [1u8; 32];
let cid = make_cid(b"hello world");
let request = RegistrationRequest {
name: "myapp".to_string(),
cid: cid.clone(),
duration: 1000,
metadata: None,
};
registry.register(request, owner, 0).unwrap();
let resolved = registry.resolve("myapp", 0).unwrap();
assert_eq!(resolved.digest, cid.digest);
}
#[test]
fn test_update_cid() {
let mut registry = NameRegistry::new();
let owner = [1u8; 32];
let cid1 = make_cid(b"version 1");
let cid2 = make_cid(b"version 2");
let request = RegistrationRequest {
name: "myapp".to_string(),
cid: cid1.clone(),
duration: 1000,
metadata: None,
};
registry.register(request, owner, 0).unwrap();
registry.update("myapp", cid2.clone(), owner, 0).unwrap();
let resolved = registry.resolve("myapp", 0).unwrap();
assert_eq!(resolved.digest, cid2.digest);
}
#[test]
fn test_transfer_ownership() {
let mut registry = NameRegistry::new();
let owner1 = [1u8; 32];
let owner2 = [2u8; 32];
let cid = make_cid(b"hello");
let request = RegistrationRequest {
name: "myapp".to_string(),
cid: cid.clone(),
duration: 1000,
metadata: None,
};
registry.register(request, owner1, 0).unwrap();
registry.transfer("myapp", owner2, owner1, 0).unwrap();
let record = registry.get_record("myapp", 0).unwrap();
assert_eq!(record.owner, owner2);
}
#[test]
fn test_name_expiry() {
let mut registry = NameRegistry::new();
let owner = [1u8; 32];
let cid = make_cid(b"hello");
let request = RegistrationRequest {
name: "myapp".to_string(),
cid: cid.clone(),
duration: 100,
metadata: None,
};
registry.register(request, owner, 0).unwrap();
// Should resolve before expiry
assert!(registry.resolve("myapp", 50).is_some());
// Should still resolve during grace period
assert!(registry.resolve("myapp", 150).is_some());
// Should not resolve after grace period
let after_grace = 100 + registry.grace_period + 1;
assert!(registry.resolve("myapp", after_grace).is_none());
}
#[test]
fn test_custom_domain() {
let mut registry = NameRegistry::new();
let owner = [1u8; 32];
let cid = make_cid(b"hello");
let request = RegistrationRequest {
name: "myapp".to_string(),
cid: cid.clone(),
duration: 1000,
metadata: None,
};
registry.register(request, owner, 0).unwrap();
registry
.add_custom_domain("myapp", "myapp.com".to_string(), owner, 0)
.unwrap();
assert_eq!(registry.resolve_domain("myapp.com"), Some("myapp"));
let resolved = registry.resolve_domain_to_cid("myapp.com", 0).unwrap();
assert_eq!(resolved.digest, cid.digest);
}
#[test]
fn test_not_owner_error() {
let mut registry = NameRegistry::new();
let owner = [1u8; 32];
let other = [2u8; 32];
let cid = make_cid(b"hello");
let request = RegistrationRequest {
name: "myapp".to_string(),
cid: cid.clone(),
duration: 1000,
metadata: None,
};
registry.register(request, owner, 0).unwrap();
// Other user cannot update
let result = registry.update("myapp", make_cid(b"new"), other, 0);
assert!(matches!(result, Err(Error::NotOwner)));
}
}