aura_core/effects/availability.rs
1//! Layer 1: Data Availability Effect Trait Definitions
2//!
3//! This module defines the pure effect trait interface for data availability
4//! within replication units (homes, neighborhoods). Data availability is
5//! scoped to the unit that owns the data:
6//! - Blocks provide DA for home-level shared data
7//! - Neighborhoods provide DA for neighborhood-level shared data
8//!
9//! **Effect Classification**: Application Effect
10//! - Implemented by protocol crates (aura-protocol provides home/neighborhood implementations)
11//! - Used by feature crates (aura-social, aura-chat) for content retrieval
12//! - Core trait definition belongs in Layer 1 (foundation)
13//!
14//! # Design Principles
15//!
16//! **Full replication within unit**: All members of a unit replicate all data.
17//! No partial replication or erasure coding in v1.
18//!
19//! **Availability = reachability**: If you can reach any member, you can
20//! retrieve any data. No threshold attestation required.
21//!
22//! **Consent follows structure**: Unit membership implies consent to
23//! replication duties.
24
25use crate::{domain::content::Hash32, types::identifiers::AuthorityId};
26use async_trait::async_trait;
27use serde::{Deserialize, Serialize};
28use std::hash::Hash;
29
30/// Error type for data availability operations.
31///
32/// These errors represent failures in the data availability layer,
33/// from local storage issues to network retrieval failures.
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub enum AvailabilityError {
36 /// Content not found in the specified unit.
37 ///
38 /// The content hash was not found locally or from any reachable peer.
39 NotFound {
40 /// The hash of the content that wasn't found
41 hash: Hash32,
42 },
43
44 /// Unit storage capacity exceeded.
45 ///
46 /// The store operation would exceed the unit's storage limit.
47 CapacityExceeded {
48 /// Currently used storage in bytes
49 used: u64,
50 /// Maximum storage limit in bytes
51 limit: u64,
52 /// Size of content that was rejected
53 requested: u64,
54 },
55
56 /// No peers reachable for retrieval.
57 ///
58 /// Content wasn't available locally and no replication peers
59 /// could be contacted.
60 NoReachablePeers {
61 /// Number of peers that were tried
62 peers_tried: u32,
63 },
64
65 /// Network error during peer retrieval.
66 ///
67 /// Communication with a peer failed during content retrieval.
68 NetworkError(String),
69
70 /// Unit not found or invalid.
71 ///
72 /// The specified unit ID doesn't exist or isn't accessible.
73 InvalidUnit(String),
74
75 /// Storage backend error.
76 ///
77 /// The local storage system returned an error.
78 StorageError(String),
79}
80
81impl std::fmt::Display for AvailabilityError {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 match self {
84 Self::NotFound { hash } => {
85 write!(f, "content not found: {hash}")
86 }
87 Self::CapacityExceeded {
88 used,
89 limit,
90 requested,
91 } => {
92 write!(
93 f,
94 "capacity exceeded: {used}/{limit} bytes used, {requested} bytes requested"
95 )
96 }
97 Self::NoReachablePeers { peers_tried } => {
98 write!(f, "no reachable peers (tried {peers_tried})")
99 }
100 Self::NetworkError(msg) => write!(f, "network error: {msg}"),
101 Self::InvalidUnit(msg) => write!(f, "invalid unit: {msg}"),
102 Self::StorageError(msg) => write!(f, "storage error: {msg}"),
103 }
104 }
105}
106
107impl std::error::Error for AvailabilityError {}
108
109/// Effect trait for data availability within a replication unit.
110///
111/// This trait defines the interface for storing and retrieving content
112/// from a fully-replicated unit (home or neighborhood). All members
113/// of the unit maintain complete copies of the data.
114///
115/// # Type Parameters
116///
117/// The associated `UnitId` type identifies the replication unit. For blocks,
118/// this is `HomeId`. For neighborhoods, this is `NeighborhoodId`.
119///
120/// # Implementation Notes
121///
122/// Implementations should:
123/// - Try local storage first for retrieval
124/// - Fall back to peers in deterministic order (for reproducibility)
125/// - Cache retrieved content locally
126/// - Enforce storage capacity limits on store operations
127/// - Replication happens via journal sync, not explicit push
128///
129/// # Example
130///
131/// ```ignore
132/// // Home availability implementation
133/// impl DataAvailability for HomeAvailability {
134/// type UnitId = HomeId;
135///
136/// fn replication_peers(&self, _unit: HomeId) -> Vec<AuthorityId> {
137/// self.members.iter()
138/// .filter(|a| *a != &self.local_authority)
139/// .copied()
140/// .collect()
141/// }
142/// // ...
143/// }
144/// ```
145#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
146#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
147pub trait DataAvailability: Send + Sync {
148 /// The identifier type for replication units.
149 ///
150 /// For homes: `HomeId`
151 /// For neighborhoods: `NeighborhoodId`
152 type UnitId: Copy + Eq + Hash + Send + Sync;
153
154 /// List peers who replicate this unit's data.
155 ///
156 /// Returns all authorities that maintain copies of the unit's data,
157 /// excluding the local authority.
158 ///
159 /// # Arguments
160 /// * `unit` - The unit to query peers for
161 ///
162 /// # Returns
163 /// A list of authority IDs that replicate this unit
164 fn replication_peers(&self, unit: Self::UnitId) -> Vec<AuthorityId>;
165
166 /// Check if content is available in local storage.
167 ///
168 /// This is a fast check that doesn't contact peers.
169 ///
170 /// # Arguments
171 /// * `unit` - The unit the content belongs to
172 /// * `hash` - The content hash to check
173 ///
174 /// # Returns
175 /// `true` if the content is available locally
176 async fn is_locally_available(&self, unit: Self::UnitId, hash: &Hash32) -> bool;
177
178 /// Retrieve content from local storage only.
179 ///
180 /// Does not contact peers. Returns `None` if not available locally.
181 ///
182 /// # Arguments
183 /// * `unit` - The unit the content belongs to
184 /// * `hash` - The content hash to retrieve
185 ///
186 /// # Returns
187 /// The content bytes if available locally, `None` otherwise
188 async fn retrieve_local(&self, unit: Self::UnitId, hash: &Hash32) -> Option<Vec<u8>>;
189
190 /// Retrieve content from the unit (local or peers).
191 ///
192 /// Tries local storage first, then contacts peers in deterministic
193 /// order until the content is found. Caches retrieved content locally.
194 ///
195 /// # Arguments
196 /// * `unit` - The unit the content belongs to
197 /// * `hash` - The content hash to retrieve
198 ///
199 /// # Returns
200 /// The content bytes
201 ///
202 /// # Errors
203 /// - `NotFound` if content isn't available from any source
204 /// - `NoReachablePeers` if local retrieval failed and no peers responded
205 /// - `NetworkError` if peer communication failed
206 async fn retrieve(
207 &self,
208 unit: Self::UnitId,
209 hash: &Hash32,
210 ) -> Result<Vec<u8>, AvailabilityError>;
211
212 /// Store content in the unit.
213 ///
214 /// Stores content locally. Replication to peers happens via
215 /// journal synchronization, not explicit push.
216 ///
217 /// # Arguments
218 /// * `unit` - The unit to store content in
219 /// * `content` - The content bytes to store
220 ///
221 /// # Returns
222 /// The hash of the stored content
223 ///
224 /// # Errors
225 /// - `CapacityExceeded` if the unit's storage limit would be exceeded
226 /// - `StorageError` if local storage failed
227 async fn store(&self, unit: Self::UnitId, content: &[u8]) -> Result<Hash32, AvailabilityError>;
228}
229
230/// Blanket implementation for Arc<T> where T: DataAvailability
231#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
232#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
233impl<T: DataAvailability + ?Sized> DataAvailability for std::sync::Arc<T> {
234 type UnitId = T::UnitId;
235
236 fn replication_peers(&self, unit: Self::UnitId) -> Vec<AuthorityId> {
237 (**self).replication_peers(unit)
238 }
239
240 async fn is_locally_available(&self, unit: Self::UnitId, hash: &Hash32) -> bool {
241 (**self).is_locally_available(unit, hash).await
242 }
243
244 async fn retrieve_local(&self, unit: Self::UnitId, hash: &Hash32) -> Option<Vec<u8>> {
245 (**self).retrieve_local(unit, hash).await
246 }
247
248 async fn retrieve(
249 &self,
250 unit: Self::UnitId,
251 hash: &Hash32,
252 ) -> Result<Vec<u8>, AvailabilityError> {
253 (**self).retrieve(unit, hash).await
254 }
255
256 async fn store(&self, unit: Self::UnitId, content: &[u8]) -> Result<Hash32, AvailabilityError> {
257 (**self).store(unit, content).await
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn test_availability_error_display() {
267 let hash = Hash32::from([0u8; 32]);
268
269 let not_found = AvailabilityError::NotFound { hash };
270 assert!(not_found.to_string().contains("not found"));
271
272 let capacity = AvailabilityError::CapacityExceeded {
273 used: 1000,
274 limit: 1000,
275 requested: 100,
276 };
277 assert!(capacity.to_string().contains("capacity exceeded"));
278
279 let no_peers = AvailabilityError::NoReachablePeers { peers_tried: 5 };
280 assert!(no_peers.to_string().contains("no reachable peers"));
281
282 let network = AvailabilityError::NetworkError("timeout".to_string());
283 assert!(network.to_string().contains("network error"));
284 }
285
286 #[test]
287 fn test_availability_error_equality() {
288 let hash = Hash32::from([1u8; 32]);
289 let e1 = AvailabilityError::NotFound { hash };
290 let e2 = AvailabilityError::NotFound { hash };
291 assert_eq!(e1, e2);
292
293 let e3 = AvailabilityError::NoReachablePeers { peers_tried: 3 };
294 assert_ne!(e1, e3);
295 }
296}