trust.rs
1 //! Trust Engine 2 //! 3 //! Centralized Circle membership, invite trees, vouching, and pruning logic. 4 //! This module implements the trust protocol that enables decentralized moderation. 5 6 use crate::codec; 7 use crate::node::NodeError; 8 use serde::{Deserialize, Serialize}; 9 10 // ───────────────────────────────────────────────────────────────────────────── 11 // Trust System Constants 12 // ───────────────────────────────────────────────────────────────────────────── 13 14 /// Maximum primary invites per member 15 pub const MAX_INVITE_CAPACITY: u8 = 10; 16 17 /// Maximum secondary vouches per member 18 pub const MAX_VOUCH_CAPACITY: u8 = 144; 19 20 /// Maximum invite depth from founder (prevents runaway trees) 21 pub const MAX_INVITE_DEPTH: u8 = 8; 22 23 /// Minimum vouches required for admin elevation 24 pub const MIN_VOUCHES_FOR_ADMIN: u8 = 3; 25 26 // ───────────────────────────────────────────────────────────────────────────── 27 // Core Types 28 // ───────────────────────────────────────────────────────────────────────────── 29 30 /// Role of a member within a Circle 31 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 32 pub enum MemberRole { 33 /// Circle creator, can invite/remove anyone 34 Founder, 35 /// Can invite new members 36 Admin, 37 /// Regular member 38 Member, 39 } 40 41 /// A secondary endorsement of a member 42 #[derive(Debug, Clone, Serialize, Deserialize)] 43 pub struct Vouch { 44 /// Who vouched (public key) 45 pub voucher: [u8; 32], 46 /// Voucher's signature over (target + circle_id + timestamp) 47 pub signature: Vec<u8>, 48 /// When the vouch was created (Unix ms) 49 pub timestamp: u64, 50 /// Whether this vouch has been revoked 51 pub revoked: bool, 52 } 53 54 /// Trust capacity for a member (how many invites/vouches they have left) 55 #[derive(Debug, Clone, Serialize, Deserialize)] 56 pub struct TrustCapacity { 57 /// Primary invites remaining 58 pub invites_remaining: u8, 59 /// Secondary vouches remaining 60 pub vouches_remaining: u8, 61 } 62 63 impl Default for TrustCapacity { 64 fn default() -> Self { 65 Self { 66 invites_remaining: MAX_INVITE_CAPACITY, 67 vouches_remaining: MAX_VOUCH_CAPACITY, 68 } 69 } 70 } 71 72 /// An action recorded in the trust ledger 73 #[derive(Debug, Clone, Serialize, Deserialize)] 74 pub enum TrustAction { 75 /// Primary invite (creates membership) 76 Invite { 77 inviter: [u8; 32], 78 invitee: [u8; 32], 79 depth: u8, 80 }, 81 /// Secondary vouch (endorses existing member) 82 Vouch { 83 voucher: [u8; 32], 84 member: [u8; 32], 85 }, 86 /// Revoke a previous vouch 87 Revoke { 88 voucher: [u8; 32], 89 member: [u8; 32], 90 }, 91 /// Prune a branch (remove member + descendants) 92 Prune { 93 admin: [u8; 32], 94 target: [u8; 32], 95 descendants_removed: u32, 96 }, 97 } 98 99 /// An entry in the trust ledger (immutable history) 100 #[derive(Debug, Clone, Serialize, Deserialize)] 101 pub struct TrustLedgerEntry { 102 /// The action taken 103 pub action: TrustAction, 104 /// When the action occurred (Unix ms) 105 pub timestamp: u64, 106 /// Actor's signature over the action 107 pub signature: Vec<u8>, 108 } 109 110 // ───────────────────────────────────────────────────────────────────────────── 111 // Configurable Trust Policies 112 // ───────────────────────────────────────────────────────────────────────────── 113 114 /// What the trust ledger records 115 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] 116 pub enum LedgerMode { 117 /// All actions recorded: invites, vouches, prunes, revokes 118 Full, 119 /// Only current member list, no action history 120 MembershipOnly, 121 /// No ledger storage at all (safest for adversarial contexts) 122 #[default] 123 Ephemeral, 124 } 125 126 /// How pruning behaves 127 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] 128 pub enum PruneMode { 129 /// Remove member + all their invite descendants (current behavior) 130 Cascade, 131 /// Remove member, descendants become root-level (depth reset to 1) 132 #[default] 133 Orphan, 134 /// Remove member, descendants assigned to the pruning admin 135 Reassign, 136 /// No pruning allowed - members can only leave voluntarily 137 Voluntary, 138 } 139 140 /// Trust policy determines how a circle handles accountability vs. privacy 141 #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] 142 pub enum TrustPolicy { 143 /// Full accountability - permanent ledger, configurable prune 144 /// Use case: Public communities, moderated groups 145 Accountable { 146 ledger_mode: LedgerMode, 147 prune_mode: PruneMode, 148 }, 149 150 /// Minimal records - only current membership visible 151 /// Use case: Private friend groups 152 Private { 153 prune_mode: PruneMode, 154 }, 155 156 /// Zero accountability - ephemeral membership, no history 157 /// Use case: Activist cells, whistleblowers, high-security situations 158 /// This is the SAFEST option for adversarial contexts. 159 #[default] 160 Anonymous, 161 } 162 163 impl TrustPolicy { 164 /// Returns whether this policy records invite trees 165 /// Both Accountable and Private record trees (for pruning), only Anonymous doesn't 166 pub fn records_invite_tree(&self) -> bool { 167 matches!(self, TrustPolicy::Accountable { .. } | TrustPolicy::Private { .. }) 168 } 169 170 /// Returns whether this policy records vouches 171 /// Both Accountable and Private record vouches, only Anonymous doesn't 172 pub fn records_vouches(&self) -> bool { 173 matches!(self, TrustPolicy::Accountable { .. } | TrustPolicy::Private { .. }) 174 } 175 176 /// Returns whether this policy maintains a trust ledger 177 pub fn has_ledger(&self) -> bool { 178 match self { 179 TrustPolicy::Accountable { ledger_mode, .. } => { 180 *ledger_mode != LedgerMode::Ephemeral 181 } 182 TrustPolicy::Private { .. } => false, 183 TrustPolicy::Anonymous => false, 184 } 185 } 186 187 /// Get the prune mode for this policy 188 pub fn prune_mode(&self) -> PruneMode { 189 match self { 190 TrustPolicy::Accountable { prune_mode, .. } => *prune_mode, 191 TrustPolicy::Private { prune_mode } => *prune_mode, 192 TrustPolicy::Anonymous => PruneMode::Voluntary, // No cascade in anonymous 193 } 194 } 195 196 /// Create a high-accountability policy (for moderated communities) 197 pub fn accountable() -> Self { 198 TrustPolicy::Accountable { 199 ledger_mode: LedgerMode::Full, 200 prune_mode: PruneMode::Cascade, 201 } 202 } 203 204 /// Create a private policy (friends group) 205 pub fn private() -> Self { 206 TrustPolicy::Private { 207 prune_mode: PruneMode::Orphan, 208 } 209 } 210 } 211 212 // ───────────────────────────────────────────────────────────────────────────── 213 // CircleMember 214 // ───────────────────────────────────────────────────────────────────────────── 215 216 /// A member of a Circle (with trust fields) 217 #[derive(Debug, Clone, Serialize, Deserialize)] 218 pub struct CircleMember { 219 /// Member's public key 220 pub pubkey: [u8; 32], 221 /// Role in the circle 222 pub role: MemberRole, 223 /// When they joined (Unix ms) 224 pub joined_at: u64, 225 226 // Trust fields 227 /// Who invited this member (None for founder) 228 pub invited_by: Option<[u8; 32]>, 229 /// Inviter's signature over the invitation 230 pub invite_signature: Option<Vec<u8>>, 231 /// Generations from founder (0 = founder) 232 pub invite_depth: u8, 233 /// Secondary endorsements received 234 pub vouches_received: Vec<Vouch>, 235 /// Trust capacity (invites/vouches remaining) 236 pub capacity: TrustCapacity, 237 } 238 239 impl CircleMember { 240 /// Create a new founder member (no inviter, depth 0) 241 pub fn new_founder(pubkey: [u8; 32], joined_at: u64) -> Self { 242 Self { 243 pubkey, 244 role: MemberRole::Founder, 245 joined_at, 246 invited_by: None, 247 invite_signature: None, 248 invite_depth: 0, 249 vouches_received: Vec::new(), 250 capacity: TrustCapacity::default(), 251 } 252 } 253 254 /// Create a new invited member 255 pub fn new_invited( 256 pubkey: [u8; 32], 257 role: MemberRole, 258 joined_at: u64, 259 invited_by: [u8; 32], 260 invite_signature: Vec<u8>, 261 invite_depth: u8, 262 ) -> Self { 263 Self { 264 pubkey, 265 role, 266 joined_at, 267 invited_by: Some(invited_by), 268 invite_signature: Some(invite_signature), 269 invite_depth, 270 vouches_received: Vec::new(), 271 capacity: TrustCapacity::default(), 272 } 273 } 274 275 /// Check if this member can invite others 276 pub fn can_invite(&self) -> bool { 277 self.capacity.invites_remaining > 0 && self.invite_depth < MAX_INVITE_DEPTH 278 } 279 280 /// Check if this member can vouch for others 281 pub fn can_vouch(&self) -> bool { 282 self.capacity.vouches_remaining > 0 283 } 284 285 /// Get total endorsement count (for elevation requirements) 286 pub fn total_endorsements(&self) -> usize { 287 // 1 for primary invite + active vouches 288 let base = if self.invited_by.is_some() { 1 } else { 0 }; 289 let vouches = self.vouches_received.iter().filter(|v| !v.revoked).count(); 290 base + vouches 291 } 292 } 293 294 // ───────────────────────────────────────────────────────────────────────────── 295 // Circle 296 // ───────────────────────────────────────────────────────────────────────────── 297 298 /// A Circle (group) - distributed state machine replicated across all members 299 #[derive(Debug, Clone, Serialize, Deserialize)] 300 pub struct Circle { 301 /// Circle ID (hash of founder_key + timestamp + nonce) 302 pub id: [u8; 32], 303 /// Circle name (plaintext - encrypted on wire) 304 pub name: String, 305 /// All members with their roles 306 pub members: Vec<CircleMember>, 307 /// Current epoch (increments on membership change) 308 pub epoch: u64, 309 /// Symmetric key for group encryption (derived from epoch + members) 310 pub symmetric_key: [u8; 32], 311 /// Creation timestamp (Unix ms) 312 pub created_at: u64, 313 /// Trust ledger (immutable history of trust actions) 314 /// Only populated if trust_policy.has_ledger() is true 315 pub trust_ledger: Vec<TrustLedgerEntry>, 316 /// Trust policy for this circle (determines privacy vs. accountability tradeoff) 317 /// Default: Anonymous (safest for adversarial contexts) 318 pub trust_policy: TrustPolicy, 319 } 320 321 322 // ───────────────────────────────────────────────────────────────────────────── 323 // TrustEngine 324 // ───────────────────────────────────────────────────────────────────────────── 325 326 use sled::Tree; 327 use thiserror::Error; 328 use tracing::debug; 329 330 /// Errors from trust operations 331 #[derive(Error, Debug)] 332 pub enum TrustError { 333 #[error("Storage error: {0}")] 334 Storage(#[from] sled::Error), 335 336 #[error("Serialization error: {0}")] 337 Serialization(String), 338 339 #[error("Circle not found")] 340 CircleNotFound, 341 342 #[error("Member not found")] 343 MemberNotFound, 344 345 #[error("Already a member")] 346 AlreadyMember, 347 348 #[error("Insufficient authority")] 349 InsufficientAuthority, 350 351 #[error("Cannot prune founder")] 352 CannotPruneFounder, 353 354 #[error("No invite capacity or max depth reached")] 355 NoInviteCapacity, 356 357 #[error("No vouch capacity")] 358 NoVouchCapacity, 359 360 #[error("Already vouched for this member")] 361 AlreadyVouched, 362 } 363 364 impl From<NodeError> for TrustError { 365 fn from(e: NodeError) -> Self { 366 TrustError::Serialization(e.to_string()) 367 } 368 } 369 370 /// Derive symmetric key for a circle 371 /// 372 /// This is a pure function using blake3 HKDF-style derivation. 373 /// Returns a 32-byte key deterministically derived from circle_id + epoch + sorted member keys. 374 pub fn derive_circle_key(circle_id: &[u8; 32], epoch: u64, member_keys: &[[u8; 32]]) -> [u8; 32] { 375 use blake3::Hasher; 376 377 let mut hasher = Hasher::new(); 378 hasher.update(b"abzu-circle-key"); 379 hasher.update(circle_id); 380 hasher.update(&epoch.to_be_bytes()); 381 382 // Sort member keys for deterministic derivation 383 let mut sorted_keys = member_keys.to_vec(); 384 sorted_keys.sort(); 385 for key in sorted_keys { 386 hasher.update(&key); 387 } 388 389 hasher.finalize().into() 390 } 391 392 /// TrustEngine handles all Circle membership, invite trees, vouching, and pruning. 393 /// 394 /// Separated from Node to enable independent testing and future persistence swaps. 395 pub struct TrustEngine { 396 circles: Tree, 397 } 398 399 impl TrustEngine { 400 /// Create a new TrustEngine with the given sled Tree 401 pub fn new(circles: Tree) -> Self { 402 Self { circles } 403 } 404 405 // ───────────────────────────────────────────────────────────────────────── 406 // Circle Lifecycle 407 // ───────────────────────────────────────────────────────────────────────── 408 409 /// Create a new Circle with the given founder and policy 410 pub fn create_circle( 411 &self, 412 founder_key: [u8; 32], 413 name: String, 414 trust_policy: TrustPolicy, 415 ) -> Result<Circle, TrustError> { 416 use blake3::Hasher; 417 418 let timestamp = std::time::SystemTime::now() 419 .duration_since(std::time::UNIX_EPOCH) 420 .unwrap() 421 .as_millis() as u64; 422 423 // Generate circle ID from founder + timestamp + random nonce 424 let mut hasher = Hasher::new(); 425 hasher.update(&founder_key); 426 hasher.update(×tamp.to_be_bytes()); 427 hasher.update(&rand::random::<[u8; 16]>()); 428 let id: [u8; 32] = hasher.finalize().into(); 429 430 // Initial symmetric key (will be re-derived on membership changes) 431 let symmetric_key = derive_circle_key(&id, 1, &[founder_key]); 432 433 let circle = Circle { 434 id, 435 name, 436 members: vec![CircleMember::new_founder(founder_key, timestamp)], 437 epoch: 1, 438 symmetric_key, 439 created_at: timestamp, 440 trust_ledger: Vec::new(), 441 trust_policy, 442 }; 443 444 // Store 445 let value = codec::encode(&circle)?; 446 self.circles.insert(id, value)?; 447 448 debug!(circle_id = ?&id[..8], name = %circle.name, policy = ?circle.trust_policy, "Created circle"); 449 Ok(circle) 450 } 451 452 /// Get a circle by ID 453 pub fn get_circle(&self, id: &[u8; 32]) -> Result<Option<Circle>, TrustError> { 454 match self.circles.get(id)? { 455 Some(value) => { 456 let circle: Circle = codec::decode(&value)?; 457 Ok(Some(circle)) 458 } 459 None => Ok(None), 460 } 461 } 462 463 /// List all circles 464 pub fn list_circles(&self) -> Result<Vec<Circle>, TrustError> { 465 let mut circles = Vec::new(); 466 for item in self.circles.iter() { 467 let (_, value) = item?; 468 let circle: Circle = codec::decode(&value)?; 469 circles.push(circle); 470 } 471 Ok(circles) 472 } 473 474 /// Save a circle to storage (internal helper) 475 fn save_circle(&self, circle: &Circle) -> Result<(), TrustError> { 476 let value = codec::encode(circle)?; 477 self.circles.insert(circle.id, value)?; 478 Ok(()) 479 } 480 481 // ───────────────────────────────────────────────────────────────────────── 482 // Membership 483 // ───────────────────────────────────────────────────────────────────────── 484 485 /// Invite a new member to a circle 486 pub fn invite_member( 487 &self, 488 caller_pubkey: [u8; 32], 489 circle_id: &[u8; 32], 490 invitee_pubkey: [u8; 32], 491 invite_signature: Vec<u8>, 492 ) -> Result<Circle, TrustError> { 493 let mut circle = self.get_circle(circle_id)? 494 .ok_or(TrustError::CircleNotFound)?; 495 496 // Check if already a member 497 if circle.members.iter().any(|m| m.pubkey == invitee_pubkey) { 498 return Err(TrustError::AlreadyMember); 499 } 500 501 let inviter_pubkey = caller_pubkey; 502 503 // Find inviter and check capacity 504 let inviter_idx = circle.members.iter() 505 .position(|m| m.pubkey == inviter_pubkey) 506 .ok_or(TrustError::MemberNotFound)?; 507 508 if !circle.members[inviter_idx].can_invite() { 509 return Err(TrustError::NoInviteCapacity); 510 } 511 512 let inviter_depth = circle.members[inviter_idx].invite_depth; 513 let new_depth = inviter_depth.saturating_add(1); 514 515 if new_depth > MAX_INVITE_DEPTH { 516 return Err(TrustError::NoInviteCapacity); 517 } 518 519 let timestamp = std::time::SystemTime::now() 520 .duration_since(std::time::UNIX_EPOCH) 521 .unwrap() 522 .as_millis() as u64; 523 524 // Decrement inviter's capacity 525 circle.members[inviter_idx].capacity.invites_remaining = 526 circle.members[inviter_idx].capacity.invites_remaining.saturating_sub(1); 527 528 // Add new member 529 // For Anonymous/Private circles, we don't store the invite tree (OpSec) 530 let new_member = if circle.trust_policy.records_invite_tree() { 531 CircleMember::new_invited( 532 invitee_pubkey, 533 MemberRole::Member, 534 timestamp, 535 inviter_pubkey, 536 invite_signature.clone(), 537 new_depth, 538 ) 539 } else { 540 // Anonymous mode: no invite tree, minimal data 541 CircleMember { 542 pubkey: invitee_pubkey, 543 role: MemberRole::Member, 544 joined_at: timestamp, 545 invited_by: None, // OpSec: no inviter recorded 546 invite_signature: None, // OpSec: no signature recorded 547 invite_depth: 0, // OpSec: no depth recorded 548 vouches_received: Vec::new(), 549 capacity: TrustCapacity::default(), 550 } 551 }; 552 circle.members.push(new_member); 553 554 // Record in ledger 555 let ledger_entry = TrustLedgerEntry { 556 action: TrustAction::Invite { 557 inviter: inviter_pubkey, 558 invitee: invitee_pubkey, 559 depth: new_depth, 560 }, 561 timestamp, 562 signature: invite_signature, 563 }; 564 // Only record in ledger if policy allows 565 if circle.trust_policy.has_ledger() { 566 circle.trust_ledger.push(ledger_entry); 567 } 568 569 circle.epoch += 1; 570 571 // Re-derive symmetric key with new membership 572 let member_keys: Vec<[u8; 32]> = circle.members.iter().map(|m| m.pubkey).collect(); 573 circle.symmetric_key = derive_circle_key(&circle.id, circle.epoch, &member_keys); 574 575 self.save_circle(&circle)?; 576 577 debug!( 578 circle_id = ?&circle_id[..8], 579 inviter = ?&inviter_pubkey[..8], 580 invitee = ?&invitee_pubkey[..8], 581 depth = new_depth, 582 "Invited new member" 583 ); 584 585 Ok(circle) 586 } 587 588 /// Remove a member from a circle (simple removal) 589 pub fn remove_member( 590 &self, 591 circle_id: &[u8; 32], 592 pubkey: &[u8; 32], 593 ) -> Result<Circle, TrustError> { 594 let mut circle = self.get_circle(circle_id)? 595 .ok_or(TrustError::CircleNotFound)?; 596 597 circle.members.retain(|m| &m.pubkey != pubkey); 598 circle.epoch += 1; 599 600 // Re-derive symmetric key 601 let member_keys: Vec<[u8; 32]> = circle.members.iter().map(|m| m.pubkey).collect(); 602 circle.symmetric_key = derive_circle_key(&circle.id, circle.epoch, &member_keys); 603 604 self.save_circle(&circle)?; 605 606 Ok(circle) 607 } 608 609 /// Accept an invitation to join a circle 610 /// 611 /// In a full implementation, this would fetch the circle state. 612 /// For now, it logs the acceptance. 613 pub fn accept_invite( 614 &self, 615 circle_id: [u8; 32], 616 inviter: [u8; 32], 617 _signature: Vec<u8>, 618 ) -> Result<(), TrustError> { 619 // TODO: Persist pending invite/membership state 620 // For now we just verify we don't have it and log 621 if self.get_circle(&circle_id)?.is_some() { 622 return Err(TrustError::AlreadyMember); 623 } 624 625 debug!( 626 circle = ?&circle_id[..8], 627 inviter = ?&inviter[..8], 628 "Accepted invite (logic pending state sync)" 629 ); 630 Ok(()) 631 } 632 633 /// Process a remote member joining 634 pub fn member_joined( 635 &self, 636 circle_id: [u8; 32], 637 member_key: [u8; 32], 638 epoch: u64, 639 ) -> Result<(), TrustError> { 640 let mut circle = match self.get_circle(&circle_id)? { 641 Some(c) => c, 642 None => return Ok(()), // Ignore updates for unknown circles 643 }; 644 645 if circle.members.iter().any(|m| m.pubkey == member_key) { 646 return Ok(()); 647 } 648 649 let timestamp = std::time::SystemTime::now() 650 .duration_since(std::time::UNIX_EPOCH) 651 .unwrap() 652 .as_millis() as u64; 653 654 // Add member with minimal known info 655 circle.members.push(CircleMember { 656 pubkey: member_key, 657 role: MemberRole::Member, 658 joined_at: timestamp, 659 invited_by: None, 660 invite_signature: None, 661 invite_depth: 0, 662 vouches_received: Vec::new(), 663 capacity: TrustCapacity::default(), 664 }); 665 666 circle.epoch = epoch; 667 668 // Update key 669 let member_keys: Vec<[u8; 32]> = circle.members.iter().map(|m| m.pubkey).collect(); 670 circle.symmetric_key = derive_circle_key(&circle.id, circle.epoch, &member_keys); 671 672 self.save_circle(&circle)?; 673 Ok(()) 674 } 675 676 /// Process a remote member leaving 677 pub fn member_left( 678 &self, 679 circle_id: [u8; 32], 680 member_key: [u8; 32], 681 ) -> Result<(), TrustError> { 682 // Reuse remove_member logic but ignore return value 683 match self.remove_member(&circle_id, &member_key) { 684 Ok(_) => Ok(()), 685 Err(TrustError::MemberNotFound) | Err(TrustError::CircleNotFound) => Ok(()), // Idempotent 686 Err(e) => Err(e), 687 } 688 } 689 690 // ───────────────────────────────────────────────────────────────────────── 691 // Trust Protocol 692 // ───────────────────────────────────────────────────────────────────────── 693 694 /// Vouch for an existing member in a circle (secondary endorsement) 695 pub fn vouch_member( 696 &self, 697 caller_pubkey: [u8; 32], 698 circle_id: &[u8; 32], 699 target_pubkey: [u8; 32], 700 signature: Vec<u8>, 701 ) -> Result<Circle, TrustError> { 702 let mut circle = self.get_circle(circle_id)? 703 .ok_or(TrustError::CircleNotFound)?; 704 705 let voucher_pubkey = caller_pubkey; 706 707 // Find voucher 708 let voucher_idx = circle.members.iter() 709 .position(|m| m.pubkey == voucher_pubkey) 710 .ok_or(TrustError::MemberNotFound)?; 711 712 // Check vouch capacity 713 if !circle.members[voucher_idx].can_vouch() { 714 return Err(TrustError::NoVouchCapacity); 715 } 716 717 // Find target 718 let target_idx = circle.members.iter() 719 .position(|m| m.pubkey == target_pubkey) 720 .ok_or(TrustError::MemberNotFound)?; 721 722 // Check if already vouched 723 let already_vouched = circle.members[target_idx].vouches_received 724 .iter() 725 .any(|v| v.voucher == voucher_pubkey && !v.revoked); 726 727 if already_vouched { 728 return Err(TrustError::AlreadyVouched); 729 } 730 731 let timestamp = std::time::SystemTime::now() 732 .duration_since(std::time::UNIX_EPOCH) 733 .unwrap() 734 .as_millis() as u64; 735 736 // Decrement voucher's capacity 737 circle.members[voucher_idx].capacity.vouches_remaining = 738 circle.members[voucher_idx].capacity.vouches_remaining.saturating_sub(1); 739 740 // Add vouch to target 741 let vouch = Vouch { 742 voucher: voucher_pubkey, 743 signature: signature.clone(), 744 timestamp, 745 revoked: false, 746 }; 747 circle.members[target_idx].vouches_received.push(vouch); 748 749 // Check for admin elevation 750 let active_vouches = circle.members[target_idx].vouches_received 751 .iter() 752 .filter(|v| !v.revoked) 753 .count(); 754 755 if active_vouches >= MIN_VOUCHES_FOR_ADMIN as usize 756 && circle.members[target_idx].role == MemberRole::Member 757 { 758 circle.members[target_idx].role = MemberRole::Admin; 759 debug!( 760 target = ?&target_pubkey[..8], 761 "Member elevated to Admin via {} vouches", active_vouches 762 ); 763 } 764 765 // Record in ledger 766 let ledger_entry = TrustLedgerEntry { 767 action: TrustAction::Vouch { 768 voucher: voucher_pubkey, 769 member: target_pubkey, 770 }, 771 timestamp, 772 signature, 773 }; 774 if circle.trust_policy.has_ledger() { 775 circle.trust_ledger.push(ledger_entry); 776 } 777 778 self.save_circle(&circle)?; 779 780 debug!( 781 circle_id = ?&circle_id[..8], 782 voucher = ?&voucher_pubkey[..8], 783 target = ?&target_pubkey[..8], 784 "Vouch recorded" 785 ); 786 787 Ok(circle) 788 } 789 790 /// Prune a member and optionally their invite descendants 791 pub fn prune_branch( 792 &self, 793 caller_pubkey: [u8; 32], 794 circle_id: &[u8; 32], 795 target_pubkey: [u8; 32], 796 signature: Vec<u8>, 797 _reason_hash: Option<[u8; 32]>, 798 ) -> Result<(Circle, Vec<[u8; 32]>), TrustError> { 799 let mut circle = self.get_circle(circle_id)? 800 .ok_or(TrustError::CircleNotFound)?; 801 802 let pruner_pubkey = caller_pubkey; 803 804 // Verify pruner has authority (founder or admin) 805 let pruner = circle.members.iter() 806 .find(|m| m.pubkey == pruner_pubkey) 807 .ok_or(TrustError::MemberNotFound)?; 808 809 if pruner.role != MemberRole::Founder && pruner.role != MemberRole::Admin { 810 return Err(TrustError::InsufficientAuthority); 811 } 812 813 // Cannot prune the founder 814 let target = circle.members.iter() 815 .find(|m| m.pubkey == target_pubkey) 816 .ok_or(TrustError::MemberNotFound)?; 817 818 if target.role == MemberRole::Founder { 819 return Err(TrustError::CannotPruneFounder); 820 } 821 822 let timestamp = std::time::SystemTime::now() 823 .duration_since(std::time::UNIX_EPOCH) 824 .unwrap() 825 .as_millis() as u64; 826 827 // Determine who to remove based on prune mode 828 let prune_mode = circle.trust_policy.prune_mode(); 829 let to_remove: Vec<[u8; 32]> = match prune_mode { 830 PruneMode::Cascade => { 831 // Remove target + all invite descendants 832 let mut remove_set = vec![target_pubkey]; 833 remove_set.extend(self.collect_invite_descendants(&circle, target_pubkey)); 834 remove_set 835 } 836 PruneMode::Orphan | PruneMode::Reassign => { 837 // Only remove the target 838 vec![target_pubkey] 839 } 840 PruneMode::Voluntary => { 841 // Cannot forcibly prune in voluntary mode 842 return Err(TrustError::InsufficientAuthority); 843 } 844 }; 845 846 // Remove members 847 let removed_count = to_remove.len(); 848 circle.members.retain(|m| !to_remove.contains(&m.pubkey)); 849 850 // Record in ledger 851 let ledger_entry = TrustLedgerEntry { 852 action: TrustAction::Prune { 853 admin: pruner_pubkey, 854 target: target_pubkey, 855 descendants_removed: (removed_count - 1) as u32, 856 }, 857 timestamp, 858 signature, 859 }; 860 if circle.trust_policy.has_ledger() { 861 circle.trust_ledger.push(ledger_entry); 862 } 863 864 circle.epoch += 1; 865 866 // Re-derive symmetric key 867 let member_keys: Vec<[u8; 32]> = circle.members.iter().map(|m| m.pubkey).collect(); 868 circle.symmetric_key = derive_circle_key(&circle.id, circle.epoch, &member_keys); 869 870 self.save_circle(&circle)?; 871 872 debug!( 873 circle_id = ?&circle_id[..8], 874 pruner = ?&pruner_pubkey[..8], 875 target = ?&target_pubkey[..8], 876 removed = removed_count, 877 "Pruned branch" 878 ); 879 880 Ok((circle, to_remove)) 881 } 882 883 /// Collect all descendants invited by a member (for cascade prune) 884 fn collect_invite_descendants(&self, circle: &Circle, root: [u8; 32]) -> Vec<[u8; 32]> { 885 let mut descendants = Vec::new(); 886 let mut to_process = vec![root]; 887 888 while let Some(current) = to_process.pop() { 889 for member in &circle.members { 890 if member.invited_by == Some(current) && !descendants.contains(&member.pubkey) { 891 descendants.push(member.pubkey); 892 to_process.push(member.pubkey); 893 } 894 } 895 } 896 897 descendants 898 } 899 } 900 901 #[cfg(test)] 902 mod tests { 903 use super::*; 904 905 #[test] 906 fn test_trust_capacity_default() { 907 let cap = TrustCapacity::default(); 908 assert_eq!(cap.invites_remaining, MAX_INVITE_CAPACITY); 909 assert_eq!(cap.vouches_remaining, MAX_VOUCH_CAPACITY); 910 } 911 912 #[test] 913 fn test_circle_member_founder() { 914 let member = CircleMember::new_founder([1u8; 32], 1000); 915 assert_eq!(member.role, MemberRole::Founder); 916 assert_eq!(member.invite_depth, 0); 917 assert!(member.invited_by.is_none()); 918 assert!(member.can_invite()); 919 } 920 921 #[test] 922 fn test_circle_member_invited() { 923 let member = CircleMember::new_invited( 924 [2u8; 32], 925 MemberRole::Member, 926 2000, 927 [1u8; 32], 928 vec![0u8; 64], 929 1, 930 ); 931 assert_eq!(member.role, MemberRole::Member); 932 assert_eq!(member.invite_depth, 1); 933 assert!(member.invited_by.is_some()); 934 } 935 936 #[test] 937 fn test_trust_policy_defaults() { 938 assert_eq!(TrustPolicy::default(), TrustPolicy::Anonymous); 939 assert!(!TrustPolicy::Anonymous.records_invite_tree()); 940 assert!(TrustPolicy::accountable().records_invite_tree()); 941 assert!(TrustPolicy::private().records_vouches()); 942 } 943 944 #[test] 945 fn test_prune_mode_for_policies() { 946 assert_eq!(TrustPolicy::Anonymous.prune_mode(), PruneMode::Voluntary); 947 assert_eq!(TrustPolicy::accountable().prune_mode(), PruneMode::Cascade); 948 assert_eq!(TrustPolicy::private().prune_mode(), PruneMode::Orphan); 949 } 950 }