1use crate::hash::{HASH_LEN, Hash};
16use commonware_cryptography::{Blake3, Hasher as CHasher};
17use commonware_storage::merkle::Bagging;
18use commonware_storage::merkle::mmb::{
19 Location as MmbLocation, Proof as MmbProof, StandardHasher, mem::Mmb as MemMmb,
20};
21pub(crate) mod ancestry;
22pub use ancestry::{
23 AncestryDescriptor, AncestrySnapshot, TrustedAncestryDescriptor, verify_ancestry,
24};
25
26const HISTORY_BAGGING: Bagging = Bagging::ForwardFold;
28fn history_hasher() -> StandardHasher<Blake3> {
29 StandardHasher::new(HISTORY_BAGGING)
30}
31
32#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
34pub struct Position(pub u64);
35impl Position {
36 #[must_use]
37 pub const fn as_u64(self) -> u64 {
38 self.0
39 }
40}
41pub type InclusionProof = MmbProof<<Blake3 as CHasher>::Digest>;
43
44#[derive(Debug, thiserror::Error)]
52#[non_exhaustive]
53pub enum HistoryError {
54 #[error("history ref: {0}")]
55 Ref(#[from] crate::refs::RefError),
56 #[error("history object: {0}")]
57 Store(#[from] crate::store::StoreError),
58 #[error("mmb error: {0}")]
59 Mmb(String),
60 #[error("invalid branch name for ancestry: {0:?}")]
61 InvalidBranch(String),
62 #[error("history snapshot is corrupt: {0}")]
63 Corrupted(String),
64 #[error("history directory I/O: {0}")]
65 Io(#[from] std::io::Error),
66}
67
68pub struct CommitHistory {
70 mmb: MemMmb<<Blake3 as CHasher>::Digest>,
71 hasher: StandardHasher<Blake3>,
72}
73impl std::fmt::Debug for CommitHistory {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 f.debug_struct("CommitHistory")
76 .field("leaves", &self.len())
77 .finish_non_exhaustive()
78 }
79}
80impl Default for CommitHistory {
81 fn default() -> Self {
82 Self::open()
83 }
84}
85impl CommitHistory {
86 #[must_use]
87 pub fn open() -> Self {
88 Self {
89 mmb: MemMmb::new(),
90 hasher: history_hasher(),
91 }
92 }
93 pub fn append(&mut self, commit_hash: &Hash) -> Result<Position, HistoryError> {
95 let leaf_loc = self.mmb.leaves();
96 self.extend(std::iter::once(commit_hash))?;
97 Ok(Position(u64::from(leaf_loc)))
98 }
99
100 pub fn extend<'a>(
110 &mut self,
111 commit_hashes: impl IntoIterator<Item = &'a Hash>,
112 ) -> Result<(), HistoryError> {
113 let mut batch = self.mmb.new_batch();
114 for h in commit_hashes {
115 let leaf = digest_from_hash(h);
116 batch = batch.add(&self.hasher, &leaf);
117 }
118 let batch = batch.merkleize(&self.mmb, &self.hasher);
119 self.mmb
120 .apply_batch(&batch)
121 .map_err(|e| HistoryError::Mmb(e.to_string()))?;
122 Ok(())
123 }
124 #[must_use]
129 pub fn root(&self) -> Hash {
130 let digest = self
131 .mmb
132 .root(&self.hasher, 0)
133 .expect("zero inactive peaks is valid");
134 let mut out = [0; HASH_LEN];
135 out.copy_from_slice(digest.as_ref());
136 out
137 }
138 #[must_use]
139 pub fn len(&self) -> u64 {
140 u64::from(self.mmb.leaves())
141 }
142 #[must_use]
143 pub fn is_empty(&self) -> bool {
144 self.len() == 0
145 }
146 pub fn prove(&self, position: Position) -> Result<InclusionProof, HistoryError> {
147 self.mmb
148 .proof(&self.hasher, MmbLocation::new(position.0), 0)
149 .map_err(|e| HistoryError::Mmb(e.to_string()))
150 }
151}
152#[must_use]
154pub fn verify_inclusion(
155 commit_hash: &Hash,
156 position: Position,
157 proof: &InclusionProof,
158 root: &Hash,
159) -> bool {
160 let leaf = digest_from_hash(commit_hash);
161 let root_digest = digest_from_hash(root);
162 let loc = MmbLocation::new(position.0);
163
164 let hasher = history_hasher();
166 proof.verify_element_inclusion(&hasher, leaf.as_ref(), loc, &root_digest)
167}
168
169fn digest_from_hash(h: &Hash) -> <Blake3 as CHasher>::Digest {
170 <<Blake3 as CHasher>::Digest as From<[u8; HASH_LEN]>>::from(*h)
171}
172#[cfg(test)]
173mod tests {
174 use super::*;
175 fn synth(i: u64) -> Hash {
176 crate::hash::hash(&i.to_be_bytes())
177 }
178 #[test]
179 fn mem_empty_history_root_is_well_defined() {
180 let h1 = CommitHistory::open();
181 let h2 = CommitHistory::open();
182 assert_eq!(h1.root(), h2.root(), "empty root must be deterministic");
183 assert!(h1.is_empty());
184 assert_eq!(h1.len(), 0);
185 }
186
187 #[test]
188 fn mem_append_returns_dense_positions() {
189 let mut h = CommitHistory::open();
190 for i in 0..16u64 {
191 let pos = h.append(&synth(i)).unwrap();
192 assert_eq!(pos, Position(i), "positions must be dense and 0-based");
193 }
194 assert_eq!(h.len(), 16);
195 }
196
197 #[test]
203 fn mem_extend_matches_sequential_append_from_empty() {
204 for count in [0u64, 1, 2, 3, 16, 17, 100] {
205 let hashes: Vec<Hash> = (0..count).map(synth).collect();
206
207 let mut sequential = CommitHistory::open();
208 for h in &hashes {
209 sequential.append(h).unwrap();
210 }
211
212 let mut batched = CommitHistory::open();
213 batched.extend(&hashes).unwrap();
214
215 assert_eq!(
216 sequential.root(),
217 batched.root(),
218 "extend({count}) root must match {count} sequential appends"
219 );
220 assert_eq!(sequential.len(), batched.len());
221 assert_eq!(sequential.len(), count);
222 }
223 }
224
225 #[test]
226 fn mem_extend_matches_sequential_append_onto_existing_history() {
227 let prefix: Vec<Hash> = (0..12u64).map(synth).collect();
228 let suffix: Vec<Hash> = (12..12 + 9u64).map(synth).collect();
229
230 let mut sequential = CommitHistory::open();
231 for h in prefix.iter().chain(suffix.iter()) {
232 sequential.append(h).unwrap();
233 }
234
235 let mut batched = CommitHistory::open();
236 for h in &prefix {
237 batched.append(h).unwrap();
238 }
239 batched.extend(&suffix).unwrap();
240
241 assert_eq!(sequential.root(), batched.root());
242 assert_eq!(sequential.len(), batched.len());
243 }
244
245 #[test]
246 fn mem_prove_and_verify_position_712_of_1000() {
247 let mut h = CommitHistory::open();
248 let commits: Vec<Hash> = (0..1000u64).map(synth).collect();
249 for c in &commits {
250 h.append(c).unwrap();
251 }
252 assert_eq!(h.len(), 1000);
253
254 let target = Position(712);
255 let proof = h.prove(target).unwrap();
256 let root = h.root();
257
258 assert!(
259 verify_inclusion(&commits[712], target, &proof, &root),
260 "honest proof must verify"
261 );
262 }
263
264 #[test]
265 fn mem_tampered_proof_fails_verification() {
266 let mut h = CommitHistory::open();
267 for i in 0..256u64 {
268 h.append(&synth(i)).unwrap();
269 }
270 let target = Position(42);
271 let mut proof = h.prove(target).unwrap();
272 let root = h.root();
273 let commit = synth(42);
274
275 assert!(verify_inclusion(&commit, target, &proof, &root));
276
277 assert!(
278 !proof.digests.is_empty(),
279 "non-trivial proof must carry at least one sibling"
280 );
281 let mut bytes: [u8; HASH_LEN] = [0u8; HASH_LEN];
282 bytes.copy_from_slice(proof.digests[0].as_ref());
283 bytes[0] ^= 0x01;
284 proof.digests[0] = <<Blake3 as CHasher>::Digest as From<[u8; HASH_LEN]>>::from(bytes);
285
286 assert!(
287 !verify_inclusion(&commit, target, &proof, &root),
288 "tampered proof must fail"
289 );
290 }
291
292 #[test]
300 fn verify_inclusion_rejects_wrong_position() {
301 let mut h = CommitHistory::open();
302 let commits: Vec<Hash> = (0..64u64).map(synth).collect();
303 for c in &commits {
304 h.append(c).unwrap();
305 }
306 let target = Position(42);
307 let proof = h.prove(target).unwrap();
308 let root = h.root();
309
310 assert!(verify_inclusion(&commits[42], target, &proof, &root));
311 assert!(!verify_inclusion(&commits[42], Position(41), &proof, &root));
315 assert!(!verify_inclusion(&commits[42], Position(0), &proof, &root));
316 }
317
318 #[test]
319 fn verify_inclusion_rejects_mismatched_leaf_count() {
320 let mut h = CommitHistory::open();
321 let commits: Vec<Hash> = (0..64u64).map(synth).collect();
322 for c in &commits {
323 h.append(c).unwrap();
324 }
325 let target = Position(42);
326 let mut proof = h.prove(target).unwrap();
327 let root = h.root();
328 assert!(verify_inclusion(&commits[42], target, &proof, &root));
329
330 proof.leaves = MmbLocation::new(63);
335 assert!(!verify_inclusion(&commits[42], target, &proof, &root));
336 }
337
338 #[test]
339 fn verify_inclusion_rejects_truncated_or_over_long_digests() {
340 let mut h = CommitHistory::open();
341 let commits: Vec<Hash> = (0..64u64).map(synth).collect();
342 for c in &commits {
343 h.append(c).unwrap();
344 }
345 let target = Position(42);
346 let proof = h.prove(target).unwrap();
347 let root = h.root();
348 assert!(verify_inclusion(&commits[42], target, &proof, &root));
349 assert!(
350 !proof.digests.is_empty(),
351 "non-trivial proof must carry at least one digest"
352 );
353
354 let mut truncated = proof.clone();
356 truncated.digests.pop();
357 assert!(!verify_inclusion(&commits[42], target, &truncated, &root));
358
359 let mut over_long = proof;
362 over_long.digests.push(over_long.digests[0]);
363 assert!(!verify_inclusion(&commits[42], target, &over_long, &root));
364 }
365}