CRUMB a card from devarno-cloud

AuditEntry & AuditLog Data Model

vest beginner 5 min read

ELI5

A library’s issue ledger: every book checkout (operation) is stamped with a running row number, stored in a numbered shelf (Vec), and cross-referenced in a card catalogue (HashMap). You can find a book by title in O(1) via the catalogue, or scan the shelf in sequence order.

Technical Deep Dive

Struct Layouts

AuditEntry (src/audit_trail.rs) holds five fields:

FieldTypeSet by
op_idStringcaller via AuditEntry::new
dataVec<u8>caller via AuditEntry::new
signatureAuditSignaturecaller via AuditEntry::new
timestamp_msu64always 0 at construction (not set by new)
sequence_numberu64set by AuditLog::append to entries.len() before push

AuditLog holds a Vec<AuditEntry> (entries) and a HashMap<String, usize> (entry_map).

Class Diagram

classDiagram
class AuditLog {
+entries: Vec~AuditEntry~
+entry_map: HashMap~String, usize~
+append(entry) Result
+get(op_id) Option~AuditEntry~
+get_by_sequence(seq) Option~AuditEntry~
+range(start, end) Vec~AuditEntry~
+hash() Vec~u8~
+len() usize
+size_bytes() usize
}
class AuditEntry {
+op_id: String
+data: Vec~u8~
+signature: AuditSignature
+timestamp_ms: u64
+sequence_number: u64
+hash() Vec~u8~
+size_bytes() usize
}
AuditLog "1" *-- "0..*" AuditEntry

Append Mechanics

// src/audit_trail.rs:76-87
pub fn append(&mut self, mut entry: AuditEntry) -> Result<(), String> {
entry.sequence_number = self.entries.len() as u64;
self.entry_map.insert(entry.op_id.clone(), self.entries.len());
self.entries.push(entry);
Ok(())
}

Inserting a duplicate op_id overwrites the HashMap entry (pointing to the new index) but does not remove the previous AuditEntry from the Vec. The old entry remains at its original index; get_by_sequence still returns it.

Hash Computation

AuditEntry::hash XOR-folds three byte sources into a 32-byte accumulator:

  1. op_id.as_bytes()
  2. data
  3. signature.proof.signature_bytes

AuditLog::hash XOR-folds all entry hashes into a single 32-byte value — a deterministic fingerprint of the entire log.

Range Queries

AuditLog::range(start, end) uses skip(start).take(end - start) on the Vec iterator. It does not validate that end > start or that either bound is within entries.len() — callers must guard bounds.

Key Terms

  • sequence_number → zero-based index into entries; assigned at append time, not by caller
  • entry_mapHashMap<String, usize> enabling O(1) lookup; maps op_id → Vec index
  • AuditEntry::hash → 32-byte XOR fold over op_id bytes, data bytes, and signature bytes
  • AuditLog::hash → aggregate XOR of all entry hashes; changes whenever any entry is modified

Q&A

Q: If op_id is 64 bytes, does size_bytes include those bytes? A: Yes — size_bytes sums op_id.len() + data.len() + signature.proof.signature_bytes.len(). The signature and other fields are not counted.

Q: What does AuditLog::range(5, 5) return? A: An empty Vectake(0) produces no elements.

Q: Is the timestamp_ms field ever set on an entry created through VestProtocol::audit_operation? A: No. AuditEntry::new always initialises timestamp_ms to 0. The field is present in the struct but the constructor does not accept a timestamp parameter.

Examples

Building a log with two entries and retrieving via both access paths:

let mut log = AuditLog::new();
let sig = AuditSignature::default();
log.append(AuditEntry::new("op1".into(), vec![1,2,3], sig.clone())).unwrap();
log.append(AuditEntry::new("op2".into(), vec![4,5,6], sig)).unwrap();
// O(1) by op_id
let e = log.get("op2").unwrap();
assert_eq!(e.sequence_number, 1);
// By sequence
let e2 = log.get_by_sequence(0).unwrap();
assert_eq!(e2.op_id, "op1");
// Range [0, 2)
let range = log.range(0, 2);
assert_eq!(range.len(), 2);

neighbors on the map