minerva 0.2.0

Causal ordering for distributed systems
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
extern crate alloc;

use alloc::vec::Vec;
use core::num::NonZeroU64;

use crate::kairos::{KAIROS_WIRE_LEN, Kairos};

use super::super::Dot;
use super::super::rhapsody::{Anchor, Locus};
use super::{Metatheses, Metathesis};
use crate::metis::dot::RawDot;

mod error;

pub use error::{MetathesesDecodeBudget, MetathesesDecodeError};

/// Version tag for the canonical [`Metatheses`] wire encoding (S272, ruling
/// R-56). A distinct version space from the rhapsody frames (PRD 0017 /
/// PRD 0023), the have-set frame (PRD 0016), the `VersionVector` frame
/// (PRD 0007), and the `Kairos` stamp (PRD 0002): the frames evolve
/// independently. Private, exactly as the sibling tags are: the tag is an
/// implementation detail of this codec, and a test that pins it reads the
/// leading byte of an encoded frame.
const METATHESES_WIRE_V1: u8 = 0x01;

/// Frame header: one version byte plus a `u64` big-endian testimony
/// count. The width covers every constructible record, so encoding is
/// total and the canonical bijection unconditional (the rhapsody frame's
/// `u32` count rides its identity plane's live-entry ceiling; this record
/// has no such ceiling, and a saturating count would emit a frame its own
/// decoder refuses).
const METATHESES_WIRE_HEADER_LEN: usize = 9;

/// The width of one dot on the wire: a `u32` `station` and a `u64` `index`.
const METATHESES_WIRE_DOT_LEN: usize = 12;

/// The fixed prefix of one testimony row before its variable-width anchor:
/// the testimony dot, the target dot, then a one-byte anchor tag.
/// `O(1)`-checkable before the anchor branch.
const METATHESES_WIRE_ROW_PREFIX_LEN: usize = 2 * METATHESES_WIRE_DOT_LEN + 1;

/// The smallest a whole testimony row can be: the fixed prefix, an origin
/// anchor (tag only), and the 17-byte rank frame. A dot anchor (after or
/// before) adds [`METATHESES_WIRE_DOT_LEN`]. Used as the per-row lower
/// bound so a frame claiming `u32::MAX` testimonies is refused in `O(1)`
/// against the buffer it cannot fill.
const METATHESES_WIRE_ROW_MIN_LEN: usize = METATHESES_WIRE_ROW_PREFIX_LEN + KAIROS_WIRE_LEN;

/// The anchor tag for a testimony re-placed at the origin
/// ([`Anchor::Origin`]), carrying no dot.
const ANCHOR_TAG_ORIGIN: u8 = 0x00;
/// The anchor tag for the region AFTER a dot ([`Anchor::After`]); a dot
/// follows.
const ANCHOR_TAG_AFTER: u8 = 0x01;
/// The anchor tag for the region BEFORE a dot ([`Anchor::Before`]); a dot
/// follows. The side rides the tag (the PRD 0018 discipline the rhapsody
/// v2 frame fixed), so a `Before` row is the same width as an `After` one.
const ANCHOR_TAG_BEFORE: u8 = 0x02;

impl Metatheses {
    /// Encodes this move record as its canonical version-1 wire frame
    /// (S272): version byte, `u64` testimony count, the rows in strictly
    /// ascending testimony-dot order (testimony dot, target dot, tagged
    /// sided anchor, 17-byte [`Kairos`](crate::kairos::Kairos) rank frame,
    /// so the stamp codec stays the single source of rank bytes).
    /// Big-endian throughout. Infallible and total: the count field's
    /// width covers every constructible record, so no value encodes to a
    /// frame its own decoder refuses. The only allocation is the frame.
    ///
    /// The row is the field order the first serializing consumer already
    /// spells (alma transport's provisional movement section, ruling
    /// R-55), under this crate's byte discipline: minerva frames are
    /// big-endian, so the two codecs agree on fields and diverge on byte
    /// order, and the consumer retires its spelling on adoption. Equal
    /// records encode to identical bytes, so the frame is a sound content
    /// hash (the canonical bijection).
    ///
    /// # Byte identity is a versioned contract, not an evolving format
    ///
    /// Every accepted frame re-encodes to its own bytes; every value has
    /// one encoding. A future need is a sibling codec under a new version
    /// byte, never an in-place evolution (R-8).
    #[must_use]
    pub fn to_bytes(&self) -> Vec<u8> {
        let count = self.len() as u64;
        let mut out = Vec::with_capacity(
            METATHESES_WIRE_HEADER_LEN + self.len() * METATHESES_WIRE_ROW_MIN_LEN,
        );
        out.push(METATHESES_WIRE_V1);
        out.extend_from_slice(&count.to_be_bytes());
        // The record enumerates ascending by testimony dot, the order
        // decode demands.
        for (dot, metathesis) in self {
            out.extend_from_slice(&dot.station().to_be_bytes());
            out.extend_from_slice(&dot.counter().to_be_bytes());
            let RawDot {
                station: target_station,
                counter: target_index,
            } = metathesis.target;
            out.extend_from_slice(&target_station.to_be_bytes());
            out.extend_from_slice(&target_index.to_be_bytes());
            match metathesis.to.anchor {
                Anchor::Origin => out.push(ANCHOR_TAG_ORIGIN),
                Anchor::After(RawDot {
                    station: astation,
                    counter: aindex,
                }) => {
                    out.push(ANCHOR_TAG_AFTER);
                    out.extend_from_slice(&astation.to_be_bytes());
                    out.extend_from_slice(&aindex.to_be_bytes());
                }
                Anchor::Before(RawDot {
                    station: astation,
                    counter: aindex,
                }) => {
                    out.push(ANCHOR_TAG_BEFORE);
                    out.extend_from_slice(&astation.to_be_bytes());
                    out.extend_from_slice(&aindex.to_be_bytes());
                }
            }
            out.extend_from_slice(&metathesis.to.rank.to_bytes());
        }
        out
    }

