pub struct Chain<H, S, C>{ /* private fields */ }Expand description
An append-only chain of audit records.
The chain is generic over its three pluggable components:
H: Hasher— the cryptographic hash function used to link records.S: Sink— the backend that persists each record.C: Clock— the time source.
Chain is !Sync by virtue of holding mutable state on &mut self.
Concurrent appenders should serialize on the chain or shard their writes
across independent chains.
§Example
use audit_trail::{
Action, Actor, Chain, Clock, Digest, Hasher, Outcome, Record, RecordId,
Sink, SinkError, Target, Timestamp, HASH_LEN,
};
// Minimal (insecure) Hasher: XOR-fold bytes into a 32-byte buffer.
struct XorHasher([u8; HASH_LEN]);
impl Hasher for XorHasher {
fn reset(&mut self) { self.0 = [0u8; HASH_LEN]; }
fn update(&mut self, bytes: &[u8]) {
for (i, b) in bytes.iter().enumerate() { self.0[i % HASH_LEN] ^= *b; }
}
fn finalize(&mut self, out: &mut Digest) { *out = Digest::from_bytes(self.0); }
}
// A clock that ticks one nanosecond per call.
struct TickClock(core::cell::Cell<u64>);
impl Clock for TickClock {
fn now(&self) -> Timestamp {
let v = self.0.get(); self.0.set(v + 1); Timestamp::from_nanos(v)
}
}
// An in-memory sink that records hashes for verification.
#[derive(Default)]
struct VecSink(Vec<Digest>);
impl Sink for VecSink {
fn write(&mut self, r: &Record<'_>) -> Result<(), SinkError> {
self.0.push(r.hash()); Ok(())
}
}
let mut chain = Chain::new(
XorHasher([0u8; HASH_LEN]),
VecSink::default(),
TickClock(core::cell::Cell::new(1)),
);
let id = chain.append(
Actor::new("user-1"),
Action::new("user.login"),
Target::new("session:abc"),
Outcome::Success,
).expect("append");
assert_eq!(id, RecordId::GENESIS);Implementations§
Source§impl<H, S, C> Chain<H, S, C>
impl<H, S, C> Chain<H, S, C>
Sourcepub fn new(hasher: H, sink: S, clock: C) -> Self
pub fn new(hasher: H, sink: S, clock: C) -> Self
Construct a fresh chain starting from genesis.
The chain begins with next_id = 0 and last_hash = Digest::ZERO.
To resume a previously persisted chain, use Chain::resume.
Sourcepub fn resume(
hasher: H,
sink: S,
clock: C,
next_id: RecordId,
last_hash: Digest,
last_timestamp: Timestamp,
) -> Self
pub fn resume( hasher: H, sink: S, clock: C, next_id: RecordId, last_hash: Digest, last_timestamp: Timestamp, ) -> Self
Resume a chain from a previously persisted tail.
next_id is the identifier the next appended record will receive.
last_hash is the hash of the most recently persisted record (or
Digest::ZERO if the chain is empty).
last_timestamp is the timestamp of the most recently persisted
record (or Timestamp::EPOCH if the chain is empty).
Sourcepub const fn last_hash(&self) -> Digest
pub const fn last_hash(&self) -> Digest
Hash of the most recently appended record (or Digest::ZERO if
none).
Sourcepub const fn last_timestamp(&self) -> Timestamp
pub const fn last_timestamp(&self) -> Timestamp
Timestamp of the most recently appended record (or
Timestamp::EPOCH if none).
Sourcepub fn append(
&mut self,
actor: Actor<'_>,
action: Action<'_>,
target: Target<'_>,
outcome: Outcome,
) -> Result<RecordId>
pub fn append( &mut self, actor: Actor<'_>, action: Action<'_>, target: Target<'_>, outcome: Outcome, ) -> Result<RecordId>
Append a record to the chain.
Returns the new record’s RecordId. On error the chain state is
left unchanged: the sink is only updated after the hash is computed
and the timestamp/id checks have passed, and last_hash /
last_timestamp / next_id are only updated after the sink write
succeeds.
§Errors
Error::NonMonotonicClock— the clock returned a timestamp not greater than the previously stored timestamp.Error::Capacity— the record id counter would overflow.Error::Sink— the sink rejected the write.
Sourcepub fn into_parts(self) -> (H, S, C)
pub fn into_parts(self) -> (H, S, C)
Consume the chain and return its three pluggable components.