1use std::sync::Mutex;
4
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7
8use crate::envelope::DealEnvelope;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
13pub struct LedgerOffset(pub u64);
14
15#[derive(Debug, Error, PartialEq)]
16pub enum LedgerError {
17 #[error("envelope's prev_hash {observed:?} does not match ledger head {expected:?}")]
18 BrokenChain {
19 expected: [u8; 32],
20 observed: [u8; 32],
21 },
22 #[error("ledger lock poisoned — internal invariant violated")]
23 LockPoisoned,
24}
25
26pub trait DealLedger: Send + Sync {
27 fn append(&self, e: DealEnvelope) -> Result<LedgerOffset, LedgerError>;
28 fn replay(&self, range: std::ops::Range<u64>) -> Vec<DealEnvelope>;
29 fn head_hash(&self) -> [u8; 32];
30 fn len(&self) -> u64;
31 fn is_empty(&self) -> bool {
32 self.len() == 0
33 }
34}
35
36#[derive(Default)]
40pub struct InMemoryDealLedger {
41 rows: Mutex<Vec<DealEnvelope>>,
42 head: Mutex<[u8; 32]>,
43}
44
45impl InMemoryDealLedger {
46 pub fn new() -> Self {
47 Self::default()
48 }
49}
50
51impl DealLedger for InMemoryDealLedger {
52 fn append(&self, e: DealEnvelope) -> Result<LedgerOffset, LedgerError> {
53 let mut head = self.head.lock().map_err(|_| LedgerError::LockPoisoned)?;
54 if e.prev_hash != *head {
55 return Err(LedgerError::BrokenChain {
56 expected: *head,
57 observed: e.prev_hash,
58 });
59 }
60 let next_head = e.next_prev_hash();
61 let mut rows = self.rows.lock().map_err(|_| LedgerError::LockPoisoned)?;
62 let offset = LedgerOffset(rows.len() as u64);
63 rows.push(e);
64 *head = next_head;
65 Ok(offset)
66 }
67
68 fn replay(&self, range: std::ops::Range<u64>) -> Vec<DealEnvelope> {
69 let rows = match self.rows.lock() {
70 Ok(g) => g,
71 Err(_) => return Vec::new(),
72 };
73 let lo = range.start as usize;
74 let hi = (range.end as usize).min(rows.len());
75 if lo >= hi {
76 return Vec::new();
77 }
78 rows[lo..hi].to_vec()
79 }
80
81 fn head_hash(&self) -> [u8; 32] {
82 self.head.lock().map(|g| *g).unwrap_or([0u8; 32])
83 }
84
85 fn len(&self) -> u64 {
86 self.rows.lock().map(|g| g.len() as u64).unwrap_or(0)
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use std::time::{Duration, SystemTime};
93
94 use super::*;
95
96 fn append_one(
97 ledger: &InMemoryDealLedger,
98 key: &[u8; 32],
99 prev: [u8; 32],
100 i: u64,
101 ) -> DealEnvelope {
102 let e = DealEnvelope::sign(
103 format!("buyer-{i}"),
104 format!("seller-{i}"),
105 serde_json::json!({"i": i}),
106 prev,
107 SystemTime::UNIX_EPOCH + Duration::from_secs(i),
108 key,
109 )
110 .unwrap();
111 ledger.append(e.clone()).unwrap();
112 e
113 }
114
115 #[test]
116 fn append_advances_offset_and_head() {
117 let ledger = InMemoryDealLedger::new();
118 let key = [7u8; 32];
119 let mut prev = [0u8; 32];
120 for i in 0..3 {
121 let e = append_one(&ledger, &key, prev, i);
122 prev = e.next_prev_hash();
123 }
124 assert_eq!(ledger.len(), 3);
125 assert_eq!(ledger.head_hash(), prev);
126 }
127
128 #[test]
129 fn broken_chain_is_rejected() {
130 let ledger = InMemoryDealLedger::new();
131 let key = [7u8; 32];
132 let _e0 = append_one(&ledger, &key, [0u8; 32], 0);
133 let bad = DealEnvelope::sign(
136 "x",
137 "y",
138 serde_json::json!({}),
139 [0u8; 32], SystemTime::UNIX_EPOCH,
141 &key,
142 )
143 .unwrap();
144 let err = ledger.append(bad).unwrap_err();
145 assert!(matches!(err, LedgerError::BrokenChain { .. }));
146 }
147
148 #[test]
149 fn replay_range_returns_subset() {
150 let ledger = InMemoryDealLedger::new();
151 let key = [7u8; 32];
152 let mut prev = [0u8; 32];
153 for i in 0..5 {
154 let e = append_one(&ledger, &key, prev, i);
155 prev = e.next_prev_hash();
156 }
157 let mid = ledger.replay(1..4);
158 assert_eq!(mid.len(), 3);
159 assert_eq!(mid[0].buyer, "buyer-1");
160 assert_eq!(mid[2].buyer, "buyer-3");
161 }
162}