    /// The exact length in bytes of the frame [`to_bytes`](Self::to_bytes)
    /// emits for this record. The codec computes it allocation-free from its
    /// own row law (S280, ruling R-62; the
    /// [`snapshot_encoded_len`](crate::metis::Rhapsody::snapshot_encoded_len)
    /// shape, ruling R-59). The length is the fixed header, then per testimony
    /// the fixed row prefix, the anchor dot when the anchor carries one, and
    /// the rank frame.
    ///
    /// The preflight door for bounded admission: an embedding protocol
    /// that caps its authenticated payload can refuse an oversized record
    /// *before* serialization, without either allocating the frame to
    /// measure it or re-deriving the row widths downstream (a second byte
    /// oracle that drifts). The wire property suite pins this length equal
    /// to the emitted frame's over generated records.
    #[must_use]
    pub fn encoded_len(&self) -> usize {
        let mut len = METATHESES_WIRE_HEADER_LEN;
        for (_, metathesis) in self {
            len += METATHESES_WIRE_ROW_PREFIX_LEN + KAIROS_WIRE_LEN;
            if !matches!(metathesis.to.anchor, Anchor::Origin) {
                len += METATHESES_WIRE_DOT_LEN;
            }
        }
        len
    }

    /// Decodes exactly one canonical frame produced by
    /// [`to_bytes`](Self::to_bytes), rejecting trailing bytes, under the
    /// input-length ceiling alone: every row is backed by at least its
    /// fixed minimum of frame bytes, so the input already bounds
    /// allocation and no separate policy is required. An embedding
    /// protocol that owns a stricter row policy states it through
    /// [`from_bytes_with_budget`](Self::from_bytes_with_budget).
    ///
    /// # Errors
    ///
    /// The variants of [`from_bytes_with_budget`](Self::from_bytes_with_budget),
    /// except [`MetathesesDecodeError::TooManyTestimonies`]: this door
    /// states no row budget, so a count the bytes cannot back refuses as
    /// [`UnexpectedLength`](MetathesesDecodeError::UnexpectedLength) at
    /// its first unbacked row, in `O(1)`.
    /// Never panics on adversarial input.
    pub fn from_bytes(bytes: &[u8]) -> Result<Self, MetathesesDecodeError> {
        Self::decode(bytes, None)
    }

