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}