Skip to main content

acyclic_fs/
foundation.rs

1//! Portable identities and authority contracts.
2
3use bytes::Bytes;
4use serde::{Deserialize, Serialize};
5
6/// Widens one native collection size into the durable 64-bit accounting domain.
7///
8/// Rust supports pointer widths no larger than 64 bits, so this conversion is
9/// an invariant rather than a recoverable filesystem outcome on every native
10/// and WebAssembly target supported by this crate.
11#[inline]
12pub(crate) fn usize_to_u64(value: usize) -> u64 {
13    value as u64
14}
15use std::fmt;
16use thiserror::Error;
17use uuid::Uuid;
18
19/// Stable identity of one independently fenced authority stream.
20#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
21#[serde(transparent)]
22pub struct AuthorityId(Uuid);
23
24impl AuthorityId {
25    /// Creates a fresh time-ordered authority identity.
26    #[must_use]
27    pub fn new() -> Self {
28        Self(Uuid::now_v7())
29    }
30
31    /// Constructs an identity from its canonical 16 bytes.
32    #[must_use]
33    pub const fn from_bytes(bytes: [u8; 16]) -> Self {
34        Self(Uuid::from_bytes(bytes))
35    }
36
37    /// Returns the canonical bytes.
38    #[must_use]
39    pub const fn into_bytes(self) -> [u8; 16] {
40        self.0.into_bytes()
41    }
42}
43
44impl Default for AuthorityId {
45    fn default() -> Self {
46        Self::new()
47    }
48}
49
50/// Stable idempotency identity for one authority operation.
51#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
52#[serde(transparent)]
53pub struct OperationId(Uuid);
54
55impl OperationId {
56    /// Creates a fresh time-ordered operation identity.
57    #[must_use]
58    pub fn new() -> Self {
59        Self(Uuid::now_v7())
60    }
61
62    /// Constructs an identity from its canonical 16 bytes.
63    #[must_use]
64    pub const fn from_bytes(bytes: [u8; 16]) -> Self {
65        Self(Uuid::from_bytes(bytes))
66    }
67
68    /// Returns the canonical bytes.
69    #[must_use]
70    pub const fn into_bytes(self) -> [u8; 16] {
71        self.0.into_bytes()
72    }
73}
74
75impl Default for OperationId {
76    fn default() -> Self {
77        Self::new()
78    }
79}
80
81/// Monotonic authority incarnation used to fence stale writers.
82#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
83#[serde(transparent)]
84pub struct Epoch(u64);
85
86impl Epoch {
87    /// Genesis authority epoch.
88    pub const GENESIS: Self = Self(1);
89
90    /// Constructs a non-zero epoch.
91    ///
92    /// # Errors
93    ///
94    /// Returns [`IdentityError::ZeroEpoch`] when `value` is zero.
95    pub const fn new(value: u64) -> Result<Self, IdentityError> {
96        if value == 0 {
97            Err(IdentityError::ZeroEpoch)
98        } else {
99            Ok(Self(value))
100        }
101    }
102
103    /// Returns the wire integer.
104    #[must_use]
105    pub const fn get(self) -> u64 {
106        self.0
107    }
108}
109
110/// Dense one-based sequence within an authority history; zero denotes genesis.
111#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
112#[serde(transparent)]
113pub struct Sequence(u64);
114
115impl Sequence {
116    /// Empty stream position.
117    pub const GENESIS: Self = Self(0);
118
119    /// Constructs a sequence from its wire integer.
120    #[must_use]
121    pub const fn new(value: u64) -> Self {
122        Self(value)
123    }
124
125    /// Returns the wire integer.
126    #[must_use]
127    pub const fn get(self) -> u64 {
128        self.0
129    }
130
131    /// Returns the next sequence, failing closed at exhaustion.
132    ///
133    /// # Errors
134    ///
135    /// Returns [`IdentityError::SequenceExhausted`] at `u64::MAX`.
136    pub const fn checked_next(self) -> Result<Self, IdentityError> {
137        match self.0.checked_add(1) {
138            Some(value) => Ok(Self(value)),
139            None => Err(IdentityError::SequenceExhausted),
140        }
141    }
142}
143
144/// Canonical 256-bit digest.
145#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
146#[serde(transparent)]
147pub struct Digest([u8; 32]);
148
149impl Digest {
150    /// Digest used only for an empty/genesis predecessor.
151    pub const ZERO: Self = Self([0; 32]);
152
153    /// Constructs a digest from canonical bytes.
154    #[must_use]
155    pub const fn from_bytes(bytes: [u8; 32]) -> Self {
156        Self(bytes)
157    }
158
159    /// Borrows canonical bytes.
160    #[must_use]
161    pub const fn as_bytes(&self) -> &[u8; 32] {
162        &self.0
163    }
164
165    /// Returns canonical bytes.
166    #[must_use]
167    pub const fn into_bytes(self) -> [u8; 32] {
168        self.0
169    }
170}
171
172impl fmt::Debug for Digest {
173    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
174        formatter
175            .debug_tuple("Digest")
176            .field(&hex::encode(self.0))
177            .finish()
178    }
179}
180
181macro_rules! uuid_identity {
182    ($name:ident, $description:literal) => {
183        #[doc = $description]
184        #[derive(
185            Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize,
186        )]
187        #[serde(transparent)]
188        pub struct $name(Uuid);
189
190        impl $name {
191            /// Creates a fresh time-ordered identity.
192            #[must_use]
193            pub fn new() -> Self {
194                Self(Uuid::now_v7())
195            }
196
197            /// Constructs an identity from its canonical 16 bytes.
198            #[must_use]
199            pub const fn from_bytes(bytes: [u8; 16]) -> Self {
200                Self(Uuid::from_bytes(bytes))
201            }
202
203            /// Returns the canonical bytes.
204            #[must_use]
205            pub const fn into_bytes(self) -> [u8; 16] {
206                self.0.into_bytes()
207            }
208        }
209
210        impl Default for $name {
211            fn default() -> Self {
212                Self::new()
213            }
214        }
215    };
216}
217
218uuid_identity!(
219    VolumeId,
220    "Stable identity of one independently versioned volume."
221);
222uuid_identity!(
223    CheckoutId,
224    "Process-independent identity of one volume checkout."
225);
226uuid_identity!(MountId, "Stable identity of one mounted-volume binding.");
227uuid_identity!(
228    WatchId,
229    "Process-local identity of one native watcher handle."
230);
231uuid_identity!(
232    FileId,
233    "Stable path-independent identity of one file record."
234);
235
236/// Content-addressed identity of one immutable volume generation.
237#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
238#[serde(transparent)]
239pub struct GenerationId(Digest);
240
241impl GenerationId {
242    /// Constructs a generation identity from its authenticated root digest.
243    #[must_use]
244    pub const fn new(digest: Digest) -> Self {
245        Self(digest)
246    }
247
248    /// Returns the authenticated root digest.
249    #[must_use]
250    pub const fn digest(self) -> Digest {
251        self.0
252    }
253}
254
255/// Durable authority head.
256#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
257pub struct Head {
258    /// Current fencing epoch.
259    pub epoch: Epoch,
260    /// Last committed sequence.
261    pub sequence: Sequence,
262    /// Hash-chain head, or [`Digest::ZERO`] at genesis.
263    pub digest: Digest,
264}
265
266impl Head {
267    /// Empty head for a new authority.
268    #[must_use]
269    pub const fn genesis(epoch: Epoch) -> Self {
270        Self {
271            epoch,
272            sequence: Sequence::GENESIS,
273            digest: Digest::ZERO,
274        }
275    }
276}
277
278/// One canonical opaque operation submitted to authority.
279#[derive(Clone, Debug, Eq, PartialEq)]
280pub struct ProposedCommit {
281    /// Stable retry identity.
282    pub operation_id: OperationId,
283    /// Digest of the complete semantic command and caller-visible preconditions.
284    pub fingerprint: Digest,
285    /// Canonical schema-versioned operation bytes.
286    pub payload: Bytes,
287}
288
289/// One recovered or newly durable authority commit.
290#[derive(Clone, Debug, Eq, PartialEq)]
291pub struct DurableCommit {
292    /// Writer-fencing epoch that admitted this commit.
293    pub epoch: Epoch,
294    /// Assigned dense sequence.
295    pub sequence: Sequence,
296    /// Stable retry identity.
297    pub operation_id: OperationId,
298    /// Original command fingerprint.
299    pub fingerprint: Digest,
300    /// Previous hash-chain head.
301    pub previous_digest: Digest,
302    /// Commit hash-chain head.
303    pub digest: Digest,
304    /// Canonical schema-versioned operation bytes.
305    pub payload: Bytes,
306}
307
308/// Canonical non-payload bytes hashed into one authority commit identity.
309pub const AUTHORITY_COMMIT_DIGEST_ENVELOPE_BYTES: u64 = 151;
310
311/// Computes the canonical hash-chain digest for one durable authority commit.
312///
313/// This function is the backend conformance boundary: every authority store
314/// must produce this exact digest for the same canonical operation.
315#[must_use]
316pub fn authority_commit_digest(
317    authority_id: AuthorityId,
318    epoch: Epoch,
319    sequence: Sequence,
320    operation_id: OperationId,
321    fingerprint: Digest,
322    previous_digest: Digest,
323    payload: &[u8],
324) -> Digest {
325    const DOMAIN: &[u8] = b"acyclic-fs-authority-commit-v1\0";
326    let mut hasher = blake3::Hasher::new();
327    hasher.update(DOMAIN);
328    hasher.update(&authority_id.into_bytes());
329    hasher.update(&epoch.get().to_le_bytes());
330    hasher.update(&sequence.get().to_le_bytes());
331    hasher.update(&operation_id.into_bytes());
332    hasher.update(fingerprint.as_bytes());
333    hasher.update(previous_digest.as_bytes());
334    hasher.update(
335        &u64::try_from(payload.len())
336            .unwrap_or(u64::MAX)
337            .to_le_bytes(),
338    );
339    hasher.update(payload);
340    Digest::from_bytes(*hasher.finalize().as_bytes())
341}
342
343/// Errors constructing portable identities.
344#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
345pub enum IdentityError {
346    /// Epoch zero is reserved and never valid.
347    #[error("authority epoch must be non-zero")]
348    ZeroEpoch,
349    /// No further sequence can be represented.
350    #[error("authority sequence exhausted")]
351    SequenceExhausted,
352}
353
354#[cfg(test)]
355#[path = "tests/foundation.rs"]
356mod tests;