    /// Decodes exactly one canonical frame under a caller-owned testimony
    /// budget (the embedding door: a protocol that caps rows per event
    /// states the cap in the frame's own unit).
    ///
    /// Decode refuses any frame the encoder could not produce, so accepted
    /// frames re-encode bit for bit (the canonical bijection). Canonical
    /// form: rows strictly ascending by testimony dot, no zero-index
    /// testimony dot (the identity law [`Dot`] carries, ruling R-91), no
    /// anchor tag above `0x02`. A zero-index *target* or *anchor* dot is
    /// deliberately accepted: the record stores both verbatim (they are
    /// payload, not row keys), so they are representable values whose
    /// refusal would break the bijection, exactly as the rhapsody frame
    /// accepts a dangling anchor. Whether a target exists, and which
    /// testimony wins, are reads
    /// ([`Rhapsody::recension`](crate::metis::Rhapsody::recension)), never
    /// wire refusals: the record never chooses.
    ///
    /// Allocation is bounded before any row work. The decoder checks the
    /// declared count against the caller's budget up front. Every row read
    /// then checks the remaining length against the per-row minimum. A frame
    /// claiming `u32::MAX` testimonies therefore refuses in `O(1)`, against
    /// the buffer it cannot fill.
    ///
    /// # Errors
    ///
    /// [`MetathesesDecodeError::UnknownVersion`] for an unrecognized
    /// version byte; [`MetathesesDecodeError::TooManyTestimonies`] for a
    /// declared count past the budget;
    /// [`MetathesesDecodeError::UnexpectedLength`] if the input is shorter
    /// than the declared count requires, or carries trailing bytes;
    /// [`MetathesesDecodeError::ZeroDot`] and
    /// [`MetathesesDecodeError::NonAscendingTestimonies`] for the
    /// canonical-form violations;
    /// [`MetathesesDecodeError::BadAnchorTag`] for an unknown tag; and
    /// [`MetathesesDecodeError::Rank`] for a malformed rank frame. Never
    /// panics on adversarial input.
    pub fn from_bytes_with_budget(
        bytes: &[u8],
        budget: MetathesesDecodeBudget,
    ) -> Result<Self, MetathesesDecodeError> {
        Self::decode(bytes, Some(budget))
    }

    /// The one decode core behind both doors: the caller's budget when one
    /// was stated, otherwise the input length alone bounds the work (each
    /// row read checks the remaining bytes against the per-row minimum, so
    /// a declared count the buffer cannot back refuses at its first
    /// unbacked row, in `O(1)`).
    fn decode(
        bytes: &[u8],
        budget: Option<MetathesesDecodeBudget>,
    ) -> Result<Self, MetathesesDecodeError> {
        let header: [u8; METATHESES_WIRE_HEADER_LEN] = bytes
            .get(..METATHESES_WIRE_HEADER_LEN)
            .and_then(|h| h.try_into().ok())
            .ok_or(MetathesesDecodeError::UnexpectedLength {
                expected: METATHESES_WIRE_HEADER_LEN,
                found: bytes.len(),
            })?;
        let [version, c0, c1, c2, c3, c4, c5, c6, c7] = header;
        if version != METATHESES_WIRE_V1 {
            return Err(MetathesesDecodeError::UnknownVersion(version));
        }
        let count = u64::from_be_bytes([c0, c1, c2, c3, c4, c5, c6, c7]);
        // Refuse a count past the caller's licence before any record
        // work; the per-row length checks below keep allocation bounded
        // by the input regardless.
        if let Some(budget) = budget
            && count > budget.max_testimonies() as u64
        {
            return Err(MetathesesDecodeError::TooManyTestimonies {
                count,
                budget: budget.max_testimonies() as u64,
            });
        }
        // Refuse a count the body cannot back before decoding a single
        // row: every row costs at least the per-row minimum, so this one
        // division is the promised `O(1)` rejection, and a frame
        // declaring one row more than it carries cannot buy the work of
        // decoding everything it does carry first.
        let body = bytes.len() - METATHESES_WIRE_HEADER_LEN;
        if count > (body / METATHESES_WIRE_ROW_MIN_LEN) as u64 {
            let expected = count
                .saturating_mul(METATHESES_WIRE_ROW_MIN_LEN as u64)
                .saturating_add(METATHESES_WIRE_HEADER_LEN as u64);
            return Err(MetathesesDecodeError::UnexpectedLength {
                expected: usize::try_from(expected).unwrap_or(usize::MAX),
                found: bytes.len(),
            });
        }

        // A cursor over the frame body. `rest` always points at the
        // unconsumed suffix, and every read first checks the remaining
        // length, so allocation and iteration are bounded by
        // `bytes.len()`, never by a declared count.
        let mut rest = &bytes[METATHESES_WIRE_HEADER_LEN..];
        let mut decoded = Self::new();
        let mut previous_dot: Option<Dot> = None;

        for _ in 0..count {
            let (dot, metathesis, tail) = decode_row(bytes, rest)?;
            rest = tail;

            if let Some(previous) = previous_dot
                && dot <= previous
            {
                // The error diagnoses refused raw input, so it keeps its
                // raw spelling (ruling R-91).
                return Err(MetathesesDecodeError::NonAscendingTestimonies {
                    previous: previous.into(),
                    found: dot.into(),
                });
            }
            previous_dot = Some(dot);
            // Ascending was just proven and the counter is nonzero by the
            // dot's own type, so the record accepts every row.
            let _ = decoded.insert(dot, metathesis);
        }

        if rest.is_empty() {
            Ok(decoded)
        } else {
            // The consumed prefix is the exact frame the count described.
            Err(MetathesesDecodeError::UnexpectedLength {
                expected: bytes.len() - rest.len(),
                found: bytes.len(),
            })
        }
    }
}

