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 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 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).