Skip to main content

aura_core/effects/
relay.rs

1//! Layer 1: Relay Selection Effect Trait Definitions
2//!
3//! This module defines the pure trait interface for relay selection in Aura's
4//! social infrastructure. Relay is **neighborhood-scoped**: both home peers
5//! and neighborhood peers can relay for anyone in the neighborhood.
6//!
7//! **Effect Classification**: Application Effect
8//! - Implemented by transport crates (aura-transport provides deterministic selector)
9//! - Used by protocol layer (aura-protocol) for relay orchestration
10//! - Core trait definition belongs in Layer 1 (foundation)
11//!
12//! # Design Principles
13//!
14//! **Tiered selection**: Home peers are preferred (closest trust), then
15//! neighborhood peers, then guardians as fallback.
16//!
17//! **Deterministic**: Selection uses `hash(context_id, epoch, nonce)` for
18//! reproducible results in testing and simulation.
19//!
20//! **Neighborhood-scoped**: All relay relationships operate at neighborhood
21//! scope - anyone in the neighborhood can relay for anyone else.
22
23use crate::types::identifiers::{AuthorityId, ContextId};
24use serde::{Deserialize, Serialize};
25
26/// Context for relay selection decisions.
27///
28/// Contains all information needed to select appropriate relay nodes
29/// for a message. The context is used to compute deterministic selection.
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct RelayContext {
32    /// The relational context scoping this relay operation.
33    pub context_id: ContextId,
34    /// The authority sending the message.
35    pub source: AuthorityId,
36    /// The authority receiving the message.
37    pub destination: AuthorityId,
38    /// Current epoch (for deterministic selection).
39    pub epoch: u64,
40    /// Message nonce (for deterministic randomness).
41    ///
42    /// Combined with context_id and epoch, this ensures different messages
43    /// select different relays while remaining reproducible.
44    pub nonce: [u8; 32],
45}
46
47impl RelayContext {
48    /// Create a new relay context.
49    pub fn new(
50        context_id: ContextId,
51        source: AuthorityId,
52        destination: AuthorityId,
53        epoch: u64,
54        nonce: [u8; 32],
55    ) -> Self {
56        Self {
57            context_id,
58            source,
59            destination,
60            epoch,
61            nonce,
62        }
63    }
64}
65
66/// How we know a potential relay peer.
67///
68/// Relay capability derives from social relationships. The relationship
69/// type affects selection priority: home peers are preferred over
70/// neighborhood peers, which are preferred over guardians.
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
72pub enum RelayRelationship {
73    /// Co-member in the same home.
74    ///
75    /// Home peers share home context and have high mutual trust.
76    /// They can relay for any member of shared neighborhoods.
77    SameHome {
78        /// The home ID (opaque 32-byte identifier)
79        home_id: [u8; 32],
80    },
81
82    /// Member of an adjacent home in a shared neighborhood.
83    ///
84    /// Neighborhood peers share neighborhood context and have
85    /// established traversal rights. They can relay for any
86    /// member of the neighborhood.
87    NeighborhoodHop {
88        /// The neighborhood ID (opaque 32-byte identifier)
89        neighborhood_id: [u8; 32],
90    },
91
92    /// Designated guardian with explicit relay capability.
93    ///
94    /// Guardians are the fallback when social topology doesn't
95    /// provide a relay path. They have explicit capability grants
96    /// for relay operations.
97    Guardian,
98}
99
100impl RelayRelationship {
101    /// Get the priority of this relationship type.
102    ///
103    /// Lower values are higher priority (selected first).
104    pub fn priority(&self) -> u8 {
105        match self {
106            Self::SameHome { .. } => 0,        // Highest priority
107            Self::NeighborhoodHop { .. } => 1, // Medium priority
108            Self::Guardian => 2,               // Fallback
109        }
110    }
111
112    /// Check if this is a home peer relationship.
113    pub fn is_same_home_member(&self) -> bool {
114        matches!(self, Self::SameHome { .. })
115    }
116
117    /// Check if this is a neighborhood peer relationship.
118    pub fn is_neighborhood_hop_member(&self) -> bool {
119        matches!(self, Self::NeighborhoodHop { .. })
120    }
121
122    /// Check if this is a guardian relationship.
123    pub fn is_guardian(&self) -> bool {
124        matches!(self, Self::Guardian)
125    }
126}
127
128/// A candidate relay peer.
129///
130/// Contains information about a potential relay, including the
131/// relationship type and current reachability status.
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133pub struct RelayCandidate {
134    /// The potential relay authority.
135    pub authority_id: AuthorityId,
136    /// How we know this relay (determines selection priority).
137    pub relationship: RelayRelationship,
138    /// Is this peer currently reachable?
139    ///
140    /// Unreachable peers are excluded from selection but may
141    /// be included in fallback lists.
142    pub reachable: bool,
143}
144
145impl RelayCandidate {
146    /// Create a new relay candidate.
147    pub fn new(
148        authority_id: AuthorityId,
149        relationship: RelayRelationship,
150        reachable: bool,
151    ) -> Self {
152        Self {
153            authority_id,
154            relationship,
155            reachable,
156        }
157    }
158
159    /// Create a reachable home peer candidate.
160    pub fn block_peer(authority_id: AuthorityId, home_id: [u8; 32]) -> Self {
161        Self::new(authority_id, RelayRelationship::SameHome { home_id }, true)
162    }
163
164    /// Create a reachable neighborhood peer candidate.
165    pub fn neighborhood_hop_member(authority_id: AuthorityId, neighborhood_id: [u8; 32]) -> Self {
166        Self::new(
167            authority_id,
168            RelayRelationship::NeighborhoodHop { neighborhood_id },
169            true,
170        )
171    }
172
173    /// Create a reachable guardian candidate.
174    pub fn guardian(authority_id: AuthorityId) -> Self {
175        Self::new(authority_id, RelayRelationship::Guardian, true)
176    }
177}
178
179/// Error type for relay operations.
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181pub enum RelayError {
182    /// No relay candidates available.
183    NoCandidates,
184
185    /// All relays failed.
186    AllRelaysFailed {
187        /// Number of relays that were tried
188        relays_tried: u32,
189    },
190
191    /// Relay rejected the request.
192    RelayRejected {
193        /// The relay that rejected
194        relay: AuthorityId,
195        /// Reason for rejection
196        reason: String,
197    },
198
199    /// Budget exhausted for relay operations.
200    BudgetExhausted,
201
202    /// Network error during relay.
203    NetworkError(String),
204}
205
206impl std::fmt::Display for RelayError {
207    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208        match self {
209            Self::NoCandidates => write!(f, "no relay candidates available"),
210            Self::AllRelaysFailed { relays_tried } => {
211                write!(f, "all {relays_tried} relays failed")
212            }
213            Self::RelayRejected { relay, reason } => {
214                write!(f, "relay {relay} rejected: {reason}")
215            }
216            Self::BudgetExhausted => write!(f, "relay budget exhausted"),
217            Self::NetworkError(msg) => write!(f, "network error: {msg}"),
218        }
219    }
220}
221
222impl std::error::Error for RelayError {}
223
224/// Strategy for selecting relay nodes.
225///
226/// Implementations determine how relays are selected from candidates.
227/// The default implementation uses deterministic random selection
228/// with tier-based priority.
229///
230/// # Implementation Notes
231///
232/// Implementations should:
233/// - Filter out unreachable candidates (unless building fallback lists)
234/// - Prefer candidates by relationship priority (home > neighborhood > guardian)
235/// - Use deterministic selection for reproducibility in tests
236/// - Return an ordered list: first choice, then fallbacks
237///
238/// # Example
239///
240/// ```ignore
241/// // Deterministic random selection
242/// impl RelaySelector for DeterministicRandomSelector {
243///     fn select(&self, context: &RelayContext, candidates: &[RelayCandidate]) -> Vec<AuthorityId> {
244///         let reachable: Vec<_> = candidates.iter().filter(|c| c.reachable).collect();
245///         let seed = hash(&[context.context_id, context.epoch, context.nonce]);
246///         select_by_tiers(&reachable, &seed)
247///     }
248/// }
249/// ```
250pub trait RelaySelector: Send + Sync {
251    /// Select relay(s) from candidates.
252    ///
253    /// Returns an ordered list of relay authorities to try:
254    /// - First element is the primary relay
255    /// - Subsequent elements are fallbacks in order of preference
256    ///
257    /// # Arguments
258    /// * `context` - The relay context (used for deterministic selection)
259    /// * `candidates` - Available relay candidates
260    ///
261    /// # Returns
262    /// An ordered list of authority IDs to use as relays.
263    /// Empty if no suitable candidates are available.
264    fn select(&self, context: &RelayContext, candidates: &[RelayCandidate]) -> Vec<AuthorityId>;
265}
266
267/// Blanket implementation for Arc<T> where T: RelaySelector
268impl<T: RelaySelector + ?Sized> RelaySelector for std::sync::Arc<T> {
269    fn select(&self, context: &RelayContext, candidates: &[RelayCandidate]) -> Vec<AuthorityId> {
270        (**self).select(context, candidates)
271    }
272}
273
274/// Blanket implementation for Box<T> where T: RelaySelector
275impl<T: RelaySelector + ?Sized> RelaySelector for Box<T> {
276    fn select(&self, context: &RelayContext, candidates: &[RelayCandidate]) -> Vec<AuthorityId> {
277        (**self).select(context, candidates)
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284    use uuid::Uuid;
285
286    fn test_authority() -> AuthorityId {
287        AuthorityId::from_uuid(Uuid::from_bytes([1u8; 16]))
288    }
289
290    fn test_context() -> RelayContext {
291        RelayContext::new(
292            ContextId::from_uuid(Uuid::from_bytes([2u8; 16])),
293            AuthorityId::from_uuid(Uuid::from_bytes([3u8; 16])),
294            AuthorityId::from_uuid(Uuid::from_bytes([4u8; 16])),
295            1,
296            [0u8; 32],
297        )
298    }
299
300    #[test]
301    fn test_relay_relationship_priority() {
302        let home_rel = RelayRelationship::SameHome { home_id: [1u8; 32] };
303        let neighborhood = RelayRelationship::NeighborhoodHop {
304            neighborhood_id: [2u8; 32],
305        };
306        let guardian = RelayRelationship::Guardian;
307
308        assert!(home_rel.priority() < neighborhood.priority());
309        assert!(neighborhood.priority() < guardian.priority());
310    }
311
312    #[test]
313    fn test_relay_relationship_checks() {
314        let home_rel = RelayRelationship::SameHome { home_id: [1u8; 32] };
315        assert!(home_rel.is_same_home_member());
316        assert!(!home_rel.is_neighborhood_hop_member());
317        assert!(!home_rel.is_guardian());
318
319        let neighborhood = RelayRelationship::NeighborhoodHop {
320            neighborhood_id: [2u8; 32],
321        };
322        assert!(!neighborhood.is_same_home_member());
323        assert!(neighborhood.is_neighborhood_hop_member());
324        assert!(!neighborhood.is_guardian());
325
326        let guardian = RelayRelationship::Guardian;
327        assert!(!guardian.is_same_home_member());
328        assert!(!guardian.is_neighborhood_hop_member());
329        assert!(guardian.is_guardian());
330    }
331
332    #[test]
333    fn test_relay_candidate_constructors() {
334        let auth = test_authority();
335
336        let block_peer = RelayCandidate::block_peer(auth, [1u8; 32]);
337        assert!(block_peer.reachable);
338        assert!(block_peer.relationship.is_same_home_member());
339
340        let neighborhood_hop_member = RelayCandidate::neighborhood_hop_member(auth, [2u8; 32]);
341        assert!(neighborhood_hop_member.reachable);
342        assert!(neighborhood_hop_member
343            .relationship
344            .is_neighborhood_hop_member());
345
346        let guardian = RelayCandidate::guardian(auth);
347        assert!(guardian.reachable);
348        assert!(guardian.relationship.is_guardian());
349    }
350
351    #[test]
352    fn test_relay_context_creation() {
353        let ctx = test_context();
354        assert_eq!(ctx.epoch, 1);
355        assert_eq!(ctx.nonce, [0u8; 32]);
356    }
357
358    #[test]
359    fn test_relay_error_display() {
360        let no_candidates = RelayError::NoCandidates;
361        assert!(no_candidates.to_string().contains("no relay candidates"));
362
363        let all_failed = RelayError::AllRelaysFailed { relays_tried: 3 };
364        assert!(all_failed.to_string().contains("3 relays failed"));
365
366        let rejected = RelayError::RelayRejected {
367            relay: test_authority(),
368            reason: "busy".to_string(),
369        };
370        assert!(rejected.to_string().contains("rejected"));
371
372        let exhausted = RelayError::BudgetExhausted;
373        assert!(exhausted.to_string().contains("budget exhausted"));
374    }
375}