1use crate::error::AxError;
23use crate::event::{EventDraft, EventRef};
24use crate::locator::B3Hash;
25
26pub const GENESIS_PREV: B3Hash = B3Hash::from_bytes([0; 32]);
28
29pub fn chain_hash(raw_line: &[u8]) -> B3Hash {
33 B3Hash::from_bytes(*blake3::hash(raw_line).as_bytes())
34}
35
36pub trait Ledger {
38 fn append(&mut self, draft: EventDraft) -> Result<EventRef, AxError>;
39
40 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 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 pub trait LedgerInspect {
82 fn raw_lines(&self) -> Result<Vec<Vec<u8>>, AxError>;
83 }
84
85 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 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 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 assert_eq!(
183 record.prev(),
184 prev,
185 "line {index}: prev must hash the previous line"
186 );
187 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 assert_eq!(
195 record.v(),
196 crate::consts_external::EVENT_LOG_V,
197 "line {index}: v must be EVENT_LOG_V"
198 );
199 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 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 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; 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 let err = AxError::failure(AxCode::InvalidArgs, "x", "y").with_recovery("z");
325 assert_eq!(err.code(), &AxCode::InvalidArgs);
326 }
327}