pub struct Ledger { /* private fields */ }Expand description
Append-only writer. Opening an existing ledger verifies the whole chain (and, with this key, every signature) before accepting new entries — fail-closed: a corrupt audit trail refuses further writes.
Implementations§
Source§impl Ledger
impl Ledger
Sourcepub fn open(path: &Path, key: SigningKey) -> Result<Self, LedgerError>
pub fn open(path: &Path, key: SigningKey) -> Result<Self, LedgerError>
Open a single-key ledger (the common case). Equivalent to
open_with_verifiers with no retired keys.
Sourcepub fn open_with_verifiers(
path: &Path,
key: SigningKey,
retired: Vec<VerifyingKey>,
) -> Result<Self, LedgerError>
pub fn open_with_verifiers( path: &Path, key: SigningKey, retired: Vec<VerifyingKey>, ) -> Result<Self, LedgerError>
Open a ledger that may have been KEY-ROTATED: key is the current signing
key, retired the public keys of every previously-active signing key. The
existing chain is verified against the whole keyring (each record by the key
its kid names; legacy kid-less records against any trusted key), so a
rotated log reopens cleanly. Fail-closed: a record signed by a key NOT in the
ring is tamper.
Sourcepub fn open_anchored(
path: &Path,
key: SigningKey,
retired: Vec<VerifyingKey>,
anchor_path: &Path,
) -> Result<Self, LedgerError>
pub fn open_anchored( path: &Path, key: SigningKey, retired: Vec<VerifyingKey>, anchor_path: &Path, ) -> Result<Self, LedgerError>
Open a ledger AND fail-closed check it against its persisted anchor in one
step — the constructor a server’s startup uses. Equivalent to
open_with_verifiers followed by
verify_against_anchor: the log must be
internally consistent under the keyring AND still extend its last committed
height, so a truncation across a restart refuses the open rather than silently
serving a shortened audit trail.
Sourcepub fn open_segmented(
dir: &Path,
key: SigningKey,
retired: Vec<VerifyingKey>,
policy: RolloverPolicy,
) -> Result<Self, LedgerError>
pub fn open_segmented( dir: &Path, key: SigningKey, retired: Vec<VerifyingKey>, policy: RolloverPolicy, ) -> Result<Self, LedgerError>
Open (or create) a SEGMENTED ledger at dir — the opt-in alternative
to the single ever-growing file, for a long-lived sovereign deployment.
dir becomes a directory of numbered segment files plus a
manifest.json; policy controls when append rolls the active
segment over to a new one. An existing single-file ledger is never
silently upgraded — this only ever creates or reopens a directory, and
open_with_verifiers refuses to open a
directory in the other direction, so the two modes can’t be confused
for each other by accident.
Reopen is fail-closed exactly like the single-file constructors: the
whole chain (every segment, in order) is re-verified against the
keyring before any further append is accepted. Every SEALED segment is
(re-)marked read-only (0444 on Unix) on open — self-healing after a
crash that landed between committing the manifest and applying that
permission, since the permission bit is defense-in-depth only, never
the source of truth (see the segment module).
Sourcepub fn open_segmented_anchored(
dir: &Path,
key: SigningKey,
retired: Vec<VerifyingKey>,
policy: RolloverPolicy,
anchor_path: &Path,
) -> Result<Self, LedgerError>
pub fn open_segmented_anchored( dir: &Path, key: SigningKey, retired: Vec<VerifyingKey>, policy: RolloverPolicy, anchor_path: &Path, ) -> Result<Self, LedgerError>
open_segmented plus the same fail-closed
anchor check open_anchored applies — a
segmented ledger’s committed height is anchored exactly the same way
as a single-file one’s (root+count don’t care how many files the bytes
span).
Sourcepub fn rotate(&mut self, new_key: SigningKey)
pub fn rotate(&mut self, new_key: SigningKey)
Rotate the signing key. Entries already written stay verifiable under the
retired key (kept in the keyring); every subsequent entry is signed by
new_key and carries its kid. The chain is uninterrupted — no re-signing of
the past, and no republish. On the next restart, pass the retired public key
to open_with_verifiers so the whole log still
verifies.
Sourcepub fn verifier_fingerprints(&self) -> Vec<String>
pub fn verifier_fingerprints(&self) -> Vec<String>
The public keys of every key trusted to have signed this log (retired + current), as hex fingerprints — what an auditor pins across a rotation.
Sourcepub fn set_sync(&mut self, sync: bool) -> &mut Self
pub fn set_sync(&mut self, sync: bool) -> &mut Self
Enable/disable fsync-per-append (see the sync field). Returns self for
builder-style config right after open.
Sourcepub fn append(&mut self, entry: Entry) -> Result<Record, LedgerError>
pub fn append(&mut self, entry: Entry) -> Result<Record, LedgerError>
Append entry to the log. entry.seq is assigned here; the serialized
bytes are hashed into the chain, signed, and written verbatim, so an
external verifier recomputes the exact same hash from what is on disk.
Sourcepub fn self_verify(&self) -> Result<VerifyReport, LedgerError>
pub fn self_verify(&self) -> Result<VerifyReport, LedgerError>
Re-read and verify this ledger’s own file against its whole keyring (every retired key plus the current one), so a rotated log verifies end-to-end. Used by the admin summary; O(entries), so not for the request hot path.
Sourcepub fn pubkey_hex(&self) -> String
pub fn pubkey_hex(&self) -> String
Hex of the Ed25519 public key that signs this ledger’s entries — the fingerprint an auditor pins.
Sourcepub fn raw_records(&self) -> Result<Vec<String>, LedgerError>
pub fn raw_records(&self) -> Result<Vec<String>, LedgerError>
Every stored line, verbatim and unparsed — for a caller who must hold this ledger’s lock as briefly as possible. The audit projection copies the bytes out under the lock and does every parse, match and proof after releasing it; what stays under the lock is one sequential read, not three parsing passes.
Sourcepub fn read_records(
&self,
offset: usize,
limit: usize,
) -> Result<Vec<Value>, LedgerError>
pub fn read_records( &self, offset: usize, limit: usize, ) -> Result<Vec<Value>, LedgerError>
A window of records for the admin ledger browser: skip offset, take up
to limit, each as its stored JSON object. Reads the file, so it’s an
admin/audit path, never the decision hot path.
Sourcepub fn read_raw_records(
&self,
offset: usize,
limit: usize,
) -> Result<Vec<Box<RawValue>>, LedgerError>
pub fn read_raw_records( &self, offset: usize, limit: usize, ) -> Result<Vec<Box<RawValue>>, LedgerError>
A window of records as their VERBATIM stored bytes — the exact line each
record was written as, preserved byte-for-byte (RawValue, no reparse).
This is what an EXTERNALLY-VERIFIABLE evidence bundle must ship: the hash
chain commits to the entry’s stored bytes (chain_hash(entry_bytes, prev)),
so a third party can only reproduce a record’s hash from those exact bytes.
read_records (which parses to Value) would re-serialize and reorder keys,
breaking the hash — use this whenever the bytes are the proof, not the data.
Sourcepub fn checkpoint(&self, ts_ms: u64) -> Checkpoint
pub fn checkpoint(&self, ts_ms: u64) -> Checkpoint
Sign the current head into a Checkpoint for external anchoring — the
operator-independent half of the audit story. Hand it to a notary / SCITT
transparency service / another party; they can later prove the log was not
rewritten below this point without trusting the operator.
Sourcepub fn tree_head(&self, ts_ms: u64) -> Result<TreeHead, LedgerError>
pub fn tree_head(&self, ts_ms: u64) -> Result<TreeHead, LedgerError>
Sign the current MERKLE tree head — the RFC 9162 root over all record hashes,
externally anchorable like checkpoint but enabling COMPACT
third-party inclusion/consistency proofs. tree_size == count. Signs through the
ledger key (a keyless hash committed by a key, same as a checkpoint).
Sourcepub fn sign_tree_head(
&self,
merkle_root: String,
tree_size: u64,
ts_ms: u64,
) -> TreeHead
pub fn sign_tree_head( &self, merkle_root: String, tree_size: u64, ts_ms: u64, ) -> TreeHead
Sign a tree head over an already-computed root — the signing half of
tree_head, for a caller who derived the leaves outside the
lock. Signs exactly what it is given: a root computed from a prefix that has since
been appended past is still a consistent commitment to that prefix, the same answer
the caller would have gotten before the append.
Sourcepub fn inclusion_proof(&self, seq: u64) -> Result<InclusionProof, LedgerError>
pub fn inclusion_proof(&self, seq: u64) -> Result<InclusionProof, LedgerError>
A compact RFC 9162 inclusion proof that the record at seq (0-based) is committed
by the current tree head’s root. Err if seq is past the end of the log.
Sourcepub fn inclusion_proofs(
&self,
seqs: &[u64],
) -> Result<Vec<InclusionProof>, LedgerError>
pub fn inclusion_proofs( &self, seqs: &[u64], ) -> Result<Vec<InclusionProof>, LedgerError>
Inclusion proofs for several records, over one pass of the log.
inclusion_proof derives every leaf in the log to prove
one record is in it, which is the right shape for one proof and the wrong shape for
a page of them: asking for m proofs that way reads and parses the whole log m
times, and does it holding the lock an append needs. This derives the leaves once.
Returns proofs in the order the sequences were given. A sequence past the end of the log fails the whole call rather than being skipped — a page of proofs with a hole in it, where the hole is silent, is worse than no page.
Sourcepub fn consistency_proof(
&self,
first_size: u64,
) -> Result<ConsistencyProof, LedgerError>
pub fn consistency_proof( &self, first_size: u64, ) -> Result<ConsistencyProof, LedgerError>
A compact RFC 9162 consistency proof that the log of the first first_size records
is an exact prefix of the current log — the operator-independent equivocation /
truncation check against an EARLIER anchored tree head. 1 <= first_size <= count.
Sourcepub fn snapshot_for_bundle(
&self,
ts_ms: u64,
) -> Result<EvidenceSnapshot, LedgerError>
pub fn snapshot_for_bundle( &self, ts_ms: u64, ) -> Result<EvidenceSnapshot, LedgerError>
A single-snapshot read for an evidence bundle: checkpoint, tree_head, and raw records
are ALL derived from the SAME in-memory (self.last_hash, self.next_seq) state captured
at one moment — unlike calling checkpoint()/tree_head()/read_raw_records() separately
(which lets an append() land between calls and make the three mutually inconsistent:
checkpoint.count != tree_head.tree_size or checkpoint.root computed over different
records than tree_head).
This is the single-file analog of ShardedLedger::evidence_snapshot.
The snapshot captures the log’s state at call time; a concurrent append() does not
change the returned values. Returns (count, raw_records, checkpoint, tree_head).
Sourcepub fn seal_anchor(
&self,
anchor_path: &Path,
ts_ms: u64,
) -> Result<Checkpoint, LedgerError>
pub fn seal_anchor( &self, anchor_path: &Path, ts_ms: u64, ) -> Result<Checkpoint, LedgerError>
Seal the current head into the persisted ANCHOR file — the last committed
height, durably recorded on THIS node (not only handed to an external notary).
On the next open_anchored /
verify_against_anchor the log must still
extend it, which is what makes a tail-truncation across a restart detectable
— a plain reopen accepts any internally-consistent shorter chain and cannot.
Sourcepub fn verify_against_anchor(
&self,
anchor_path: &Path,
) -> Result<(), LedgerError>
pub fn verify_against_anchor( &self, anchor_path: &Path, ) -> Result<(), LedgerError>
Fail-closed truncation/rewrite check against the persisted anchor. Call it
right after opening (or use open_anchored): if an
anchor exists it must (a) be signed by a key in this ledger’s keyring — a
forged anchor cannot be used to downgrade the committed height — and (b) still
be extended by the log (at least count records that re-derive root). Any
failure is Tamper: the log was truncated below, or rewritten at/below, its
last committed height. No anchor file ⇒ Ok (nothing committed yet).