value.rs
1 //! Value types and metadata 2 //! 3 //! The DHT stores multiple value types with different semantics: 4 //! - PeerAnnounce: "I'm at this address" 5 //! - ContentProvider: "I have content with this CID" 6 //! - RootAnnounce: "I'm a root with this tree" 7 //! - SignalOffer/Answer: WebRTC signaling payloads 8 9 use serde::{Deserialize, Serialize}; 10 use crate::key::DhtKey; 11 12 /// Maximum value payload size (64 KB) 13 pub const MAX_VALUE_SIZE: usize = 65536; 14 15 /// Default TTL for values (1 hour) 16 #[allow(dead_code)] 17 pub const DEFAULT_TTL_SECS: u64 = 3600; 18 19 /// Types of values stored in the DHT 20 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 21 #[repr(u8)] 22 pub enum ValueType { 23 /// Peer announcement: node is reachable at an address 24 PeerAnnounce = 0, 25 26 /// Content provider: node has content with given CID 27 ContentProvider = 1, 28 29 /// Root announcement: node is a root with a spanning tree 30 RootAnnounce = 2, 31 32 /// WebRTC signaling offer 33 SignalOffer = 3, 34 35 /// WebRTC signaling answer 36 SignalAnswer = 4, 37 38 /// WebRTC ICE candidate 39 IceCandidate = 5, 40 41 /// Mailbox delegation: "If I'm offline, send messages to this node" 42 MailboxDelegate = 6, 43 44 /// Radicle repository announcement: "I have repo with this RID" 45 RadicleRepo = 7, 46 47 /// User profile announcement: "Here's my profile data" 48 UserProfile = 8, 49 50 /// Follow record: "I follow this peer" 51 FollowRecord = 9, 52 53 /// Token balance record: "My current ABZU balance" 54 TokenBalance = 10, 55 56 /// Reputation bond record: "I have this bond locked" 57 TokenBond = 11, 58 59 /// Service offer: "I offer this service at this price" 60 ServiceOffer = 12, 61 62 /// Generic application data (extensible) 63 Application = 255, 64 } 65 66 impl ValueType { 67 /// Default TTL for this value type 68 pub fn default_ttl(&self) -> u64 { 69 match self { 70 // Long-lived announcements 71 Self::PeerAnnounce => 3600, // 1 hour 72 Self::ContentProvider => 3600, // 1 hour 73 Self::RootAnnounce => 1800, // 30 minutes 74 75 // Short-lived signaling (auto-cleanup) 76 Self::SignalOffer => 120, // 2 minutes 77 Self::SignalAnswer => 120, // 2 minutes 78 Self::IceCandidate => 60, // 1 minute 79 80 // Mailbox delegation (republished every 6 hours) 81 Self::MailboxDelegate => 21600, // 6 hours 82 83 // Radicle repo announcements (long-lived, republished daily) 84 Self::RadicleRepo => 86400, // 24 hours 85 86 // Social primitives 87 Self::UserProfile => 21600, // 6 hours (republished regularly) 88 Self::FollowRecord => 86400, // 24 hours 89 90 // Token economy (republished frequently by active nodes) 91 Self::TokenBalance => 3600, // 1 hour 92 Self::TokenBond => 21600, // 6 hours 93 Self::ServiceOffer => 3600, // 1 hour (active offers) 94 95 // Application-defined 96 Self::Application => 3600, 97 } 98 } 99 100 /// Maximum allowed TTL for this type 101 pub fn max_ttl(&self) -> u64 { 102 match self { 103 Self::PeerAnnounce => 86400, // 24 hours 104 Self::ContentProvider => 86400, // 24 hours 105 Self::RootAnnounce => 3600, // 1 hour 106 Self::SignalOffer => 300, // 5 minutes max 107 Self::SignalAnswer => 300, // 5 minutes max 108 Self::IceCandidate => 300, // 5 minutes max 109 Self::MailboxDelegate => 86400, // 24 hours max 110 Self::RadicleRepo => 604800, // 7 days max 111 Self::UserProfile => 86400, // 24 hours max 112 Self::FollowRecord => 604800, // 7 days max 113 Self::TokenBalance => 86400, // 24 hours max 114 Self::TokenBond => 604800, // 7 days max 115 Self::ServiceOffer => 86400, // 24 hours max 116 Self::Application => 86400, 117 } 118 } 119 120 /// Whether this value type requires signature verification 121 pub fn requires_signature(&self) -> bool { 122 match self { 123 Self::SignalOffer | Self::SignalAnswer | Self::IceCandidate => true, 124 Self::PeerAnnounce | Self::ContentProvider | Self::RootAnnounce => true, 125 Self::MailboxDelegate => true, // Critical: prevents hijacking 126 Self::RadicleRepo => true, // Signed by repo owner 127 Self::UserProfile => true, // Signed by profile owner 128 Self::FollowRecord => true, // Signed by follower 129 Self::TokenBalance => true, // Signed by balance owner 130 Self::TokenBond => true, // Signed by bond owner 131 Self::ServiceOffer => true, // Signed by service provider 132 Self::Application => true, // All values should be signed 133 } 134 } 135 } 136 137 /// Metadata about a stored value 138 #[derive(Debug, Clone, Serialize, Deserialize)] 139 pub struct ValueMetadata { 140 /// Type discriminant 141 pub value_type: ValueType, 142 143 /// Publisher's public key 144 pub publisher: [u8; 32], 145 146 /// Unix timestamp when published 147 pub published_at: u64, 148 149 /// Unix timestamp when expires 150 pub expires_at: u64, 151 152 /// Sequence number (for updates) 153 pub seq: u64, 154 } 155 156 impl ValueMetadata { 157 /// Check if the value has expired 158 pub fn is_expired(&self, now: u64) -> bool { 159 now >= self.expires_at 160 } 161 162 /// Remaining TTL in seconds (0 if expired) 163 pub fn remaining_ttl(&self, now: u64) -> u64 { 164 self.expires_at.saturating_sub(now) 165 } 166 } 167 168 use serde_big_array::BigArray; 169 170 /// A complete stored value with metadata and payload 171 #[derive(Debug, Clone, Serialize, Deserialize)] 172 pub struct StoredValue { 173 /// The DHT key this value is stored under 174 pub key: DhtKey, 175 176 /// Value metadata 177 pub metadata: ValueMetadata, 178 179 /// Raw value payload 180 pub payload: Vec<u8>, 181 182 /// Ed25519 signature over (key || metadata || payload) 183 #[serde(with = "BigArray")] 184 pub signature: [u8; 64], 185 } 186 187 impl StoredValue { 188 /// Create a new stored value (signature should be added separately) 189 pub fn new( 190 key: DhtKey, 191 value_type: ValueType, 192 publisher: [u8; 32], 193 payload: Vec<u8>, 194 now: u64, 195 ttl: Option<u64>, 196 ) -> Self { 197 let ttl = ttl 198 .map(|t| t.min(value_type.max_ttl())) 199 .unwrap_or_else(|| value_type.default_ttl()); 200 201 Self { 202 key, 203 metadata: ValueMetadata { 204 value_type, 205 publisher, 206 published_at: now, 207 expires_at: now + ttl, 208 seq: 0, 209 }, 210 payload, 211 signature: [0u8; 64], // Must be set by caller 212 } 213 } 214 215 /// Get the bytes that should be signed 216 pub fn signable_bytes(&self) -> Vec<u8> { 217 let mut bytes = Vec::with_capacity(32 + 64 + self.payload.len()); 218 bytes.extend_from_slice(&self.key); 219 bytes.extend_from_slice(&(self.metadata.value_type as u8).to_le_bytes()); 220 bytes.extend_from_slice(&self.metadata.publisher); 221 bytes.extend_from_slice(&self.metadata.published_at.to_le_bytes()); 222 bytes.extend_from_slice(&self.metadata.expires_at.to_le_bytes()); 223 bytes.extend_from_slice(&self.metadata.seq.to_le_bytes()); 224 bytes.extend_from_slice(&self.payload); 225 bytes 226 } 227 228 /// Check if value has expired 229 pub fn is_expired(&self, now: u64) -> bool { 230 self.metadata.is_expired(now) 231 } 232 233 /// Value type 234 pub fn value_type(&self) -> ValueType { 235 self.metadata.value_type 236 } 237 } 238 239 /// Peer announcement payload 240 #[derive(Debug, Clone, Serialize, Deserialize)] 241 #[allow(dead_code)] 242 pub struct PeerAnnouncePayload { 243 /// Network address(es) 244 pub addresses: Vec<String>, 245 246 /// Optional tree coordinates for hybrid routing 247 pub tree_coords: Option<Vec<u8>>, 248 249 /// Supported protocol versions 250 pub protocol_versions: Vec<u8>, 251 } 252 253 /// Content provider payload 254 #[derive(Debug, Clone, Serialize, Deserialize)] 255 #[allow(dead_code)] 256 pub struct ContentProviderPayload { 257 /// Content ID being provided 258 pub cid: DhtKey, 259 260 /// Optional size hint 261 pub size_hint: Option<u64>, 262 } 263 264 /// Root announcement payload 265 #[derive(Debug, Clone, Serialize, Deserialize)] 266 #[allow(dead_code)] 267 pub struct RootAnnouncePayload { 268 /// Root's public key 269 pub root_pubkey: [u8; 32], 270 271 /// Root's tree identifier 272 pub tree_id: [u8; 32], 273 274 /// Number of nodes in tree (approximate) 275 pub node_count_hint: Option<u32>, 276 } 277 278 /// Radicle repository announcement payload 279 /// 280 /// Announces that a node is seeding a Radicle repository. 281 /// Key: Hash of the Repository ID (RID) 282 /// Used to discover repos and their seeders via the Abzu mesh. 283 /// 284 /// # Integration with Sovereign Agent 285 /// - Same Ed25519 key for both Abzu identity and Radicle Node ID 286 /// - Enables agents to discover each other's code repositories 287 /// - Part of the "Code Mesh" vision: agentic collaboration through shared repos 288 #[derive(Debug, Clone, Serialize, Deserialize)] 289 pub struct RadicleRepoPayload { 290 /// Radicle Repository ID (RID) - e.g., "rad:z3gqcJUoA..." 291 pub rid: String, 292 293 /// Node ID of the seeder (z-base32 format) 294 pub seeder_node_id: String, 295 296 /// Network addresses where the node can be reached 297 pub seeder_addrs: Vec<String>, 298 299 /// Repository description (optional) 300 pub description: Option<String>, 301 302 /// Repository topics/tags 303 pub topics: Vec<String>, 304 305 /// Default branch (usually "main" or "master") 306 pub default_branch: Option<String>, 307 308 /// Whether this node accepts patches (contributions) 309 pub accepts_patches: bool, 310 } 311 312 /// Mailbox delegation record 313 /// 314 /// Allows an offline Edge node to have messages delivered to its Home Node. 315 /// Key: Edge node's identity (public key) 316 /// Stored on k-closest nodes in DHT. 317 /// 318 /// # Security 319 /// - `sequence`: Monotonic counter prevents replay attacks 320 /// - `signature`: Ed25519 over all fields prevents impersonation 321 /// - `not_valid_after`: Auto-expires stale records 322 /// 323 /// # Validation Order (cheap-to-expensive) 324 /// 1. Check `not_valid_after` (timestamp compare) 325 /// 2. Check `sequence` > stored.sequence (integer compare) 326 /// 3. Verify `signature` (Ed25519 - expensive) 327 #[derive(Debug, Clone, Serialize, Deserialize)] 328 pub struct MailboxRecord { 329 /// Home Node's identity that will receive messages 330 pub delegate_peer_id: [u8; 32], 331 332 /// Addresses where the delegate can be reached 333 pub delegate_addrs: Vec<String>, 334 335 /// Unix timestamp after which this record is invalid 336 pub not_valid_after: u64, 337 338 /// Monotonic counter for ordering updates (anti-replay) 339 pub sequence: u64, 340 341 /// Ed25519 signature over all fields above 342 #[serde(with = "BigArray")] 343 pub signature: [u8; 64], 344 } 345 346 impl MailboxRecord { 347 /// Create a new unsigned mailbox record 348 pub fn new( 349 delegate_peer_id: [u8; 32], 350 delegate_addrs: Vec<String>, 351 not_valid_after: u64, 352 sequence: u64, 353 ) -> Self { 354 Self { 355 delegate_peer_id, 356 delegate_addrs, 357 not_valid_after, 358 sequence, 359 signature: [0u8; 64], // Must be signed by caller 360 } 361 } 362 363 /// Get bytes to sign (all fields except signature) 364 pub fn signable_bytes(&self) -> Vec<u8> { 365 let mut bytes = Vec::with_capacity(32 + 8 + 8 + self.delegate_addrs.len() * 64); 366 bytes.extend_from_slice(&self.delegate_peer_id); 367 bytes.extend_from_slice(&self.not_valid_after.to_le_bytes()); 368 bytes.extend_from_slice(&self.sequence.to_le_bytes()); 369 for addr in &self.delegate_addrs { 370 bytes.extend_from_slice(addr.as_bytes()); 371 bytes.push(0); // Null separator 372 } 373 bytes 374 } 375 376 /// Check if record has expired 377 pub fn is_expired(&self, now: u64) -> bool { 378 now > self.not_valid_after 379 } 380 } 381 382 // ───────────────────────────────────────────────────────────── 383 // Social Primitives 384 // ───────────────────────────────────────────────────────────── 385 386 /// Profile kind discriminant 387 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 388 #[repr(u8)] 389 pub enum ProfileKind { 390 /// AI agent 391 Agent = 0, 392 /// Human user 393 Human = 1, 394 } 395 396 impl Default for ProfileKind { 397 fn default() -> Self { 398 Self::Agent 399 } 400 } 401 402 /// User profile announcement payload 403 /// 404 /// Published to DHT under key: H(pubkey || "profile") 405 /// Allows other nodes to discover profile info by public key. 406 /// 407 /// # Security 408 /// - Signed by profile owner (publisher field in StoredValue) 409 /// - Profile updates require incrementing sequence number 410 #[derive(Debug, Clone, Serialize, Deserialize)] 411 pub struct UserProfilePayload { 412 /// Display name 413 pub name: String, 414 415 /// Bio/description (max 500 chars recommended) 416 pub bio: String, 417 418 /// Agent or Human 419 pub kind: ProfileKind, 420 421 /// Radicle repository IDs this profile is seeding 422 pub repositories: Vec<String>, 423 424 /// Optional avatar content address (BLAKE3 hash) 425 pub avatar_cid: Option<[u8; 32]>, 426 } 427 428 impl UserProfilePayload { 429 /// Create a new profile payload 430 pub fn new(name: impl Into<String>, bio: impl Into<String>, kind: ProfileKind) -> Self { 431 Self { 432 name: name.into(), 433 bio: bio.into(), 434 kind, 435 repositories: Vec::new(), 436 avatar_cid: None, 437 } 438 } 439 } 440 441 /// Follow record payload 442 /// 443 /// Represents a unidirectional "follow" relationship. 444 /// Key: H(follower_pubkey || "follows" || followee_pubkey) 445 /// 446 /// # Privacy 447 /// - By default, stored locally only 448 /// - Optional DHT publishing for discoverability 449 /// - Follower signs the record to prove authenticity 450 #[derive(Debug, Clone, Serialize, Deserialize)] 451 pub struct FollowRecordPayload { 452 /// Who is following (should match StoredValue.publisher) 453 pub follower: [u8; 32], 454 455 /// Who is being followed 456 pub followee: [u8; 32], 457 458 /// Unix timestamp when follow was created 459 pub since: u64, 460 } 461 462 impl FollowRecordPayload { 463 /// Create a new follow record 464 pub fn new(follower: [u8; 32], followee: [u8; 32], since: u64) -> Self { 465 Self { follower, followee, since } 466 } 467 } 468 469 // ───────────────────────────────────────────────────────────── 470 // Token Economy Primitives 471 // ───────────────────────────────────────────────────────────── 472 473 /// Token balance announcement payload 474 /// 475 /// Published to DHT under key: H(pubkey || "balance") 476 /// Allows nodes to verify balances for payments. 477 /// 478 /// # Security 479 /// - Signed by balance owner 480 /// - Contains sequence number for ordering 481 /// - Updated on every balance change 482 #[derive(Debug, Clone, Serialize, Deserialize)] 483 pub struct TokenBalancePayload { 484 /// Balance in drops (1 ABZU = 10^18 drops) 485 pub balance_drops: u128, 486 487 /// Locked amount (in bonds or escrow) 488 pub locked_drops: u128, 489 490 /// Sequence number for ordering updates 491 pub seq: u64, 492 } 493 494 impl TokenBalancePayload { 495 /// Create a new balance payload 496 pub fn new(balance_drops: u128, locked_drops: u128, seq: u64) -> Self { 497 Self { balance_drops, locked_drops, seq } 498 } 499 500 /// Available (unlocked) balance 501 pub fn available(&self) -> u128 { 502 self.balance_drops.saturating_sub(self.locked_drops) 503 } 504 } 505 506 /// Reputation bond announcement payload 507 /// 508 /// Published to DHT under key: H(pubkey || "bond") 509 /// Allows discovery of providers with locked stakes. 510 /// 511 /// # State Machine 512 /// - Active: Currently locked 513 /// - Cooldown: Unlocking (7 days remaining) 514 /// - Slashed: Bond was slashed for misbehavior 515 #[derive(Debug, Clone, Serialize, Deserialize)] 516 pub struct TokenBondPayload { 517 /// Bond amount in drops 518 pub amount_drops: u128, 519 520 /// Bond state: 0=Active, 1=Cooldown, 2=Slashed 521 pub state: u8, 522 523 /// When cooldown ends (Unix timestamp, 0 if active) 524 pub cooldown_ends: u64, 525 526 /// Unix timestamp when bond was created 527 pub created_at: u64, 528 529 /// Sequence number for ordering 530 pub seq: u64, 531 } 532 533 impl TokenBondPayload { 534 /// Check if bond is active 535 pub fn is_active(&self) -> bool { 536 self.state == 0 537 } 538 539 /// Check if bond is in cooldown 540 pub fn is_cooldown(&self) -> bool { 541 self.state == 1 542 } 543 } 544 545 /// Service offer announcement payload 546 /// 547 /// Published to DHT under key: H(category || service_id) 548 /// Allows service discovery across the mesh. 549 /// 550 /// # Pricing 551 /// All prices in drops. Units indicate what price applies to: 552 /// - PerMillionTokens: LLM inference 553 /// - PerGbMonth: Storage 554 /// - PerRequest: API calls 555 #[derive(Debug, Clone, Serialize, Deserialize)] 556 pub struct ServiceOfferPayload { 557 /// Service category (0=Inference, 1=Storage, 2=Privacy, 3=Compute) 558 pub category: u8, 559 560 /// Service identifier (e.g., "llama-3-70b") 561 pub service_id: String, 562 563 /// Price in drops 564 pub price_drops: u128, 565 566 /// Pricing unit (0=PerMillionTokens, 1=PerGbMonth, 2=PerRequest) 567 pub pricing_unit: u8, 568 569 /// Provider's bond amount (trust signal) 570 pub bond_amount_drops: u128, 571 572 /// Provider's reputation score (0-1000, divide by 1000 for 0.0-1.0) 573 pub reputation_permille: u16, 574 575 /// When this offer expires (Unix timestamp) 576 pub expires_at: u64, 577 } 578 579 impl ServiceOfferPayload { 580 /// Reputation as float (0.0 to 1.0) 581 pub fn reputation(&self) -> f64 { 582 f64::from(self.reputation_permille) / 1000.0 583 } 584 } 585 586 587 #[cfg(test)] 588 mod tests { 589 use super::*; 590 591 #[test] 592 fn test_value_type_ttl() { 593 assert!(ValueType::SignalOffer.default_ttl() < ValueType::PeerAnnounce.default_ttl()); 594 assert!(ValueType::SignalOffer.max_ttl() < ValueType::PeerAnnounce.max_ttl()); 595 } 596 597 #[test] 598 fn test_ttl_clamping() { 599 let key = [0u8; 32]; 600 let publisher = [1u8; 32]; 601 602 // Request TTL longer than max 603 let value = StoredValue::new( 604 key, 605 ValueType::SignalOffer, 606 publisher, 607 vec![1, 2, 3], 608 1000, 609 Some(999999), // Way over max 610 ); 611 612 // Should be clamped to max 613 let expected_max = ValueType::SignalOffer.max_ttl(); 614 assert_eq!(value.metadata.expires_at, 1000 + expected_max); 615 } 616 617 #[test] 618 fn test_expiration() { 619 let key = [0u8; 32]; 620 let publisher = [1u8; 32]; 621 622 let value = StoredValue::new( 623 key, 624 ValueType::SignalOffer, 625 publisher, 626 vec![1, 2, 3], 627 1000, 628 Some(60), 629 ); 630 631 assert!(!value.is_expired(1000)); 632 assert!(!value.is_expired(1059)); 633 assert!(value.is_expired(1060)); 634 assert!(value.is_expired(2000)); 635 } 636 637 #[test] 638 fn test_signable_bytes_deterministic() { 639 let key = [0xABu8; 32]; 640 let publisher = [0xCDu8; 32]; 641 642 let v1 = StoredValue::new(key, ValueType::PeerAnnounce, publisher, vec![1, 2, 3], 1000, None); 643 let v2 = StoredValue::new(key, ValueType::PeerAnnounce, publisher, vec![1, 2, 3], 1000, None); 644 645 assert_eq!(v1.signable_bytes(), v2.signable_bytes()); 646 } 647 648 // ───────────────────────────────────────────────────────────── 649 // Social Primitives Tests 650 // ───────────────────────────────────────────────────────────── 651 652 #[test] 653 fn test_user_profile_ttl() { 654 assert_eq!(ValueType::UserProfile.default_ttl(), 21600); // 6 hours 655 assert_eq!(ValueType::UserProfile.max_ttl(), 86400); // 24 hours 656 } 657 658 #[test] 659 fn test_follow_record_ttl() { 660 assert_eq!(ValueType::FollowRecord.default_ttl(), 86400); // 24 hours 661 assert_eq!(ValueType::FollowRecord.max_ttl(), 604800); // 7 days 662 } 663 664 #[test] 665 fn test_social_types_require_signature() { 666 assert!(ValueType::UserProfile.requires_signature()); 667 assert!(ValueType::FollowRecord.requires_signature()); 668 } 669 670 #[test] 671 fn test_user_profile_payload_serialization() { 672 let profile = UserProfilePayload::new("Test Agent", "A test bio", ProfileKind::Agent); 673 let bytes = bincode::serialize(&profile).unwrap(); 674 let decoded: UserProfilePayload = bincode::deserialize(&bytes).unwrap(); 675 assert_eq!(decoded.name, "Test Agent"); 676 assert_eq!(decoded.kind, ProfileKind::Agent); 677 } 678 679 #[test] 680 fn test_follow_record_payload_serialization() { 681 let follower = [1u8; 32]; 682 let followee = [2u8; 32]; 683 let record = FollowRecordPayload::new(follower, followee, 1700000000); 684 let bytes = bincode::serialize(&record).unwrap(); 685 let decoded: FollowRecordPayload = bincode::deserialize(&bytes).unwrap(); 686 assert_eq!(decoded.follower, follower); 687 assert_eq!(decoded.followee, followee); 688 assert_eq!(decoded.since, 1700000000); 689 } 690 }