Skip to main content

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}