Skip to main content

kernel/
ledger.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! The Ledger port: the only write entrance to history (seam list,
7//! ARCHITECTURE section 3), plus the chain rule both adapters and replay
8//! share.
9//! The part `crates/kernel/spec/Ledger.lean` specifies this module.
10//!
11//! Contract owned here:
12//! - implementations own seq/prev assignment and byte production; callers
13//!   never serialize. `Ok(ref)` means the record is durable in that
14//!   adapter's medium; `Err` means nothing observable was appended (torn
15//!   bytes are the reopen path's business).
16//! - the chain hashes the canonical line bytes exactly, excluding the
17//!   line terminator; the genesis line's prev is 32 zero bytes.
18//! - the port is write-only: production reads go through projections.
19//!   The read-back needed for verification lives behind the
20//!   `conformance` feature and never ships in the production surface.
21
22use crate::error::AxError;
23use crate::event::{EventDraft, EventRef};
24use crate::locator::B3Hash;
25
26/// prev of the genesis line: 64 zero hex digits.
27pub const GENESIS_PREV: B3Hash = B3Hash::from_bytes([0; 32]);
28
29/// Chain rule: prev(line k+1) = blake3(canonical bytes of line k).
30/// One hash function, one home; jsonl, the in-memory ledger and replay
31/// all call this exact function.
32pub fn chain_hash(raw_line: &[u8]) -> B3Hash {
33    B3Hash::from_bytes(*blake3::hash(raw_line).as_bytes())
34}
35
36/// The only write entrance to history.
37pub trait Ledger {
38    fn append(&mut self, draft: EventDraft) -> Result<EventRef, AxError>;
39
40    /// Every draft in one wave, answered positionally.
41    ///
42    /// **A durable write costs one disk barrier, and a barrier costs the
43    /// same whether one record or fifty ride on it.** Measured on one
44    /// windows-x86_64 NVMe machine: 585 µs per record at one record per
45    /// barrier against 13 µs at fifty, of which the write itself is
46    /// about 2.5 µs. A caller that already holds a wave therefore pays
47    /// forty times what the disk asks by handing it over one at a time.
48    ///
49    /// The contract of [`Ledger::append`] holds for every element: when
50    /// this answers `Ok`, every record in it is durable, and the refs
51    /// come back in the order the drafts were given. The default
52    /// implementation appends one at a time, which is correct and is
53    /// what a store with no batch of its own can honestly offer.
54    ///
55    /// # Errors
56    /// The first refusal ends the wave. Records before it may already be
57    /// durable, which is the same promise a single append makes when the
58    /// one after it fails.
59    fn append_all(&mut self, drafts: Vec<EventDraft>) -> Result<Vec<EventRef>, AxError> {
60        let mut refs = Vec::with_capacity(drafts.len());
61        for draft in drafts {
62            refs.push(self.append(draft)?);
63        }
64        Ok(refs)
65    }
66}
67
68#[cfg(feature = "conformance")]
69pub mod conformance {
70    //! V3: one assertion suite for every implementation. A stand-in that
71    //! cannot pass this suite is not an implementation of the port.
72
73    use super::{GENESIS_PREV, Ledger, chain_hash};
74    use crate::address::Address;
75    use crate::error::AxError;
76    use crate::event::{EventDraft, EventKind, EventRecord, Payload, RunId, Seq, TimeMs};
77
78    /// Verification-only read-back. Production callers never read through
79    /// the Ledger handle; this trait exists so the suite can compare raw
80    /// bytes, and it compiles only under the `conformance` feature.
81    pub trait LedgerInspect {
82        fn raw_lines(&self) -> Result<Vec<Vec<u8>>, AxError>;
83    }
84
85    /// Fixed draft script: deterministic on purpose so assertion six
86    /// (byte-identical logs from two fresh instances) is meaningful.
87    fn script() -> Vec<EventDraft> {
88        let run = RunId::from_bytes([1; 16]);
89        let parsed = Address::parse("lab");
90        assert!(parsed.is_ok(), "fixture address must parse: {parsed:?}");
91        let addr = parsed.ok();
92        vec![
93            EventDraft {
94                run: RunId::CITY,
95                t: TimeMs::new(1),
96                who: "city".to_owned(),
97                addr: None,
98                kind: EventKind::CityInitialized,
99                data: Payload::empty(),
100                ig: false,
101            },
102            EventDraft {
103                run: RunId::CITY,
104                t: TimeMs::new(2),
105                who: "city".to_owned(),
106                addr,
107                kind: EventKind::BuildingCreated,
108                data: Payload::empty(),
109                ig: false,
110            },
111            EventDraft {
112                run,
113                t: TimeMs::new(3),
114                who: "planner@lab.1".to_owned(),
115                addr: None,
116                kind: EventKind::RunStarted,
117                data: Payload::empty(),
118                ig: false,
119            },
120            EventDraft {
121                run,
122                t: TimeMs::new(4),
123                who: "planner@lab.1".to_owned(),
124                addr: None,
125                kind: EventKind::ToolCalled,
126                data: Payload::empty(),
127                ig: false,
128            },
129            EventDraft {
130                run,
131                t: TimeMs::new(5),
132                who: "planner@lab.1".to_owned(),
133                addr: None,
134                kind: EventKind::RunFrozen,
135                data: Payload::empty(),
136                ig: true,
137            },
138        ]
139    }
140
141    fn collect_lines<L: Ledger + LedgerInspect>(ledger: &mut L) -> Vec<Vec<u8>> {
142        for (index, draft) in script().into_iter().enumerate() {
143            let kind = draft.kind;
144            let appended = ledger.append(draft);
145            assert!(
146                appended.is_ok(),
147                "append {index} ({kind:?}) must succeed: {appended:?}"
148            );
149            let Ok(echo) = appended else {
150                return Vec::new();
151            };
152            // Assertion five (half): the echo points at what was appended.
153            assert_eq!(echo.kind(), kind, "ref kind must echo the draft");
154            assert_eq!(
155                echo.seq(),
156                Seq::new(u64::try_from(index).unwrap_or(u64::MAX))
157            );
158        }
159        let lines = ledger.raw_lines();
160        assert!(lines.is_ok(), "raw_lines must succeed: {lines:?}");
161        let Ok(lines) = lines else { return Vec::new() };
162        lines
163    }
164
165    /// The suite (`crates/kernel/spec/Ledger.lean` §8-9, six assertions). `fresh` must yield an
166    /// empty ledger each call.
167    pub fn assert_ledger_conformance<L, F>(mut fresh: F)
168    where
169        L: Ledger + LedgerInspect,
170        F: FnMut() -> L,
171    {
172        let mut first = fresh();
173        let lines = collect_lines(&mut first);
174        assert_eq!(lines.len(), script().len(), "one line per draft");
175
176        let mut prev = GENESIS_PREV;
177        for (index, line) in lines.iter().enumerate() {
178            let parsed = EventRecord::parse_line(line);
179            assert!(parsed.is_ok(), "line {index} must parse: {parsed:?}");
180            let Ok(record) = parsed else { return };
181            // Assertion one and three: genesis prev, then chain continuity.
182            assert_eq!(
183                record.prev(),
184                prev,
185                "line {index}: prev must hash the previous line"
186            );
187            // Assertion two: contiguous seq from FIRST.
188            assert_eq!(
189                record.seq(),
190                Seq::new(u64::try_from(index).unwrap_or(u64::MAX)),
191                "line {index}: seq must be contiguous"
192            );
193            // Assertion five: v is pinned.
194            assert_eq!(
195                record.v(),
196                crate::consts_external::EVENT_LOG_V,
197                "line {index}: v must be EVENT_LOG_V"
198            );
199            // Assertion four: the writer is canonical.
200            let echo = record.canonical_line();
201            assert!(echo.is_ok(), "line {index} must reserialize");
202            let Ok(echo) = echo else { return };
203            assert_eq!(
204                &echo, line,
205                "line {index}: adapter bytes must be the canonical bytes"
206            );
207            prev = chain_hash(line);
208        }
209
210        // Assertion six: determinism across fresh instances.
211        let mut second = fresh();
212        let again = collect_lines(&mut second);
213        assert_eq!(
214            lines, again,
215            "two fresh instances fed the same drafts must produce identical bytes"
216        );
217    }
218}
219
220#[cfg(test)]
221#[allow(
222    clippy::unwrap_used,
223    clippy::expect_used,
224    clippy::panic,
225    clippy::indexing_slicing,
226    reason = "test code"
227)]
228mod tests {
229    use super::*;
230
231    #[test]
232    fn genesis_prev_is_sixty_four_zeros() {
233        assert_eq!(GENESIS_PREV.to_string(), "0".repeat(64));
234    }
235
236    #[test]
237    fn chain_hash_is_plain_blake3_of_the_line_bytes() {
238        let line = b"{\"v\":1}";
239        assert_eq!(
240            chain_hash(line).to_string(),
241            blake3::hash(line).to_hex().to_string()
242        );
243    }
244}
245
246#[cfg(all(test, feature = "conformance"))]
247#[allow(
248    clippy::unwrap_used,
249    clippy::expect_used,
250    clippy::panic,
251    clippy::indexing_slicing,
252    reason = "test code"
253)]
254mod conformance_self_test {
255    use super::conformance::{LedgerInspect, assert_ledger_conformance};
256    use super::*;
257    use crate::error::{AxCode, AxError};
258    use crate::event::{EventDraft, EventRecord, EventRef, Seq};
259
260    /// Minimal reference implementation: proves the suite runs against any
261    /// implementation before citysim provides the second one.
262    struct VecLedger {
263        lines: Vec<Vec<u8>>,
264        next_seq: Seq,
265        prev: B3Hash,
266    }
267
268    impl VecLedger {
269        fn new() -> Self {
270            VecLedger {
271                lines: Vec::new(),
272                next_seq: Seq::FIRST,
273                prev: GENESIS_PREV,
274            }
275        }
276    }
277
278    impl Ledger for VecLedger {
279        fn append(&mut self, draft: EventDraft) -> Result<EventRef, AxError> {
280            let record = EventRecord::from_draft(draft, self.next_seq, self.prev);
281            let line = record.canonical_line()?;
282            self.prev = chain_hash(&line);
283            self.next_seq = self.next_seq.next()?;
284            self.lines.push(line);
285            Ok(record.to_ref())
286        }
287    }
288
289    impl LedgerInspect for VecLedger {
290        fn raw_lines(&self) -> Result<Vec<Vec<u8>>, AxError> {
291            Ok(self.lines.clone())
292        }
293    }
294
295    #[test]
296    fn the_reference_implementation_passes_the_suite() {
297        assert_ledger_conformance(VecLedger::new);
298    }
299
300    #[test]
301    fn a_chain_breaking_implementation_fails_the_suite() {
302        struct BrokenPrev(VecLedger);
303        impl Ledger for BrokenPrev {
304            fn append(&mut self, draft: EventDraft) -> Result<EventRef, AxError> {
305                let echo = self.0.append(draft)?;
306                self.0.prev = GENESIS_PREV; // never advances: chain breaks at line 2
307                Ok(echo)
308            }
309        }
310        impl LedgerInspect for BrokenPrev {
311            fn raw_lines(&self) -> Result<Vec<Vec<u8>>, AxError> {
312                self.0.raw_lines()
313            }
314        }
315        let outcome = std::panic::catch_unwind(|| {
316            assert_ledger_conformance(|| BrokenPrev(VecLedger::new()));
317        });
318        assert!(outcome.is_err(), "the suite must bite a broken chain");
319    }
320
321    #[test]
322    fn suite_error_paths_use_invalid_args() {
323        // Anchors the AxCode used when a draft cannot even serialize.
324        let err = AxError::failure(AxCode::InvalidArgs, "x", "y").with_recovery("z");
325        assert_eq!(err.code(), &AxCode::InvalidArgs);
326    }
327}