/ abzu-dht / src / store / value.rs
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  }