/ abzu-core / src / trust.rs
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(&timestamp.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  }