Skip to main content

aex_audit/
memory_log.rs

1//! In-memory [`AuditLog`] used by tests and the M1 demo.
2
3use std::sync::Arc;
4
5use async_trait::async_trait;
6use time::OffsetDateTime;
7use tokio::sync::Mutex;
8
9use crate::{
10    event::{Event, EventReceipt, StoredEvent},
11    AuditError, AuditLog, AuditResult, GENESIS_HEAD,
12};
13
14#[derive(Default)]
15pub struct MemoryAuditLog {
16    inner: Arc<Mutex<Inner>>,
17}
18
19#[derive(Default)]
20struct Inner {
21    events: Vec<StoredEvent>,
22}
23
24impl MemoryAuditLog {
25    pub fn new() -> Self {
26        Self::default()
27    }
28
29    pub async fn snapshot(&self) -> Vec<StoredEvent> {
30        self.inner.lock().await.events.clone()
31    }
32}
33
34#[async_trait]
35impl AuditLog for MemoryAuditLog {
36    async fn append(&self, event: Event) -> AuditResult<EventReceipt> {
37        let mut guard = self.inner.lock().await;
38        let position = guard.events.len() as u64;
39        let prev_hash = guard
40            .events
41            .last()
42            .map(|e| e.this_hash.clone())
43            .unwrap_or_else(|| GENESIS_HEAD.to_string());
44
45        let timestamp = OffsetDateTime::now_utc();
46        let this_hash = event.compute_hash(timestamp, &prev_hash)?;
47        let stored = StoredEvent {
48            position,
49            event_id: format!("evt_{}", uuid::Uuid::new_v4().simple()),
50            timestamp,
51            prev_hash,
52            this_hash,
53            event,
54        };
55
56        let receipt = EventReceipt::from(&stored);
57        guard.events.push(stored);
58        Ok(receipt)
59    }
60
61    async fn current_head(&self) -> AuditResult<String> {
62        let guard = self.inner.lock().await;
63        Ok(guard
64            .events
65            .last()
66            .map(|e| e.this_hash.clone())
67            .unwrap_or_else(|| GENESIS_HEAD.to_string()))
68    }
69
70    async fn verify_chain(&self) -> AuditResult<()> {
71        let guard = self.inner.lock().await;
72        let mut expected_prev = GENESIS_HEAD.to_string();
73        for (i, ev) in guard.events.iter().enumerate() {
74            if ev.prev_hash != expected_prev {
75                return Err(AuditError::ChainBroken {
76                    position: i as u64,
77                    expected: expected_prev,
78                    found: ev.prev_hash.clone(),
79                });
80            }
81            ev.verify_hash()?;
82            expected_prev = ev.this_hash.clone();
83        }
84        Ok(())
85    }
86
87    async fn len(&self) -> AuditResult<u64> {
88        Ok(self.inner.lock().await.events.len() as u64)
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use crate::event::EventKind;
96
97    #[tokio::test]
98    async fn empty_log_head_is_genesis() {
99        let log = MemoryAuditLog::new();
100        assert_eq!(log.current_head().await.unwrap(), GENESIS_HEAD);
101        assert_eq!(log.len().await.unwrap(), 0);
102    }
103
104    #[tokio::test]
105    async fn append_advances_head() {
106        let log = MemoryAuditLog::new();
107        let r = log
108            .append(Event::new(
109                EventKind::AgentRegistered,
110                "spize:acme/alice:a4f8b2",
111                "spize:acme/alice:a4f8b2",
112                serde_json::json!({"fingerprint": "a4f8b2"}),
113            ))
114            .await
115            .unwrap();
116        assert_eq!(r.position, 0);
117        assert_ne!(r.chain_head, GENESIS_HEAD);
118        assert_eq!(log.current_head().await.unwrap(), r.chain_head);
119    }
120
121    #[tokio::test]
122    async fn chain_verifies_after_many_appends() {
123        let log = MemoryAuditLog::new();
124        for i in 0..20 {
125            log.append(Event::new(
126                EventKind::TransferInitiated,
127                "spize:acme/alice:a4f8b2",
128                format!("tx_{}", i),
129                serde_json::json!({"seq": i}),
130            ))
131            .await
132            .unwrap();
133        }
134        log.verify_chain().await.unwrap();
135        assert_eq!(log.len().await.unwrap(), 20);
136    }
137
138    #[tokio::test]
139    async fn tampering_breaks_chain() {
140        let log = MemoryAuditLog::new();
141        for i in 0..3 {
142            log.append(Event::new(
143                EventKind::TransferInitiated,
144                "",
145                format!("tx_{}", i),
146                serde_json::json!({"seq": i}),
147            ))
148            .await
149            .unwrap();
150        }
151        // Mutate event in place — simulates a disk tamper.
152        {
153            let mut g = log.inner.lock().await;
154            g.events[1].event.subject = "tx_evil".into();
155        }
156        let err = log.verify_chain().await.unwrap_err();
157        assert!(matches!(err, AuditError::HashMismatch { position: 1, .. }));
158    }
159
160    #[tokio::test]
161    async fn broken_link_detected() {
162        let log = MemoryAuditLog::new();
163        for i in 0..3 {
164            log.append(Event::new(
165                EventKind::TransferInitiated,
166                "",
167                format!("tx_{}", i),
168                serde_json::json!({"seq": i}),
169            ))
170            .await
171            .unwrap();
172        }
173        {
174            let mut g = log.inner.lock().await;
175            g.events[2].prev_hash = "f".repeat(64);
176        }
177        let err = log.verify_chain().await.unwrap_err();
178        assert!(matches!(err, AuditError::ChainBroken { position: 2, .. }));
179    }
180}