/// One decoded testimony row: its testimony dot, the metathesis, and the
/// unconsumed tail.
type DecodedRow<'a> = (Dot, Metathesis, &'a [u8]);

/// Decodes one testimony row off the front of `rest`, returning the
/// testimony dot, the metathesis, and the unconsumed tail. Length-checks
/// the fixed prefix (against the per-row minimum, so a large count cannot
/// force an unbacked read), then the anchor's variable body, then the rank
/// frame; each is a total, panic-free slice conversion. Validates the
/// testimony dot is non-zero and the anchor tag is known; the
/// ascending-order check is the caller's.
fn decode_row<'a>(bytes: &[u8], rest: &'a [u8]) -> Result<DecodedRow<'a>, MetathesesDecodeError> {
    let prefix: [u8; METATHESES_WIRE_ROW_PREFIX_LEN] = rest
        .get(..METATHESES_WIRE_ROW_PREFIX_LEN)
        .and_then(|p| p.try_into().ok())
        .ok_or(MetathesesDecodeError::UnexpectedLength {
            // The bytes consumed so far plus one whole minimal row is a
            // sound lower bound on the frame the count requires.
            expected: bytes.len() - rest.len() + METATHESES_WIRE_ROW_MIN_LEN,
            found: bytes.len(),
        })?;
    let [
        s0,
        s1,
        s2,
        s3,
        i0,
        i1,
        i2,
        i3,
        i4,
        i5,
        i6,
        i7,
        t0,
        t1,
        t2,
        t3,
        t4,
        t5,
        t6,
        t7,
        t8,
        t9,
        t10,
        t11,
        tag,
    ] = prefix;
    let station = u32::from_be_bytes([s0, s1, s2, s3]);
    let index = u64::from_be_bytes([i0, i1, i2, i3, i4, i5, i6, i7]);
    let Some(index) = NonZeroU64::new(index) else {
        // The non-dot `0`: refused here, before the identity is built, so
        // the acceptance set is unchanged (ruling R-91).
        return Err(MetathesesDecodeError::ZeroDot { station });
    };
    let dot = Dot::new(station, index);
    let target = (
        u32::from_be_bytes([t0, t1, t2, t3]),
        u64::from_be_bytes([t4, t5, t6, t7, t8, t9, t10, t11]),
    );
    let mut rest = &rest[METATHESES_WIRE_ROW_PREFIX_LEN..];

    let anchor = match tag {
        ANCHOR_TAG_ORIGIN => Anchor::Origin,
        ANCHOR_TAG_AFTER | ANCHOR_TAG_BEFORE => {
            let anchor: [u8; METATHESES_WIRE_DOT_LEN] = rest
                .get(..METATHESES_WIRE_DOT_LEN)
                .and_then(|a| a.try_into().ok())
                .ok_or(MetathesesDecodeError::UnexpectedLength {
                    expected: bytes.len() - rest.len() + METATHESES_WIRE_DOT_LEN + KAIROS_WIRE_LEN,
                    found: bytes.len(),
                })?;
            rest = &rest[METATHESES_WIRE_DOT_LEN..];
            let [a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11] = anchor;
            let dot = (
                u32::from_be_bytes([a0, a1, a2, a3]),
                u64::from_be_bytes([a4, a5, a6, a7, a8, a9, a10, a11]),
            );
            // The side rides the tag: an after tag hangs in the region
            // after the dot, a before tag in the region before it (the
            // PRD 0018 discipline).
            let dot = RawDot::from(dot);
            if tag == ANCHOR_TAG_AFTER {
                Anchor::After(dot)
            } else {
                Anchor::Before(dot)
            }
        }
        other => return Err(MetathesesDecodeError::BadAnchorTag { tag: other }),
    };

    let rank_bytes: [u8; KAIROS_WIRE_LEN] = rest
        .get(..KAIROS_WIRE_LEN)
        .and_then(|r| r.try_into().ok())
        .ok_or(MetathesesDecodeError::UnexpectedLength {
            expected: bytes.len() - rest.len() + KAIROS_WIRE_LEN,
            found: bytes.len(),
        })?;
    let rank = Kairos::from_bytes(&rank_bytes).map_err(MetathesesDecodeError::Rank)?;
    rest = &rest[KAIROS_WIRE_LEN..];

    Ok((
        dot,
        Metathesis {
            target: RawDot::from(target),
            to: Locus { anchor, rank },
        },
        rest,
    ))
}