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:
| Field | Type | Set by |
|---|---|---|
op_id | String | caller via AuditEntry::new |
data | Vec<u8> | caller via AuditEntry::new |
signature | AuditSignature | caller via AuditEntry::new |
timestamp_ms | u64 | always 0 at construction (not set by new) |
sequence_number | u64 | set 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..*" AuditEntryAppend Mechanics
// src/audit_trail.rs:76-87pub 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:
op_id.as_bytes()datasignature.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_map →
HashMap<String, usize>enabling O(1) lookup; mapsop_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 Vec — take(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_idlet e = log.get("op2").unwrap();assert_eq!(e.sequence_number, 1);
// By sequencelet 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
- SignatureProof & AuditSignature Chain debugging a VestError::InvalidSignature returned from ProofVerifier::verify_signature
- Timeline Reconstruction implementing a new timeline UI control
- FNP CRDT Conflict-Free Merge Semantics understanding CRDT properties and guarantees